無理をしないコードレビュー

会員事業部の三吉です。 クックパッドでは、GitHub Enterprise の Pull Request を使ったコードレビューを広く実施しています。 この記事では、私がコードレビューすることに対する苦手意識をなくすために意識したことを紹介します。

クックパッドでは、テックリードや新卒、インターン、バイトといった肩書きに関係なく、誰もがレビュワー・レビュイーになります。 チームやプロダクトによって開発ルールは少しずつ異なりますが、私の所属する会員事業部では、PR を出したときに GHE やチャットで部内のエンジニアにメンションして、その時にレビューできる人がレビューするという形を取っています。

私は、昨年2017年に新卒入社したのですが、それまでは個人開発や研究用のコードしか書いたことがなく、短期インターンシップを除くチーム開発の経験がありませんでした。 配属当初からコードレビューすることは求められていのですが、はじめの数ヶ月間はレビューすることに対して苦手意識があり、なかなか積極的にレビューに参加することができませんでした。

コードレビューの難しさ

コードレビューに対する苦手意識は、自分のレビュー内容に自信が持てないことによるものでした。 コードレビューは、真剣に取り組もうとすると非常に難しい作業です。 以下、「見るべき項目の多さ」「文脈によるレビュー内容の変化」「開発速度とのトレードオフ」の3点からコードレビューの難しさを見ていきます。

見るべき項目の多さ

コードレビューでチェックするべき点は非常に多いです。 以下に、ざっくりとレビューの観点をカテゴライズしました(順序に深い意味はありません)。

  • 挙動(意図どおり動作するか)
  • バグ
  • セキュリティ
  • 可読性
  • テスト
  • パフォーマンス
  • ドキュメンテーション
  • 設計 *1
  • ……などなど

これらは、それだけで分厚い本が書けるようなカテゴリであり、それぞれについて一般的な原則や社内のルールがたくさんあります。 ちょっとした変更であればともかく、毎回のコードレビューでそれらを厳密に網羅することは不可能で、どこかで妥協する必要があります。

文脈によるレビュー内容の変化

では、どこで妥協するのかというと、それはレビュー対象のコードの文脈に依存します。

極端な例を挙げると、ユーザーテスト用のプロトタイプと、決済周りのデータ処理を行うジョブとでは、どのくらい細かくレビューするべきか変わってきます。 前者は、あくまでプロトタイプであって、テストするのに問題なければ最悪バグが含まれていても構いません。 それに対し、後者にバグが含まれていると、深刻なデータ不整合が起きたり、ユーザーに不利益が出たりすることになります。 後者をレビューするときには、テストの内容やエンバグの可能性の丁寧なチェックが必要です。

他にも、変更頻度の高い箇所であればメンテナビリティを考慮したり、検索などリクエストの多い部分であればパフォーマンスを意識したりと、コードの位置する文脈に依存して、重点を置くべき観点が変わってきます。

開発速度とのトレードオフ

レビュー時どこに重点を置くべきかは、文脈だけでなく、レビューに割ける時間によっても変わります。

コードレビューは思いのほかコストのかかる作業です。 レビューしているあいだレビュワーは他の作業をすることができず、また、レビュイーにも指摘箇所の修正だけでなく、レビューがつくたびに発生するコンテキストスイッチの負担があります。

しかし、コードレビューにはそのコストをかけるだけの価値があります。 私たちがコードを書く目的は、ユーザーに価値を届けることであり、コードレビューの目的も変わりません。 コードレビューは、届ける価値の品質を担保するために必要な作業です。

とはいえ、サービス開発においては、その価値をすばやく届けることも非常に重要です。 レビューに不必要なまでに時間をかけて、高速な開発サイクルを回せなくなっては本末転倒です。 品質と速度とのトレードオフから、レビューにどの程度コストをかけるべきか考える必要があります。

コードレビューをするときに意識していること

以上にみてきたように、コードレビューは、無数の項目について、コードが置かれた文脈から優先順位をつけ、開発速度と品質を最大化するような時間でチェックしていく、というとても困難な作業です。

……と、これが理想のコードレビューかもしれませんが、人間が意識的にできるものではありません。 重要なのは、そういった難しさがあることを知った上で、できる範囲でやる ことです。 とはいえ、コードレビューに慣れない頃は、できる範囲でやったレビューには多くのヌケモレがあるように思えて不安になったり、しっかりレビューしたときには時間をかけ過ぎではないかと不安になったりしていました。 ここからは、そうした不安や、それに起因する苦手意識をなくすために私が行っている工夫を紹介します。

レビューした範囲を明示する

自分のレビューが不十分に感じられたときは、何を見たか、あるいは、何を見られていないかを Review summary などに書くようにしています。

f:id:sankichi92:20180618184907p:plain

こうすることで、他のレビュワーに重点を置いて見てほしい場所を伝えています。 広く薄く見ただけであれば「ざっと見ました」というコメントでも構いません。

また、自信のないときは、素直に他のレビュワーに依頼します。

f:id:sankichi92:20180618184918p:plain

重要な定数などについて、仕様と照らし合わせて正しいことを指差し確認するのも、ヌケモレを防ぐのに大事です。

f:id:sankichi92:20180618184933p:plain

わからなかったら質問する

もちろん、他のレビュワーが現れることを期待できない場合は、ひとりで全範囲をレビューする必要があります。 そういう時に自信を持てない箇所、わからない箇所を見つけたら、わかった気になるまで読むのではなく、質問することが重要です。

次のように、質問に答えることで、実装者がミスや考慮漏れに気づくということも少なくありません。

f:id:sankichi92:20180618184946p:plain

コードから汲み取ったことを言語化して、その認識であっているか確認するだけでも効果があります。 込み入った内容であれば、実装者と一緒にペアレビューするという方法も有効です。 ペアレビューで理解した内容を PR にコメントするとなお良いです。

コードレビューには、問題点を見つけるだけではなく、そのコードの理解者を増やして属人性をなくす機能もあります。 GHE に残った質問のログが数年後に再び役立つということも珍しくありません。

nits や IMO, MUST といったラベルを利用する

先にも述べたように、サービス開発ではスピードも重要です。 本質的でない修正に時間をかけるよりも、先にリリースした方が良い場合もあります。

とはいえ、レビューしていると、どうしても細かいところが気になってしまうものです。 そうしたときは、[nits] や [IMO] といったラベルをレビューコメントの先頭につけて、修正の判断を実装者にゆだねます*2

f:id:sankichi92:20180618184957p:plain

逆に、どうしても修正してほしい場合は [MUST] ラベルをつけます。 すぐに修正できない問題であれば、その PR で修正するのではなく、Issue にして後日修正するのも有効です。

おわりに

以上が、私の意識している「無理をしないコードレビュー」です。 レビューすることに慣れて、日常化すれば特に意識する必要はなくなります。 しかし、1年前の私にとってそれはとても難しいことだったので、当時の私に伝えるつもりでコードレビューするときのコツを書いてみました。

コードレビューする機会が増えて感じるのは、レビュワー側も非常に勉強になるということです。 レビューする時の視点は、単にコードリーディングする時の視点とは違います。 エンバグはないか、テストは必要十分か、などなど普段以上に神経をとがらせて見ることになります。 そして、その視点はそのままコードを書く時にも活かすことができます。

また、今回はコードレビュー「する」ときの工夫に焦点を当てました*3が、「される」ときの工夫も重要です*4。 他にも、クックパッドでは、コードレビューのコストを下げるための自動化等の取り組みも行っています*5

コードレビューのスタイルは、組織や開発するプロダクトの性質によって変わってくるものだと思いますが、この記事が少しでも参考になれば幸いです。

*1:大きな変更であれば事前に設計レビューを行います。

*2:リポジトリにもよりますが、クックパッドでは実装者がマージを行うことが多いです。

*3:コードレビューすることに関する記事として、他にも たのしくなるコードレビュー があります。

*4:開発速度を上げるための Pull-Request のつくり方 など。

*5:Android開発のコードレビューbotを乗り換えた話 など。

Header Bidding 導入によるネットワーク広告改善の開発事情

こんにちは。メディアプロダクト開発部の我妻謙樹(id:kenju)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。

cookpad における広告開発

2015年11月に、"クックパッドの広告エンジニアは何をやっているのか" というタイトルで、広告開発部の開発内容について紹介する記事が公開されていますが、それから 2 年余り経過し、広告配信システム周りの状況も大きく変化しました。はじめに、現在の cookpad における広告開発の概要について、軽くご紹介します。

まず、私が所属しているメディアプロダクト開発部では、広告配信システムに加え、動画配信サービスの開発も担当しています。過去には同じチームから、動画配信周りの技術について以下のような投稿もありますので、そちらもご覧ください。

広告配信システムの開発で担当しているサービスの一覧は、以下の通りです:

  • 広告配信サーバーの開発 (Rails)
  • 社内向け広告入稿システムの開発 (Rails)
  • 広告ログ基盤の運用(Python, Kinesis Streams, DynamoDB, Lambda)
  • 広告配信用 SDK の開発(各プラットフォームに準拠。WEB 向けは JavaScript)

プロジェクト別で言えば、以下がスコープです:

  • 既存自社広告商品の改善
  • 新広告商品の新規開発
  • ネットワーク広告商品の開発や改善

例えば、昨年末から今年はじめにかけて、とある新広告商品の開発に携わっていたのですが、その時のプロジェクトの一部についてスライドが公開されているので、そちらもご覧ください。

今回は、その中でも「ネットワーク広告商品の開発や改善」における一プロジェクトについてご紹介します。

背景

cookpad では、以下の 2 種類の広告を配信しています。

  • 自社広告 1
  • ネットワーク広告 2

この内、ネットワーク広告においては、 Supply Side Platform (以下、SSP)各社と連携して複数のアドサーバー経由で広告を配信しているのですが、それらの広告は頭打ちになってきています。そのため、広告の収益改善に取り組む必要がありました。

安易に広告枠を増やすことはユーザー体験の低下やネットワーク負荷増加に繋がるため、避けなければなりません。したがって、現在配信されている広告の買付け額や配信フローを改善させる必要があります。

そこで、近年日本でも導入が進んできている Header Bidding と呼ばれる仕組みを導入することになりました。

About Header Bidding

Header Bidding とは、アドサーバーに広告のリクエストをする前に、SSP 各社に広告枠の最適な額を入札します。

仕組み的には、<head> タグ内(= Header)で事前に入札リクエスト(= Bidding)を行うことから、"Header Bidding" と呼ばれています。

Without Header Bidding

例えば、Header Bidding を経由しない、従来のパターンでの広告枠買付け方式を見てみましょう。ここで図の用語の定義は以下の通りとします:

  • Client ... 広告を表示させる側ここでは cookpad の本体サイト
  • Ad Server ... アドサーバー
  • SSP ... SSP各社
  • Floor ... フロアプライス 3
  • Bid ... 入札結果 4
  • Winning bid ... 買付けに成功した入札価格

f:id:itiskj:20180613203016p:plain

既存のアドサーバーの入札ロジックは、基本的にウォーターフォール方式(5)で買付けが行われます。したがって、上記図のケースの場合、

  • 広告枠に対してフロアプライスが $1.0
  • SSP α の入札結果が $0.8(フロアプライス以下)
  • SSP β の入札結果が $1.2(フロアプライス以上)

上から順番に問い合わせていった結果、最初にフロアプライス以上の価格で入札してきた「SSP β」の広告が表示されることになります。

ここで「SSP δ」の入札結果が、$2.0 であることに着目してください。もし、「SSP δ」入札結果を反映できていたら、その広告枠の価値は $2.0 になります。つまり、広告枠本来の価値は $2.0 といえます。しかし、ウォーターフォール形式の仕組み上の制約によって、$1.2 の入札結果が反映されてしまいました(差し引き $0.8 の機会損失ですね)。

これを解決するのが、Header Bidding です。

With Header Bidding

次に、入札サーバを挟む場合で見ていきましょう。なお、この場合は後述する Server-to-Server 方式で説明していきます。

f:id:itiskj:20180613203037p:plain

この場合、

  1. Client -> Bid Server ...Header Bidding を入札サーバにリクエスト
  2. Bid Server -> SSP ... 入札。このとき、各社への入札は 同じタイミングで入札 される
  3. Bid Server -> Client ... 入札結果を返す。ここでは、「SSP δ」が $2.0 で買付けをすレスポンスを返してきたので、Winning bid が $2.0 になる
  4. Client -> Ad Server ... 入札結果が返ってきてから広告をリクエストする。このとき、Winnig bid を伝えることで、「SSP δ」広告が返却されることになる 6

という順番で処理が実行されます。Header Bidding を利用しないケースと違って、一番買付け額が高い SSP の入札結果が反映されたことがわかります。

ポイントは、

  • アドサーバーにリクエストする前に入札を行うこと
  • SSP 各社へのリクエストが並行に行われること

です。これによって、本来失われてしまっていた入札価格を最適化することができます。

Client vs S2S Header Bidding

Header Bidding には

  • Client Header Bidding
  • Server-to-Server Header Bidding

の2種類の方式が存在します。

Client Header Biddingは、クライアント側で入札を行う形式です。技術的には、<script> タグ内で、入札先の SSP 一覧を指定して、それぞれに入札リクエストを行います。それらの入札リクエストの結果を待って、一番 eCPM の優れた入札結果を選択する形式です。

Server-to-Server Header Bidding は、サーバー側で入札を行う形式です。Client Header Bidding との違いは、入札サーバーに 1 回だけリクエストを送信すれば良い点です。また、入札ロジック(例:SSP 各社からの入札結果の待ち合わせ、タイムアウト処理、入札結果の比較)を入札サーバーが担ってくれるので、クライアント側の責務が大幅に削減されることです。

現在は、Server-to-Server 方式が主流です。

Header Bidding Services

なお、Transparent Ad Marketplace(以下、TAM)という、Amazon が提供する Header Bidding の広告サービスを採用しています。

設計・実装

基本的には、TAMの提供するドキュメントに沿って、<head> タグ入札サーバーにリクエストするスクリプトを埋め込めば、導入は完了します。

しかし、弊社の場合

  • 独自の広告入稿・配信サーバーを介して自社広告・ネットワーク広告すべての広告を配信している
  • しかも、ページごとに配信される広告スロット(7)は静的ではなく動的に変化する

といった制約のため、スムーズな導入ができず改修が必要でした。

以下の図が、Header Bidding を行うまでの大幅な流れです。ここで、

  • ads ... 社内広告配信サーバ
  • display.js ... 広告表示用の JavaScript SDK
  • cookpad_ads-ruby ... display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
  • apstag ... TAM の提供する Header Bidding 用ライブラリ
  • googletag ... DFP の提供するアドネットワーク用ライブラリ

だとします。なお、googletag の公式ドキュメントは、https://developers.google.com/doubleclick-gpt/reference からご覧になれます。

f:id:itiskj:20180613203104p:plain

Header Bidding を実行するまでの大まかな流れは、以下の通りです:

  1. JavaScript SDK が、広告配信サーバーから表示すべき広告リクエストする
  2. 広告にネットワーク広告が含まれている場合、Header Biddingの一連の処理を開始する
  3. まずは、apstag, googletag それぞれの初期化を行う(例:デフォルトのタイムアウト設定)
  4. apstag を用いて TAM に Header Bidding リクエストを送る
  5. 入札結果をもとに、DFP に広告リクエストを送る
  6. DFP から広告リクエストが返却されたら、広告を表示する

ポイントは、Header Bidding をリクエストしている間、

  • googletag.pubads().disableInitialLoad() で DFP リクエストを中断し、Header Bidding を行う
  • 入札結果が返ってきたら、googletag.pubads().refresh([opt_slots...]) で広告のレンダリングフローを再開する

という点です。

結果

以上を持って、Header Bidding を導入するまでの一連の流れを説明してきました。具体的な数字はここでは伏せますが、今回の導入によってネットワーク広告の収益改善を実現することができました。

新たな課題

広告レンダリングフローのパフォーマンス悪化

しかし、ここで新たな課題も発生してしまいました。

それは、Header Bidding リクエストの分、ネットワーク広告が表示されるまでのレイテンシが増加してしまった、という点です。

広告が表示されるまでの一連のレンダリングプロセスを、以下の図に示しました。

Processing, DOMContentLoaded, load は、ブラウザが HTML/CSS をパースしてレンダリングするまで一連のフローの一般的用語です。気になる方は、Ilya Grigorik(8) による"Measuring the Critical Rendering Path" をご覧ください)

f:id:itiskj:20180613203121p:plain

ぱっと見て気づくのは、社内広告配信サーバの ads へのリクエストから、Header Bidding 、そして DFPへのリクエストまですべてがシリアルに実行されていることです。今回 Header Bidding を導入したことによって、その分レイテンシが増加したのです、大体 150 ~ 400 (ms) と、かなり致命的なパフォーマンス低下になってしまいました。

広告レンダリングフローの可視化がされていない

筆者の肌感で「150 ~ 400ms」と説明しましたが、実はクライアント環境で実行されるまでの広告レンダリングフローは、今まで計測・可視化されていませんでした。

上記で挙げた広告レンダリングフローのパフォーマンスを改善したいものの、ボトルネックが正確にはどこになるのか、計測するまでわかりません。計測できないものは改善できないと言われるように、まずは計測・可視化のフローを導入しました。

ここで幸いなことに、Fluentd にログを流し、分析可能なデータウェアハウス(cookpad の場合は、現状 Redshift)にテーブルを構築するまでの仕組みはすでに存在していました。したがってクライアント側と多少のテーブル定義を書くだけで実現できました。

(補足:cookpad におけるデータ活用基盤については、"クックパッドのデータ活用基盤" をご覧ください。)

以下は、取得したログデータをもとに可視化してみた様子です。ログを先日から取得し始めたばかりなので、可視化のフローはまだ未着手です。社内で推奨されている BI ツールにダッシュボードを作り、定点観測できるところが直近のゴールです。

f:id:itiskj:20180613203133p:plain

広告レンダリングのクリティカルパスは任意のタイミングでロガーを仕込むだけですが、DFP の場合、googletag.events.SlotRenderEndedEventを利用すると、広告枠が表示されたタイミング、広告が "Viewable"(9)になったタイミングでイベントを取得できます。

対策と今後の展望

以上が、Header Bidding の導入から、新たに浮上した課題への対策の説明でした。直近ですと、以下に取り組んでいく予定です。

  • 広告レンダリングフローの可視化フェーズ
  • 広告レンダリングフローの最適化

広告レンダリングフローの最適化

「広告レンダリングフローの最適化」では、自社の広告配信サーバーへのリクエストのタイミングを、今より前倒しにする方針で設計及び PoC の実装を行っている段階です。

具体的に言うと、現在 HTML ファイルの <body> 下部で広告レンダリングフローを開始しているのですが、それを <head> タグの可能な限り早い段階で開始するように改善をする必要があります(過去の設計の都合上、広告配信サーバーへのリクエストは、各広告スロットの HTMLElement 要素が レンダーツリー10 に挿入され、実際に描画されるタイミングにブロックされている)。

f:id:itiskj:20180614112206p:plain

パフォーマンスの可視化及びレンダリングフローの最適化についても、また別の機会にご紹介したいと思います。

まとめ

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。技術的にチャレンジングな課題も多く、非常に面白い領域です。ぜひ、興味を持っていただけたら、Twitter などからご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。


  1. 自社広告 … 自社独自の営業チームが、直接広告主と契約を結び配信している広告。自社で配信されるクリエイティブを運用できるため、意図しない広告が配信されることがない。

  2. ネットワーク広告 … 他社の広告配信会社が提供している広告配信サーバーを経由して、広告の買付け・配信を行う広告。各社が提供する <script> タグを HTML に埋め込み、返却された広告を <iframe> にレンダリングする形が一般的。

  3. フロアプライス … 最低落札価格のこと。例えば、「フロアプライス $0.8」広告をリクエストしたとき、$0.8 以下の広告枠の買付けは行わない。

  4. 入札結果 …SSP 各社が、広告枠をいくらで買い付けるかを示す価格。これがフロアプライスより低い場合、広告枠に広告が表示されることはない。

  5. ウォーターフォール方式 … SSP 各社定義した順番で一つ一入札していく方式。「滝」語義が表すとおり、上から順に問い合わせ得ていく様子からこの名前で呼ばれる。

  6. アドサーバー側にどのように入札結果を伝えるかは、各アドサーバー側の仕様や実装に依存する。例えば TAM が DFP に対して Header Bidding を行う場合、Key/Value Targeting の仕組みを使っている。

  7. 広告スロット … 広告枠が表示される枠のこと。

  8. Ilya Grigorik … Google のエンジニアで、“High Performance Browser Networking” の著者、と言えばわかる方も多いかもしれません。完全に蛇足ですが、尊敬しているエンジニアの1人で、彼の著作をきっかけ Web の裏側に興味を持ちました。

  9. Viewable Impression … 広告が「ユーザーに見える状態」になったかどうかでインプレッションを測定している。詳細については、“Viewabiliity and Action View”を参考のこと。

  10. Render Tree … https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction

Service Mesh and Cookpad

This article is a translation of the original article which was published at the beginning of May. To make up for the backgroud of this article, Cookpad is mid-size technology company having 200+ product developes, 10+ teams, 90 million monthly average users. https://www.cookpadteam.com/


Hello, this is Taiki from developer productivity team. For this time, I would like to introduce about the knowledge obtained by building and using a service mesh at Cookpad.

For the service mesh itself, I think that you will have full experience with the following articles, announcements and tutorials:

Our goals

We introduced a service mesh mainly to solve operational problems such as troubleshooting, capacity planning, and keeping system reliability. In particular:

  • Reduction of management cost of services
  • Improvement of Observability*1*2
  • Building a better fault isolation mechanism

As for the first one, there was a problem that it became difficult to grasp as to which service and which service was communicating, where the failure of a certain service propagated, as the scale expanded. I think that this problem should be solved by centrally managing information on where and where they are connected.

For the second one, we further digged the first one, which was a problem that we do not know the status of communication between one service and another service easily. For example, RPS, response time, number of success / failure status, timeout, status of circuit breaker, etc. In the case where two or more services refer to a certain backend service, resolution of metrics from the proxy or load balancer of the backend service was insufficient because they were not tagged by request origin services.

For the third one, it was an issue that "fault isolation configuration has not been successfully set". At that time, using the library in each application, setting of timeout, retry, circuit breaker were done. But to know what kind of setting, it is necessary to see application code separately. There is no listing and situation grasp and it was difficult to improve those settings continuously. Also, because the settings related to Fault Isolation should be improved continuously, it was better to be testable, and we wanted such a platform.

In order to solve more advanced problems, we also construct functions such as gRPC infrastructure construction, delegation of processing around distribution tracing, diversification of deployment method by traffic control, authentication authorization gateway, etc. in scope. This area will be discussed later.

Current status

The service mesh in the Cookpad uses Envoy as the data-plane and created our own control-plane. Although we initially considered installing Istio which is already implemented as a service mesh, nearly all applications in the Cookpad are operating on a container management service called AWS ECS, so the merit of cooperation with Kubernetes is limited. In consideration of what we wanted to realize and the complexity of Istio's software itself, we chose the path of our own control-plane which can be started small.

The control-plane part of the service mesh implemented this time consists of several components. I will explain the roles and action flow of each component:

  • A repository that centrally manages the configuration of the service mesh.
  • Using the gem named kumonos, the Envoy xDS API response JSON is generated
  • Place the generated response JSON on Amazon S3 and use it as an xDS API from Envoy

The reason why the setting is managed in the central repository is that,

  • we'd like to keep track of change history with reason and keep track of it later
  • we would like to be able to review changes in settings across organizations such as SRE team

Regarding load balancing, initally, I designed it by Internal ELB, but the infrastructure for gRPC application went also in the the requirement *3, we've prepared client-side load balancing by using SDS (Service Discovery Service) API *4. We are deploying a side-car container in the ECS task that performs health check for app container and registers connection destination information in SDS API.

f:id:aladhi:20180501141121p:plain

The configuration around the metrics is as follows:

  • Store all metrics to Prometheus
  • Send tagged metrics to statsd_exporter running on the ECS container host instance using dog_statsd sink*5
  • All metrics include application id via fixed-string tags to identify each node*6
  • Prometheus pulls metris using EC2 SD
  • To manage port for Prometheus, we use exporter_proxy between statsd_exporter and Prometheus
  • Vizualize metrics with Grafana and Vizceral

In case the application process runs directly on the EC2 instance without using ECS or Docker, the Envoy process is running as a daemon directly in the instance, but the architecture is almost the same. There is a reason for not setting pull directly from Prometheus to Envoy, because we still can not extract histogram metrics from Envoy's Prometheus compatible endpoint*7. As this will be improved in the future, we plan to eliminate stasd_exporter at that time.

f:id:aladhi:20180502132413p:plain

On Grafana, dashboards and Envoy's entire dashboard are prepared for each service, such as upstream RPS and timeout occurrence. We will also prepare a dashboard of the service x service dimension.

Per service dashboard:

f:id:aladhi:20180501175232p:plain

For example, circuit breaker related metrics when the upstream is down:

f:id:aladhi:20180502144146p:plain

Dashboard for envoys:

f:id:aladhi:20180501175222p:plain

The service configuration is visualized using Vizceral developed by Netflix. For implementation, we developed fork of promviz and promviz-front*8. As we are introducing it only for some services yet, the number of nodes currently displayed is small, but we provide the following dashboards.

Service configuration diagram for each region, RPS, error rate:

f:id:aladhi:20180501175213p:plain

Downstream / upstream of a specific service:

f:id:aladhi:20180501175217p:plain

As a subsystem of the service mesh, we deploy a gateway for accessing the gRPC server application in the staging environment from the developer machine in our offices*9. It is constructed by combining SDS API and Envoy with software that manages internal application called hako-console.

  • Gateway app (Envoy) sends xDS API request to gateway controller
  • The Gateway controller obtains the list of gRPC applications in the staging environment from hako-console and returns the Route Discovery Service / Cluster Discovery Service API response based on it
  • The Gateway app gets the actual connection destination from the SDS API based on the response
  • From the hand of the developer, the AWS ELB Network Load Balancer is referred to and the gateway app performs routing

f:id:aladhi:20180502132905p:plain

Results

The most remarkable in the introduction of service mesh was that it was able to suppress the influence of temporary disability. There are multiple cooperation parts between services with many traffic, and up to now, 200+ network-related trivial errors*10 have been constantly occurring in an hour*11, it decreased to about whether it could come out in one week or not with the proper retry setting by the service mesh.

Various metrics have come to be seen from the viewpoint of monitoring, but since we are introducing it only for some services and we have not reached full-scale utilization due to the introduction day, we expect to use it in the future. In terms of management, it became very easy to understand our system when the connection between services became visible, so we would like to prevent overlooking and missing consideration by introducing it to all services.

Future plan

Migrate to v2 API, transition to Istio

The xDS API has been using v1 because of its initial design situation and the requirement to use S3 as a delivery back end, but since the v1 API is deprecated, we plan to move this to v2. At the same time we are considering moving control-plane to Istio. Also, if we are going to make our own control-plane, we plane to build LDS/RDS/CDS/EDS API*12 using go-control-plane.

Replacing Reverse proxy

Up to now, Cookpad uses NGINX as reverse proxy, but considering replacing reverse proxy and edge proxy from NGINX to Envoy considering the difference in knowledge of internal implementation, gRPC correspondence, and acquisition metrics.

Traffic Control

As we move to client-side load balancing and replace reverse proxy, we will be able to freely change traffic by operating Envoy, so we will be able to realize canary deployment, traffic shifting and request shadowing.

Fault injection

It is a mechanism that deliberately injects delays and failures in a properly managed environment and tests whether the actual service group works properly. Envoy has various functions *13.

Perform distributed tracing on the data-plane layer

In Cookpad, AWS X-Ray is used as a distributed tracing system*14. Currently we implement the distributed tracing function as a library, but we are planning to move this to data-plane and realize it at the service mesh layer.

Authentication Authorization Gateway

This is to authenticate and authorize processing only at the front-most server receiving user's request, and the subsequent servers will use the results around. Previously, it was incompletely implemented as a library, but by shifting to data-plane, we can recieve the advantages of out of process model.

Wrapping up

We have introduced the current state and future plan of service mesh in Cookpad. Many functions can be easily realized already, and as more things can be done by the layer of service mesh in the future, it is highly recommended for every microservices system.

*1:https://blog.twitter.com/engineering/en_us/a/2013/observability-at-twitter.html

*2:https://medium.com/@copyconstruct/monitoring-and-observability-8417d1952e1c

*3:Our gRPC applications already use this mechanism in a production environment

*4:Server-side load balancing which simply use Internal ELB (NLB or TCP mode CLB) has disadvantages in terms of performance due to unbalanced balancing and also it is not enough in terms of metrics that can be obtained

*5:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-dogstatsdsink . At first I implemented it as our-own extension, but later I sent a patch: https://github.com/envoyproxy/envoy/pull/2158

*6:This is another our work: https://github.com/envoyproxy/envoy/pull/2357

*7:https://github.com/envoyproxy/envoy/issues /1947

*8:For the convenience of delivering with NGINX and conforming to the service composition in the Cookpad

*9:Assuming access using client-side load balancing, we need a component to solve it.

*10:It's very small number comparing to the traffic.

*11:Retry is set up in some partes though.

*12:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md#apis

*13:https://www.envoyproxy.io/docs/envoy/v1.6.0/configuration/http_filters/fault_filter.html

*14:http://techlife.cookpad.com/entry/2017/09/06/115710

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://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('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/