Download - Cloud Developer Day 2016 ダウンロード資料
クックパッドが どのように
Microservices化してきたかクックパッド株式会社 技術部 開発基盤グループ
吉川 崇倫
1
2
デイリー5,000アクセスのサービス どんな構成にしますか?
3
1) 一応冗長化して2台App + DB 2) 数百台までオートスケールできる構成
4
サービス要求に対して 適切な構成を考える
5
スクラッチでサービスを立ち上げる どんな構成にしますか?
6
1) Monolithic アーキテクチャ 2) Microservices アーキテクチャ
7
Microservices いつやるべき?
8
9
ここまで必要?
10
• 最初からMicroservicesではない • フェーズごとの工夫の積み重ね • クックパッドの構成の変遷を辿る
11
※注意
話の流れをわかりやすくするために トピックごとの時系列が前後したり 近いトピックをまとめたりしています
12
• 黎明期
• 単一サービス、モデル共有、用途ごとのインフラ
• サービス拡大期
• アプリケーション肥大化への対応と限界
• 脱Monolithic期
• 統一的なAPI、共通基盤、サービス分割
• Microservices化期
• サービス分割の弊害を解消する、分割コストの低下
13
黎明期
14
単一のRails
cookpad
15
用途ごとにサーバーを分けたい• インフラの性質の違いでわけて管理したい
• APIは別ドメインで配信する
• バックグラウンドジョブ用のサーバーをわける
• DBやキャッシュ管理なども必要に応じてそれぞれ分けたい
• アプリケーションの実装はまとめておきたい
16
puppetのroleをわけて別々に構成管理puppetがアプリケーションのconfigを配布
cookpad (web)
cookpad (api)
cookpad (background worker)
アプリケーションコードは全て同じ。別モードとして動作させる
17
アプリケーション間でモデルを共有したい• 機能が大きく異なるアプリケーションを分けたい
• PC/スマートフォン向けサイト
• フィーチャーフォン向けサイト
• もともとフィーチャーフォン向け別サービスだったのを統合した
• スタッフ向け管理サイト
• データは同じものを見て、モデルロジックも共有したい18
モデルや共通ライブラリ部分をsharedとして独立 各アプリケーションからはsymlinkで参照
cookpad(web, api…) cookpad(admin)
cookpad (feature phone)
19
20
sharedを含め全アプリケーションが一つのリポジトリに同居 「本体」「cookpad_all」と呼ばれる
サービス拡大期
21
レシピ以外のサービスが生まれ始める
• レシピをのせる、さがす以外の機能
• 特売情報、料理教室などのサービスにつながっていく
• プロトタイプを作って価値検証を繰り返す
• プロトタイプなので継続して使うかは不明
• 工数を最小限にするために、既存の資産を最大限活用
22
別アプリケーションとする場合もsharedを共有
本体にあいのり (本体は巨大化)
認証、決済、ログなどは 本体のモジュールを直接使う
必要ならサービスごとの処理を直接実装
23
CIにかかる時間が肥大化• ただでさえテストがどんどん増えて時間がかかる
• sharedを通じて全部つながっているので全然違う箇所で思わぬ影響が出てテストが失敗する
• 修正して再実行にまた時間がかかる
• あるチームがCIをfailさせると他のチームもデプロイできない
• デプロイまでの待ち時間を最小化したい24
remote_spec 複数台のサーバーで テストを分散実行
10分以内に 終わるように
25
テストはさらに増え続けた
• 開発がピーク時のテストをさばくためのワーカー数増
• 一方休日は全然要らない
• 柔軟に増減させたい
• ワーカーの障害やたまたま失敗するexampleの影響増
26
RRRSpec
• github.com/cookpad/rrrspec
• EC2スポットインスタンスを利用してコストを抑える
• 失敗したテストの自動再実行
27
デプロイに時間がかかる• capistrano
• 要するにssh接続してデプロイ
• 台数も多く1回のデプロイで10分以上かかる
• sshで失敗する頻度が増加
• リトライする羽目になりさらに時間がかかる
28
mamiya• github.com/sorah/mamiya
• Serfでオーケストレーション
• CIが通ったらtar ballにしてS3にアップ
• 開発者が準本番環境で動作確認している間に配布して準備
• デプロイコマンドでは切り替えるだけ
29
開発環境が重すぎる• 手元でrailsを起動してトップ表示するまで1分以上かかる
• とにかくgemや発行クエリが多すぎる
• 開発環境のパフォーマンスチューニング
• 開発環境でもキャッシュを活用するようになる
• 不要なコード棚卸しの啓蒙
30
修正が及ぼす影響範囲がわからない• 黎明期は共通モジュールとして切り出したりAPIを整備するのではなく、直接使ったり拡張したりする荒々しいスタイル
• sharedで複数のアプリケーションがつながっていてどう使われるのかわからない
• ライブラリもモンキーパッチやフォークしているものが多く知っている挙動と違う
31
機能の境界があいまい• どのチームが担当しているのかがあいまい
• 体制変更などでの引き継ぎ漏れも発生しがち
• プロダクトオーナーはAチームだけど実質運用や実装はBチームがやっちゃってる
• 機能変更するときに影響受けるチームが多い
• チームをまたいだ調整とコミュニケーションが大変32
どうすれば・・・
33
脱Monolithic期
34
脱本体/脱sharedしたい• 既存の資産を使えるといっても辛みが強くなってきた
• モジュール自体がサービスと密結合しており結局再利用しづらい
• スクラッチで作ればコードベースも小さく済む
• 依存が少なく開発環境の動作やテストも速い
• 少なくとも新規で作るなら分けたほうがよいのでは
35
認証など本体とのやりとりは APIを通じて行うコードもDBも
本体から独立
決済のように 共通して利用できる機能を切り出し
切り出された機能は 本体からもAPI連携
36
APIを通してリソースを扱う• iOS/Androidアプリが高機能化
• WebViewで提供していたものもネイティブ実装化
• APIで本体のリソースを細かく制御できるようにする
• インターフェイスを統一したい
• フォーマットの異なるAPIが複数存在していた
• APIごとにxxx-client37
RESTful hypermedia APIGET /v1/users/123
{ "id": 123, "name": "パド美", "kitchen": { "id": 15, "created": "2016-03-10T10:59:24+09:00", } }, "_links": { "self": { "href": "/v1/users/123" }, "recipes": { "href": "/v1/users/123/recipes" }, } }
リソース間の関連を リンクとして表現できる
38
Garage• github.com/cookpad/garage
class Employee < ActiveRecord::Baseinclude Garage::Representer
belongs_to :division has_many :projects
property :id property :title property :division, selectable: true collection :projects, selectable: true
link(:division) { division_path(division) }link(:projects) { employee_projects_path(self) }
def self.build_permissions(perms, other, target) perms.permits! :readend
end
リソースとしての 属性やリンクを定義
認可条件も定義できる
39
GarageClient• github.com/cookpad/garage_client
# GET https://garage.example.com/v1/meuser = client.get("/me")user.id user.name
# GET https://garage.example.com/v1/recipesrecipes = client.get("/recipes")recipes.total_count recipes[0].id recipes[0].name
リソースを文字列で指定 APIの実装が増えても
クライアントはそのまま使える
どのサービスを使うときも 使い方は同じ
各エンドポイントの仕様だけ わかればよい
40
Autodoc• github.com/r7kamura/autodoc
# spec/requests/users_spec.rbdescribe "Users" do describe "GET /v1/me", autodoc: true do it "returns a current_resource_owner" do get "/v1/me", params: {}, headers: headers
expect(response).to have_http_status(:success)expect(response.body).to be_json_as(
id: resource_owner.id, name: resource_owner.name
)end
endend
テストからドキュメントを 自動生成
41
認証、ログ、通知など様々な共通基盤サービスが整備され
ていった
shared連携していたサービスや 本体内から卒業するサービスが
出てきた
本体は依存関係等の問題で garage APIを別モードで起動 アプリも別サービスも 同じAPIを使う
各サービス間も garage APIで連携
42
Microservices化期
43
構成管理が大変• Web/バッチなど用途に応じて構成管理を分けるスタイル
• appサーバーは独立したrole
• バッチジョブは共通のバッチワーカーの一部として管理
• 設定の配布漏れ
• アプリケーションが同居すると権限管理が困難
• IAM Roleや秘匿値の管理など44
Docker• ポータブルな実行環境
• staging/production/バッチ 同じイメージ
• 手元で検証することもできる
• Dockerfileはアプリケーションリポジトリに置く
• アプリケーション固有の情報が集約
• 環境依存値は環境変数としてデプロイ時に注入45
etcenv/etcvault• github.com/sorah/etcenv
• etcdで設定値を管理
• github.com/sorah/etcvault
• etcdの値を暗号化
• もともとetcdにACLが無かったため作られた
• 指定の を持っているインスタンスでしか復号できない46
etcweb• github.com/sorah/etcweb
WebUIで全サービスの 設定値を一括管理
秘匿値を扱うので 権限を持つ人だけが操作
47
Kuroko2• github.com/cookpad/kuroko2
• 全サービスのバッチジョブやワークフローを管理
• ワーカーが最新のDockerイメージを取得して実行
Webでジョブ管理 ログ閲覧
再実行までできる
48
Barbeque• github.com/cookpad/barbeque
• Kuroko2同様Dockerコンテナでジョブ実行できる
49
Docker運用の洗練• 当初は小さなツールを組み合わせて管理していた
• 統一してコントロールするものがない
• 設定するものが結構多い
• ホストの管理の手間
• ECSの登場
• AWSマネージドなコンテナサービス50
Hako• github.com/eagletmt/hako
• Dockerアプリ用のデプロイツール
• クックパッドではバックエンドにECSを利用
• アプリケーションごとの設定はYAMLで管理
• 環境ごとに環境変数を設定できる
• 秘匿値のみetcenv/etcvaultで管理51
煩雑な管理からの解放• とにかくDockerイメージがあれば様々な用途で実行できる
• クラウド上の権限管理がシンプルに
• ECS TaskにIAM Roleで権限付与
• バッチジョブはデプロイの概念がなくなった
• その時点で最新のイメージを取得して実行される
• sshしてログ調査することはなくなり、Web UIで確認52
アプリケーション追加が低コスト化• DockerイメージがあればOK
• web/batch/job queueどこでも使える
• garage_clientだけ入れておけば他のサービスが使える
• 以前ならモードをわけたりしていたものも新規で追加した方が楽
• サービスの粒度は次第に小さくなり、増える速度も増していく
53
Backend For Frontend
リソースは同じもの 提供したいが
認証方式が異なるなど 特殊なクライアント
専用バックエンド
54
API仕様の齟齬による事故• 破壊的な仕様変更による事故
• 多くの場合は意図したものではなく発生
• 意図している場合もどこから利用されているのか把握しづらい
• テストは?
• 自動テストでは他サービスのAPIはスタブしている
• APIプロバイダ側が仕様変更してもスタブは更新されないので検知できない
55
vcr• github.com/vcr/vcr
• テスト時に実際のHTTPのリクエストとレスポンスを記録
• 次回以降のリクエストでは記録したカセットデータから挙動を再現する
• 実リクエストからスタブを生成する
56
Rack::VCR• github.com/miyagawa/rack-vcr
• APIプロバイダ側が自身をテストしたカセットデータを記録・配布
• APIクライアント側はテスト時APIプロバイダからカセットデータを取得して自身をテストする際のスタブとする
• クライアントは常に最新の挙動のスタブを使ってテストできる
57
CI実行頻度の差が大きい場合の問題• 開発が活発で一日に何度もCI実行+デプロイされるものもあれば、一日に一回程度しかCIが実行されないものもある
• Rack::VCRはクライアント側で実行されてはじめて仕様が壊れたことを検知できる
• クライアント側のCI頻度が少ないと、APIプロバイダ側の変更を検知する前にデプロイされてしまう
58
Consumer-Driven Contract testing
• クライアント(Consumer)がAPIをスタブしてテスト
• 期待する振る舞いを契約(Contract)としてAPI側に渡す
• APIプロバイダ側は自身がContractを満たしているかをテストする
• APIプロバイダ側のCIでテストできるので、問題になるリビジョンをデプロイする前に検知できる
http://martinfowler.com/articles/consumerDrivenContracts.html
59
Pact• github.com/realestate-com-au/pact
• クライアント側
• CIでpactファイルを生成
• APIプロバイダ側
• CIでpactファイルをverify
https://github.com/realestate-com-au/pact#how-does-it-work60
Pact(Consumer)describe 'get_all' dolet(:recipe_a) { { id: Pact.like(1), name: Pact.like('Curry') } }let(:recipe_b) { { id: Pact.like(2), name: Pact.like('Salada') } }
before do provider_app.given('there are 2 recipes').
upon_receiving('a request for recipes').with(method: :get, path: '/v1/recipes').will_respond_with(
status: 200, headers: {
'Content-Type' => Pact.term( generate: 'application/json', matcher: %r{application/json} ),
}, body: [recipe_a, recipe_b]
)end
it 'returns recipes' do recipes = described_class.get_all
expect(recipes.size).to eq(2)expect(recipes.first.name).to eq('Curry')
endend
テストは通常と同じ
Pactでスタブ
61
Pactfile{ "consumer": { "name": "ConsumerApp" }, "provider": { "name": "ProviderApp" }, "interactions": [ { "description": "a request for recipes", "provider_state": "there are 2 recipes", "request": { "method": "get", "path": "/v1/recipes" }, "response": { "status": 200, "headers": { "Content-Type": { "json_class": "Pact::Term", "data": { "generate": "application/json", "matcher": { "json_class": "Regexp", "o": 0, "s": "application/json" }
リクエストと レスポンスの
内容をjsonで出力
62
Pact(Provider)• API側
• シナリオに沿ったセットアップコードだけを用意
Pact.provider_states_for 'ConsumerApp' do provider_state "there are 2 recipes" do set_up do
%w[Curry Salada].each {|name| Recipe.create!(name: name) }end
endend
63
障害の伝播• 実装にバグがなかろうと障害は起こりうる
• あるサービスが障害になるとそれを利用しているサービスに伝播
• リクエストごとにタイムアウトまで待つことでクライアント側も詰まる
• クライアント側のリトライ機構がAPIプロバイダにとどめを刺す
64
Circuit Breaker
• 平常時の失敗はクライアントはリトライできる
• APIプロバイダがダウンしている場合はリクエストしないようにする
• タイムアウト待ちもしない
• しばらくしてプロバイダが復活したらまたリクエストを再開する
65
Expeditor• github.com/cookpad/expeditor
EXPEDITOR_SERVICE = Expeditor::Service.new( non_break_count: 20, threshold: 0.2, period: 10, sleep: 5,)
def notification_counts Expeditor::Command.new(service: EXPEDITOR_SERVICE, timeout: 3) { response = client.get("/notifications/counts", params, options) response.notification_count}.set_fallback {|e|
Raven.capture_exception(e)0
}.start(current_thread: true).getend
Circuit Breakerの閾値を設定
エラー時はログを記録して デフォルト値を返す
66
Expeditor• 平常時
67
クライアントサービスクライアントサービス
オプションに応じて リトライしたり どうしてもだめなら fallback処理
APIプロバイダ
Expeditor• 障害時
68
クライアントサービスクライアントサービス
リクエストせず 最初からfallback処理
APIプロバイダAPIプロバイダ
サービス分割時に困るポイントが解決されてきた• APIが乱立してどこをどう使えばいいのかわからなくなる
• Garageでどのサービスも同じように使える
• 毎回サーバー作って構成管理するのが大変
• DockerfileとYAMLを書けばHakoで環境用意できる
• サービス境界でバグや障害になりやすい
• Pactが互換性を担保しExpeditorが障害の伝播を防ぐ69
サービスを分割することで得られる良いこと• コードベースが小さい
• テストや開発環境の動作が速い
• 変更による影響範囲が小さい
• 既存実装による制約が小さい
• チームにあった実装ルール
• 機能に適したライブラリ、言語の選択70
サービスを分割することで得られる良いこと
• システム境界が明確
• チーム内でコミュニケーションが完結する
• 完結できない場合境界としてあまりよくない
71
まとめ• 最初から、あるいはある日突然Microservicesになったわけではない
• その都度問題になったことに対応してきた積み重ね
• モノリシックなシステムは徐々に辛みが大きくなっていく
• サービスを分けることで発生する問題は技術で回避できる
• テクニックを知りフェーズに応じた設計をするのが重要
72