最近のサービス間のデータとイベントの連携について

こんにちは。牧本 (@makimoto) です。最近はバックエンドシステムの設計をやったりしています。

今回は複数のサービスが存在するとき、その間でどのようにデータ連携を実現するかついて述べていきます。

背景と問題定義

cookpad.com は世界有数の規模の Ruby on Rails で作られたウェブアプリケーションです。

巨大な Rails アプリケーションは単純に起動やデプロイ、テストが遅いという問題以上に、自分の変更が与える影響範囲を予測するのが困難という大きな問題があります。cookpad のメインレポジトリ (cookpad_all と呼ばれる) には1つの mountable engine を共有する Rails アプリケーションは7つがあり、困難さに拍車をかけています。cookpad_all を触る開発者は新しい機能を追加する、既存の機能に手を入れる、不用な機能を消すなど様々な場面でこの困難さと対峙することになります。

そのため、われわれは既存の機能を切り出したり、新機能を新しいアプリケーションとして実装するという取り組みを行なっています。その際問題となることの一つに、複数のサービス間から必要とされるデータをどのように共有したり、またそれらデータの変更などをどのように伝播させるかというものがあります。

1つのデータストレージがモデルロジックの異なる複数のサービスで共有されるのはデータの不備や不整合の温床となるので原則として行ないません。*1 そのため、一方のサービスのデータを他方で使いたい場合は、直接データストレージにアクセスするのではなく、データを管理するサービスを通して取得するというのが基本となります。

そのような複数のサービスの間でデータを連携したいという状況では、一方のサービスで発生した変更を受けて他方のサービスで処理をする、といった場面が増えてきます。たとえば、「あるユーザーが退会したことにより、他のサービスに存在するそのユーザーに関連するアイテムを削除してアクセスできなくする」といった場合です。

これらの課題を現実的なコストで解決することが必要となってきます。

アプローチ

Garage と GarageClient の導入による RESTful Hypermedia API の社内標準化

まず、データ連携の第一歩としてウェブ API によるデータの作成・閲覧・更新・削除についてです。

クックパッドではウェブ API の開発・利用には Garage とそのクライアントである GarageClient を用いています。 Garage は RESTful Hypermedia API を Ruby on Rails 上で開発するためのライブラリであり、クックパッドで4年近くの開発・利用されています。Garage 自体については過去の紹介記事を参照いただくことにして詳細は割愛します。

共通の API 実装のためのライブラリを用いることで、各サービス間のインターフェースの差異をなくし、NantokaClient を乱立させることなくスムーズにサービスが管理しているデータにアクセスできるようにしています。また、Garage は認証認可の機能も提供しているので、共通基盤である認証システムと連携させることで適切なアクセス制御も実現できるようになっています。

イベントにもとづくデータの連携

前節では各サービスの管理しているデータにアクセスする方法を説明しました。 次は前述した「あるユーザーが退会したことにより、他のサービスに存在するそのユーザーに関連するアイテムを削除してアクセスできなくする」というケースについて考えていきます。

シンプルな方法

複数のサービスをまたいだデータの連携を実現するためのもっとも単純な方法は、データの変更があったタイミングで、そのデータに依存しているサービスに対し処理を促すリクエストを行なう方法です。この方法は、2つのサービス間でしか連携がないことがわかっている場合はうまく動きます。しかし、サービスが増えてくると複雑さが増していきます。また、この方法では、データの提供元と提供先の両方の実装に手を入れる必要があるため開発自体も煩雑になります。

Amazon SNS を用いた pub-sub メッセージング

そこでクックパッドでは、 Amazon Web Services の提供する Simple Notification Service (SNS) を用いた pub-sub メッセージングを採用しています。

これは以下の流れで実現します:

  1. あるイベント (たとえば、ユーザーの削除など) が発生するとそれに対応する SNS トピックに通知を行なう *2
  2. SNS はトピックを購読している HTTPS エンドポイントに対しイベントが発生した旨を通知する
  3. 通知を受けた HTTPS エンドポイントのサービスはその通知内容に応じて処理を行なう

なお、 SNS のメッセージには 256 KB のサイズ制限があるので、SNS のメッセージとしてはデータの ID のみを渡して、データ本体は Garage 経由で取得するという方式がよく取られます。

これらは Ping という名前の社内ライブラリによって実現されており、各サービスで簡単に導入できるようになっています。

この方法の問題点

しかしながら、この方法はいくつかの問題があります:

  • 本来内部通信しかないサービスであったとしても HTTPS エンドポイントを SNS の通知を受け取るためにインターネットに公開する必要がある
  • 通知を HTTP リクエストとして受けるので、リクエスト数が増えたらアプリケーションサーバ (Unicorn であるケースが多い) が詰まるリスクがある

Barbeque と Amazon SQS を用いたファンアウト

これを解決するため、通知の広報先を HTTPS エンドポイントから AWS の Simple Queue Service (SQS) のキューに変更し、そのキューをポーリングしてジョブを実行するという方法に転換しようとしています。 (いわゆるファンアウト)

この仕組みは、クックパッドのジョブキューシステムである Barbeque の機能として実装しています。 Barbeque は SQS をキューとして使っているため、実装的には、キューとジョブと SNS トピックを関連付けてキューでトピックを購読する機能を追加するだけでこの仕組みは実現可能でした。 (https://github.com/cookpad/barbeque/pull/20)

この方法での流れは以下の通りです:

  1. あるサービスでイベント (たとえば、ユーザーが退会する、など) が発生すると、サービスは SNS トピックに通知を行なう
  2. SNS はトピックを購読している SQS のキューに対しイベントが発生した旨を通知する
  3. Barbeque のワーカーが対象のキューをポーリングし、通知を受けとる
  4. Barbeque のワーカーが通知をもとに関連付けられたジョブを実行する

Barbeque は Docker の利用を前提としたジョブキューシステムなので、ジョブの実行は Docker コンテナ内で行なわれます。すでに稼動しているサービスとは別のコンテナでジョブが実行されるため、ジョブが増えたことによりリクエストが増えて Unicorn のワーカーが詰まる問題を避け、よりスケーラブルなジョブ実行環境となります。クックパッドでは、現状ほとんどすべての新規サービスが Docker コンテナ上で動いているため、この仕組みの恩恵を十分に受けることができます。

まとめ

本稿では、クックパッドでどのように複数のサービス間でのデータ連携を行なっているかについて述べました。 Docker、AWS のメッセージング系のサービス、内製のソフトウェアを組み合わせて複数のサービスで協調してデータを扱うことを実現しています。

サービスを分割するときの心配事の1つとして、既存サービスのデータとうまく連動させるのが難しいというのがありますが、今回紹介した方法である程度は解決できると考えています。

*1:ただし、既存サービスから機能を徐々に切り出す場合 などのケースで例外はあります。

*2:実際の運用では SNS にリクエストを送るために fluentd を経由させています。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/