72
クックパッドが どのように Microservices化してきたか クックパッド株式会社 技術部 開発基盤グループ 吉川 崇倫 1

Cloud Developer Day 2016 ダウンロード資料

Embed Size (px)

Citation preview

Page 1: Cloud Developer Day 2016 ダウンロード資料

クックパッドが どのように

Microservices化してきたかクックパッド株式会社 技術部 開発基盤グループ

吉川 崇倫

1

Page 2: Cloud Developer Day 2016 ダウンロード資料

2

Page 3: Cloud Developer Day 2016 ダウンロード資料

デイリー5,000アクセスのサービス どんな構成にしますか?

3

Page 4: Cloud Developer Day 2016 ダウンロード資料

1) 一応冗長化して2台App + DB 2) 数百台までオートスケールできる構成

4

Page 5: Cloud Developer Day 2016 ダウンロード資料

サービス要求に対して 適切な構成を考える

5

Page 6: Cloud Developer Day 2016 ダウンロード資料

スクラッチでサービスを立ち上げる どんな構成にしますか?

6

Page 7: Cloud Developer Day 2016 ダウンロード資料

1) Monolithic アーキテクチャ 2) Microservices アーキテクチャ

7

Page 8: Cloud Developer Day 2016 ダウンロード資料

Microservices いつやるべき?

8

Page 9: Cloud Developer Day 2016 ダウンロード資料

9

Page 10: Cloud Developer Day 2016 ダウンロード資料

ここまで必要?

10

Page 11: Cloud Developer Day 2016 ダウンロード資料

• 最初からMicroservicesではない • フェーズごとの工夫の積み重ね • クックパッドの構成の変遷を辿る

11

Page 12: Cloud Developer Day 2016 ダウンロード資料

※注意

話の流れをわかりやすくするために トピックごとの時系列が前後したり 近いトピックをまとめたりしています

12

Page 13: Cloud Developer Day 2016 ダウンロード資料

• 黎明期

• 単一サービス、モデル共有、用途ごとのインフラ

• サービス拡大期

• アプリケーション肥大化への対応と限界

• 脱Monolithic期

• 統一的なAPI、共通基盤、サービス分割

• Microservices化期

• サービス分割の弊害を解消する、分割コストの低下

13

Page 14: Cloud Developer Day 2016 ダウンロード資料

黎明期

14

Page 15: Cloud Developer Day 2016 ダウンロード資料

単一のRails

cookpad

15

Page 16: Cloud Developer Day 2016 ダウンロード資料

用途ごとにサーバーを分けたい• インフラの性質の違いでわけて管理したい

• APIは別ドメインで配信する

• バックグラウンドジョブ用のサーバーをわける

• DBやキャッシュ管理なども必要に応じてそれぞれ分けたい

• アプリケーションの実装はまとめておきたい

16

Page 17: Cloud Developer Day 2016 ダウンロード資料

puppetのroleをわけて別々に構成管理puppetがアプリケーションのconfigを配布

cookpad (web)

cookpad (api)

cookpad (background worker)

アプリケーションコードは全て同じ。別モードとして動作させる

17

Page 18: Cloud Developer Day 2016 ダウンロード資料

アプリケーション間でモデルを共有したい• 機能が大きく異なるアプリケーションを分けたい

• PC/スマートフォン向けサイト

• フィーチャーフォン向けサイト

• もともとフィーチャーフォン向け別サービスだったのを統合した

• スタッフ向け管理サイト

• データは同じものを見て、モデルロジックも共有したい18

Page 19: Cloud Developer Day 2016 ダウンロード資料

モデルや共通ライブラリ部分をsharedとして独立 各アプリケーションからはsymlinkで参照

cookpad(web, api…) cookpad(admin)

cookpad (feature phone)

19

Page 20: Cloud Developer Day 2016 ダウンロード資料

20

sharedを含め全アプリケーションが一つのリポジトリに同居 「本体」「cookpad_all」と呼ばれる

Page 21: Cloud Developer Day 2016 ダウンロード資料

サービス拡大期

21

Page 22: Cloud Developer Day 2016 ダウンロード資料

レシピ以外のサービスが生まれ始める

• レシピをのせる、さがす以外の機能

• 特売情報、料理教室などのサービスにつながっていく

• プロトタイプを作って価値検証を繰り返す

• プロトタイプなので継続して使うかは不明

• 工数を最小限にするために、既存の資産を最大限活用

22

Page 23: Cloud Developer Day 2016 ダウンロード資料

別アプリケーションとする場合もsharedを共有

本体にあいのり (本体は巨大化)

認証、決済、ログなどは 本体のモジュールを直接使う

必要ならサービスごとの処理を直接実装

23

Page 24: Cloud Developer Day 2016 ダウンロード資料

CIにかかる時間が肥大化• ただでさえテストがどんどん増えて時間がかかる

• sharedを通じて全部つながっているので全然違う箇所で思わぬ影響が出てテストが失敗する

• 修正して再実行にまた時間がかかる

• あるチームがCIをfailさせると他のチームもデプロイできない

• デプロイまでの待ち時間を最小化したい24

Page 25: Cloud Developer Day 2016 ダウンロード資料

remote_spec 複数台のサーバーで テストを分散実行

10分以内に 終わるように

25

Page 26: Cloud Developer Day 2016 ダウンロード資料

テストはさらに増え続けた

• 開発がピーク時のテストをさばくためのワーカー数増

• 一方休日は全然要らない

• 柔軟に増減させたい

• ワーカーの障害やたまたま失敗するexampleの影響増

26

Page 27: Cloud Developer Day 2016 ダウンロード資料

RRRSpec

• github.com/cookpad/rrrspec

• EC2スポットインスタンスを利用してコストを抑える

• 失敗したテストの自動再実行

27

Page 28: Cloud Developer Day 2016 ダウンロード資料

デプロイに時間がかかる• capistrano

• 要するにssh接続してデプロイ

• 台数も多く1回のデプロイで10分以上かかる

• sshで失敗する頻度が増加

• リトライする羽目になりさらに時間がかかる

28

Page 29: Cloud Developer Day 2016 ダウンロード資料

mamiya• github.com/sorah/mamiya

• Serfでオーケストレーション

• CIが通ったらtar ballにしてS3にアップ

• 開発者が準本番環境で動作確認している間に配布して準備

• デプロイコマンドでは切り替えるだけ

29

Page 30: Cloud Developer Day 2016 ダウンロード資料

開発環境が重すぎる• 手元でrailsを起動してトップ表示するまで1分以上かかる

• とにかくgemや発行クエリが多すぎる

• 開発環境のパフォーマンスチューニング

• 開発環境でもキャッシュを活用するようになる

• 不要なコード棚卸しの啓蒙

30

Page 31: Cloud Developer Day 2016 ダウンロード資料

修正が及ぼす影響範囲がわからない• 黎明期は共通モジュールとして切り出したりAPIを整備するのではなく、直接使ったり拡張したりする荒々しいスタイル

• sharedで複数のアプリケーションがつながっていてどう使われるのかわからない

• ライブラリもモンキーパッチやフォークしているものが多く知っている挙動と違う

31

Page 32: Cloud Developer Day 2016 ダウンロード資料

機能の境界があいまい• どのチームが担当しているのかがあいまい

• 体制変更などでの引き継ぎ漏れも発生しがち

• プロダクトオーナーはAチームだけど実質運用や実装はBチームがやっちゃってる

• 機能変更するときに影響受けるチームが多い

• チームをまたいだ調整とコミュニケーションが大変32

Page 33: Cloud Developer Day 2016 ダウンロード資料

どうすれば・・・

33

Page 34: Cloud Developer Day 2016 ダウンロード資料

脱Monolithic期

34

Page 35: Cloud Developer Day 2016 ダウンロード資料

脱本体/脱sharedしたい• 既存の資産を使えるといっても辛みが強くなってきた

• モジュール自体がサービスと密結合しており結局再利用しづらい

• スクラッチで作ればコードベースも小さく済む

• 依存が少なく開発環境の動作やテストも速い

• 少なくとも新規で作るなら分けたほうがよいのでは

35

Page 36: Cloud Developer Day 2016 ダウンロード資料

認証など本体とのやりとりは APIを通じて行うコードもDBも

本体から独立

決済のように 共通して利用できる機能を切り出し

切り出された機能は 本体からもAPI連携

36

Page 37: Cloud Developer Day 2016 ダウンロード資料

APIを通してリソースを扱う• iOS/Androidアプリが高機能化

• WebViewで提供していたものもネイティブ実装化

• APIで本体のリソースを細かく制御できるようにする

• インターフェイスを統一したい

• フォーマットの異なるAPIが複数存在していた

• APIごとにxxx-client37

Page 38: Cloud Developer Day 2016 ダウンロード資料

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

Page 39: Cloud Developer Day 2016 ダウンロード資料

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

Page 40: Cloud Developer Day 2016 ダウンロード資料

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

Page 41: Cloud Developer Day 2016 ダウンロード資料

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

Page 42: Cloud Developer Day 2016 ダウンロード資料

認証、ログ、通知など様々な共通基盤サービスが整備され

ていった

shared連携していたサービスや 本体内から卒業するサービスが

出てきた

本体は依存関係等の問題で garage APIを別モードで起動 アプリも別サービスも 同じAPIを使う

各サービス間も garage APIで連携

42

Page 43: Cloud Developer Day 2016 ダウンロード資料

Microservices化期

43

Page 44: Cloud Developer Day 2016 ダウンロード資料

構成管理が大変• Web/バッチなど用途に応じて構成管理を分けるスタイル

• appサーバーは独立したrole

• バッチジョブは共通のバッチワーカーの一部として管理

• 設定の配布漏れ

• アプリケーションが同居すると権限管理が困難

• IAM Roleや秘匿値の管理など44

Page 45: Cloud Developer Day 2016 ダウンロード資料

Docker• ポータブルな実行環境

• staging/production/バッチ 同じイメージ

• 手元で検証することもできる

• Dockerfileはアプリケーションリポジトリに置く

• アプリケーション固有の情報が集約

• 環境依存値は環境変数としてデプロイ時に注入45

Page 46: Cloud Developer Day 2016 ダウンロード資料

etcenv/etcvault• github.com/sorah/etcenv

• etcdで設定値を管理

• github.com/sorah/etcvault

• etcdの値を暗号化

• もともとetcdにACLが無かったため作られた

• 指定の を持っているインスタンスでしか復号できない46

Page 47: Cloud Developer Day 2016 ダウンロード資料

etcweb• github.com/sorah/etcweb

WebUIで全サービスの 設定値を一括管理

秘匿値を扱うので 権限を持つ人だけが操作

47

Page 48: Cloud Developer Day 2016 ダウンロード資料

Kuroko2• github.com/cookpad/kuroko2

• 全サービスのバッチジョブやワークフローを管理

• ワーカーが最新のDockerイメージを取得して実行

Webでジョブ管理 ログ閲覧

再実行までできる

48

Page 49: Cloud Developer Day 2016 ダウンロード資料

Barbeque• github.com/cookpad/barbeque

• Kuroko2同様Dockerコンテナでジョブ実行できる

49

Page 50: Cloud Developer Day 2016 ダウンロード資料

Docker運用の洗練• 当初は小さなツールを組み合わせて管理していた

• 統一してコントロールするものがない

• 設定するものが結構多い

• ホストの管理の手間

• ECSの登場

• AWSマネージドなコンテナサービス50

Page 51: Cloud Developer Day 2016 ダウンロード資料

Hako• github.com/eagletmt/hako

• Dockerアプリ用のデプロイツール

• クックパッドではバックエンドにECSを利用

• アプリケーションごとの設定はYAMLで管理

• 環境ごとに環境変数を設定できる

• 秘匿値のみetcenv/etcvaultで管理51

Page 52: Cloud Developer Day 2016 ダウンロード資料

煩雑な管理からの解放• とにかくDockerイメージがあれば様々な用途で実行できる

• クラウド上の権限管理がシンプルに

• ECS TaskにIAM Roleで権限付与

• バッチジョブはデプロイの概念がなくなった

• その時点で最新のイメージを取得して実行される

• sshしてログ調査することはなくなり、Web UIで確認52

Page 53: Cloud Developer Day 2016 ダウンロード資料

アプリケーション追加が低コスト化• DockerイメージがあればOK

• web/batch/job queueどこでも使える

• garage_clientだけ入れておけば他のサービスが使える

• 以前ならモードをわけたりしていたものも新規で追加した方が楽

• サービスの粒度は次第に小さくなり、増える速度も増していく

53

Page 54: Cloud Developer Day 2016 ダウンロード資料

Backend For Frontend

リソースは同じもの 提供したいが

認証方式が異なるなど 特殊なクライアント

専用バックエンド

54

Page 55: Cloud Developer Day 2016 ダウンロード資料

API仕様の齟齬による事故• 破壊的な仕様変更による事故

• 多くの場合は意図したものではなく発生

• 意図している場合もどこから利用されているのか把握しづらい

• テストは?

• 自動テストでは他サービスのAPIはスタブしている

• APIプロバイダ側が仕様変更してもスタブは更新されないので検知できない

55

Page 56: Cloud Developer Day 2016 ダウンロード資料

vcr• github.com/vcr/vcr

• テスト時に実際のHTTPのリクエストとレスポンスを記録

• 次回以降のリクエストでは記録したカセットデータから挙動を再現する

• 実リクエストからスタブを生成する

56

Page 57: Cloud Developer Day 2016 ダウンロード資料

Rack::VCR• github.com/miyagawa/rack-vcr

• APIプロバイダ側が自身をテストしたカセットデータを記録・配布

• APIクライアント側はテスト時APIプロバイダからカセットデータを取得して自身をテストする際のスタブとする

• クライアントは常に最新の挙動のスタブを使ってテストできる

57

Page 58: Cloud Developer Day 2016 ダウンロード資料

CI実行頻度の差が大きい場合の問題• 開発が活発で一日に何度もCI実行+デプロイされるものもあれば、一日に一回程度しかCIが実行されないものもある

• Rack::VCRはクライアント側で実行されてはじめて仕様が壊れたことを検知できる

• クライアント側のCI頻度が少ないと、APIプロバイダ側の変更を検知する前にデプロイされてしまう

58

Page 59: Cloud Developer Day 2016 ダウンロード資料

Consumer-Driven Contract testing

• クライアント(Consumer)がAPIをスタブしてテスト

• 期待する振る舞いを契約(Contract)としてAPI側に渡す

• APIプロバイダ側は自身がContractを満たしているかをテストする

• APIプロバイダ側のCIでテストできるので、問題になるリビジョンをデプロイする前に検知できる

http://martinfowler.com/articles/consumerDrivenContracts.html

59

Page 60: Cloud Developer Day 2016 ダウンロード資料

Pact• github.com/realestate-com-au/pact

• クライアント側

• CIでpactファイルを生成

• APIプロバイダ側

• CIでpactファイルをverify

https://github.com/realestate-com-au/pact#how-does-it-work60

Page 61: Cloud Developer Day 2016 ダウンロード資料

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

Page 62: Cloud Developer Day 2016 ダウンロード資料

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

Page 63: Cloud Developer Day 2016 ダウンロード資料

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

Page 64: Cloud Developer Day 2016 ダウンロード資料

障害の伝播• 実装にバグがなかろうと障害は起こりうる

• あるサービスが障害になるとそれを利用しているサービスに伝播

• リクエストごとにタイムアウトまで待つことでクライアント側も詰まる

• クライアント側のリトライ機構がAPIプロバイダにとどめを刺す

64

Page 65: Cloud Developer Day 2016 ダウンロード資料

Circuit Breaker

• 平常時の失敗はクライアントはリトライできる

• APIプロバイダがダウンしている場合はリクエストしないようにする

• タイムアウト待ちもしない

• しばらくしてプロバイダが復活したらまたリクエストを再開する

65

Page 66: Cloud Developer Day 2016 ダウンロード資料

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

Page 67: Cloud Developer Day 2016 ダウンロード資料

Expeditor• 平常時

67

クライアントサービスクライアントサービス

オプションに応じて リトライしたり どうしてもだめなら fallback処理

APIプロバイダ

Page 68: Cloud Developer Day 2016 ダウンロード資料

Expeditor• 障害時

68

クライアントサービスクライアントサービス

リクエストせず 最初からfallback処理

APIプロバイダAPIプロバイダ

Page 69: Cloud Developer Day 2016 ダウンロード資料

サービス分割時に困るポイントが解決されてきた• APIが乱立してどこをどう使えばいいのかわからなくなる

• Garageでどのサービスも同じように使える

• 毎回サーバー作って構成管理するのが大変

• DockerfileとYAMLを書けばHakoで環境用意できる

• サービス境界でバグや障害になりやすい

• Pactが互換性を担保しExpeditorが障害の伝播を防ぐ69

Page 70: Cloud Developer Day 2016 ダウンロード資料

サービスを分割することで得られる良いこと• コードベースが小さい

• テストや開発環境の動作が速い

• 変更による影響範囲が小さい

• 既存実装による制約が小さい

• チームにあった実装ルール

• 機能に適したライブラリ、言語の選択70

Page 71: Cloud Developer Day 2016 ダウンロード資料

サービスを分割することで得られる良いこと

• システム境界が明確

• チーム内でコミュニケーションが完結する

• 完結できない場合境界としてあまりよくない

71

Page 72: Cloud Developer Day 2016 ダウンロード資料

まとめ• 最初から、あるいはある日突然Microservicesになったわけではない

• その都度問題になったことに対応してきた積み重ね

• モノリシックなシステムは徐々に辛みが大きくなっていく

• サービスを分けることで発生する問題は技術で回避できる

• テクニックを知りフェーズに応じた設計をするのが重要

72