
2025.08.01
災害大国・日本に求められる“命しか守れない防災”からの脱却 最長2週間先の気象災害予測による対応策
gRPC in Cookpad(全1記事)
リンクをコピー
記事をブックマーク
岩間雄太氏:お願いします。クックパッドのgRPCの話をします。
「自己紹介いるか?」と思いましたが、一応書いておきました。
岩間雄太といいます。よろしくお願いします。ふだんは「@ganmacs」でやっています。今日いる人はみなさんそうですが、技術部開発基盤グループというところにいます。2017年度に新卒として入社しました。
目次ですが、今日は「これまでクックパッドでサービス間通信をどうやっていたか」「結局gRPCを入れることになってどうしているか」「RubyでgRPCどうしているか」という話をします。
これまでのクックパッドは、「マイクロサービスやっていくぞ」とやっていました。社内では「GarageとGarageClient」というRESTfulなAPIをいい感じに作れるやつでAPIを共通化しています。Garage を使用したアプリケーションもだいぶ増えてきたところです。。ドキュメンテーションはAutodocでやっていて、これはGarageのAPIがどういう値を返すかみたいなことを書いておけるやつですね。
あとは、PactでCDCテスティングもやっています。このへんに参考があるので良ければ見てください。
「結局つらいよね」となったのがこれです。
「IDLとスキーマが欲しい」。Autodocを使っているときの問題としては、誰かががんばって(ドキュメントを)書いてくれるといいのですが、誰かががんばらないと書かれないという問題があります。
「結局型は何なんだ」という問題があって、各フィールドの型を定義して参照したいっていうのがまず1個あります。また、さっき「RESTfulなAPIを(Garageで)作れる」って言ったんですけど、RESTなエンドポイントへのマッピングが困難なケースが増えてきているんですね。
これはあるリソースに対してGETやPOSTではなく「全部POSTでバッとやったほうがこのAPIに適してない?」というのが増えてきたということです。
それと多言語化です。先ほど言った通りCookpad ではほとんどRailsアプリですが、Goも増えてきていて、「Garage使えんやんけ」みたいなことも問題にありました。
それでgRPCはおそらくみなさんもそこそこ知っていると思いますが、軽く説明しておきます。
オープンソースのRPCのフレームワークで、IDLとしてProtocol Buffersをデフォルトで使えます。JSONも使えるらしいんですけど、JSONは使ったことないので一切知らないです。
Protocol Buffersで定義を書いて、いい感じにジェネレートするコマンドがあって、クライアント、サーバーのコードがこういうふうにできて、サーバーの実装を書くと動くという感じですね。
C++、Ruby、Java、Goとかさまざまな言語で実装があって、クライアントとサーバーのコード生成ができるっていうフレームワークですね。
もう少し話すと、これはHTTP2上で動いていて、ここに書いてあるリンクにgRPCの仕様が書いてあります。4種類のRPCがあって、Unary RPCがいわゆる普通のHTTP1みたいな「リクエストしたらレスポンスが返ってくる」というただそれだけのRPCです。
Server streaming RPCというのは、1回リクエストを送ったらサーバーからレスポンスをいっぱい返ってくるってやつです。Client streaming RPCは逆で、クライアントがいっぱいリクエスト送ったあとにサーバーがレスポンスを1つだけ返すというRPC。Bidirectional streaming RPCは、クライアントもサーバーもいっぱい送れる、チャットみたいなことが実現できる RPC ですね。
この4種類がサポートされています。Interceptorがあって、「ログ、認証、エラー処理をいい感じにやってくれや」というのを差し込めるレイヤーがあります。
これが実際のProtocol Buffersの定義になっていて、これを元にコードジェネレートされる感じですね。これはHelloServiceっていうサービスで、RPCの名前はSayHelloです。HelloRequestっていう型を受けて、HelloResponseっていうのを返すと。
HelloRequestがどんなのかっていうと、stringのgreetingっていうフィールドを持った型で、messageになっていると。Responseも同じような感じです。
gRPCの運用をどうやっているかというと、基本的には「hako」というAmazon ECS上で動いています。例外もあって、cookpad.comというのは EC2 インスタンス上で動いていて、これは gRPC クライアントのみが動いている感じです。
あとはService Discovery Service(SDS)を自前で実装していて、先ほどTaikiさんが言っていたEnvoyを使って、Client side Loadbalancingでサービス間の通信をしています。これは便利なリンクがあるので見てください。
あと、Envoyのload_balancing_weightという機能があります。それでSlow Startみたいなことができます。サービスインしたときにいきなり全部のリクエストを流すのではなく、サービスインしたサービスに対してちょっとずつリクエストを増やしていくみたいなのもサポートしています。
今はリクエスト送る側の話だったんですけど、受ける時も違うEnvoy経由でリクエストを受けています。あと開発環境からstaging環境へのアクセスもできるようになっています。
これがサービスインの時の図です。Service Discovery Serviceがいて、それがサービス名とエンドポイントとweightを持っていて、ここがhako appで起動してきたアプリケーションです。
このアプリケーションはappというコンテナで、これが実際のアプリケーションのコンテナですね。appコンテナと、あとfront-envoyコンテナとenvoyコンテナ。この2つのコンテナは別々のコンテナにしてあります。
あとregistratorというコンテナもいて、このコンテナはサービスAが立ち上がってきたときにService Discovery Serviceに対して「起動したよ」と教えてあげる。教えるときは、front-envoyのエンドポイントをService Discovery Serviceに教えてあげる。ここだと、「サービスAは10.0.0.0:7777にあるよ」と教えてあげます。
Slow Startは、先ほどのweightの部分が2からスタートしているだけです。そのweightの部分をClient side LoadbalancingするときにEnvoyが見てくれていて、「2だったらloadは2くらいからスタートして最後は100までいく」みたいな感じになっています。
起動中はregistratorがappコンテナに対して定期的に「生きてる?」とヘルスチェックをして、生きていたらSDSに対して「生きてるよ」と教えてあげる流れになっています。
先ほど口頭で言いましたが、ややこしいので図にしました。
サービスBからサービスAに対してアクセスをするときに、サービスBのenvoyコンテナがService Discovery Serviceに対して「サービスAの情報をくれ」と教えてもらいにいきます。サービスAのエンドポイントを教えてもらって、appがenvoyコンテナに対してリクエストを送ると。
それで、envoyコンテナがサービスAのfront-envoyコンテナに対してリクエストを送る。front-envoyコンテナがappに対してリクエストをそのまま流す。逆も一緒です。これがリクエストの流れになっています。
サービスアウトするときは、Service Discovery Serviceに対してサービスAのregistratorが「死ぬらしいよ」と教えてあげる、するとSDSが「じゃあ消しとくね」と言ってテーブルからエンドポイントのデータを消すという流れになっています。
これはクックパッドでProtocol Buffersをどういうふうに管理しているかです。サービスは複数あるんですけど、基本的にProtocol Buffersの定義は1つのリポジトリにまとめています。
サービスごとにディレクトリを切るようになっていて、「serviceAだったら/serviceA以下に、そのサービスが使うProtocol Buffersの定義を全部書いてください」という運用にしています。
使用したいアプリケーションは、このProtocol BuffersのリポジトリをそのままGitのサブモジュールで引っ張ってきて、それをもとにコードをジェネレートして、サーバーを書いたりクライアント書いたりする流れになっています。
Protocol Buffersの書き方もバラバラだと困るのでLintを入れています。あとProtocol Buffersの定義を元にドキュメントを自動で作ってくれるやつがあるので、それを使ってドキュメントを作るようにしてます。そのドキュメントのリンクをhako-consoleというアプリケーションのコンソールアプリに出すようにしています。
例えばサービスAに対してリクエストを送りたい人がいたら、サービスAのhako-consoleのページを見にいきます。それで、このProtocol Buffersの定義を見て、「あぁ型こういうのね」と書く流れになっています。
これはメトリクスについてです。
先ほど言った2つの「front-envoy」と「ふつうのenvoy」です。「ふつうの」という言い方が正しいのかわかんないですけど、この2つでメトリクスをとっています。Envoy statsというEnvoyに付いているstatsの機能と、アクセスログの2つを使っています。
メトリクスとアクセスログは、Prometheusにがんばって入れてGrafanaで見ることになっています。このアクセスログをそのままGrafanaに入れることができないので、mtailを使ってPrometheusで読めるかたちにしてPrometheusに入れています。
これもhako-consoleに対してGrafanaのリンクを貼ってあるので、サービスAを見ているサービスチームの人はサービスAのhako-consoleのページに行って、「メトリクス見たいな」と思ったらそのメトリクスのリンクを押せばすぐ見れるというふうになっています。
それで、EnvoyのアクセスログをどうやってPrometheusに入れるようにしているかですね。ここまでの話のほとんどは、サービス開発者が何もしなくてよくて、1行ぐらい定義をシュッと書けばだいたい動く感じです。
どういう動作かというと、もう1個mtail用のコンテナが動いています。mtailのコンテナをfront-envoyがマウントするようなかたちになっていて、mtailがEnvoyのアクセスログを順次読んでいって、Prometheusが読める形式に出力するようになっています。MVPを雑に作ったやつがあるので、興味ある人は見てください。
Prometheusというのはプル型のメトリクスをとるアプリなのでmtailに対してPrometheusからアクセスができないといけないのですが、サービスAというのはECS上で動いているんです。つまり、EC2の上でDockerが動いていて、そのDockerの1つのコンテナがmtailで、そのコンテナに対してPrometheusからアクセスを通す必要があります。
先ほどのSDSとは違う「ECS用のSDS」を自前で書いていて、そのSDSに対してアクセスしに行って、Prometheusの設定を毎回アップデートします。Prometheusはその設定を見てmtailを見に行くという流れになっています。
これはメトリクスの例で、ちっちゃいな(笑)。こちらがリクエストカウントで、ふつうにとれますよという話です。そちらがレスポンスタイムで、このあたりがgRPCのパスごとのリクエストカウントやリクエストタイムです。こういうのもとれます。
これはサービスメッシュ感がありますが、「あるサービスからどれくらいリクエストが来たか」をとれるようになっています。
こちらはホストごとのリクエスト数やレスポンスタイムをとることができて、どこかのコンテナにリクエストが集中してないかを見たくて作りました。
このあたりはコンテナが受け取ったリクエストカウント、むこうがレスポンスタイム。こちらはそれにgRPCのパスごとをプラスしたものです。むこうもリクエスト、レスポンスタイムという感じです。
RubyでgRPCをどうやっているかという話をします。grpc gemについてまずしゃべりますが、拡張ライブラリとしてCとC++で書かれているやつをRubyの拡張ライブラリという形で使って作られています。
grpc/grpcの下にRubyの実装や、ほかにもC++やPythonとかいろんな言語があって、それらがCとC++で作られたでっかいgRPC coreをそれぞれ拡張ライブラリとして使うという作られ方をしています。
grpc gemはマルチスレッドなんですが、シングルプロセスでなかなか厳しいです(笑)。
RubyにはGILがあるのでシングルプロセスだとマルチスレッドなアプリケーションでも並列に動きません。一応、IOは動きますけどCPUは並列に動かないので厳しいです。
マルチプロセス化する予定がないかというのをがんばって探したんですが「直近のロードマップにはないよ」というのが GitHub の Issue のに書いてあったので多分しばらくはなさそうですね。
どういうふうに使うかというと、grpc-tool gemを使うとサーバーとクライアントのコードがいい感じに作れます。先ほどのこれを書いてgrpc-toolを使うとクラスができます。
このHelloworld::Greeter::Serviceが自動生成でできます。先ほどにはなかったsay_hello_againもありますけど、say_helloの実装を書きます。そうするとこれで動くみたいなけっこうお手軽なやつです。
RubyでgRPCをクックパッドでどうしているかと言うと、普通に公式のgrpc gemを使っています。
「グラフ」と読むのかよくわかってないですけど、grufというフレームワークも見つけましたが、これは「そんなでもないな」と思ったので使いませんでした(笑)。
(会場笑)
失礼しました(笑)。先ほど、Interceptorの話が出ましたが、アプリケーションレイヤーでアクセスログとか「どんなメッセージ送られてきたか」みたいなのがとれないので、それ用のInterceptorを自作しました。それと、grpc gemはシグナルを受けると死んでしまう作りなので、トラップしていい感じにストップするやつも作ったりしています。
また、ほかのInterceptorも作っています。例えば、例外処理のInterceptorも作って動いているし、あとNew Relicを使いたいんだけど、サポートがないので、それ用のInterceptorも作りました。
Goだとgo-grpc-middlewareみたいなものがありますが、Rubyにはないので作らないといけませんでした。
Ruby on RailsでgRPCをどうしているか。Ruby単体で動いているアプリはあんまりなくて、基本的にはRailsで動いているんでこの話はディレクトリをどうやって置いているかというただそれだけのことなんですけど。
先ほどの実装をapp以下のgrpcやserviceというディレクトリに置きます。lib以下に自動生成のコードを配置して、config/initializersにgRPC用の設定のファイルを1個作って、そこでポートとか、Interceptorはどういうの使うのかとか、どういうサービスをハンドルするかみたいなのを書いています。
どうやって起動させているかというと、rails serverみたいなコマンドがないので、rails runnerで起動しています。最初はrake taskを作りましたが、それで起動すると勝手にeager_loadがオフになってハマリまくったのでrails runnerに変えました。
あとActiveRecordのconnection poolとの相性があって、それについてはこの次でしゃべりますが、先ほど言ったみたいにマルチスレッドなんですね。マルチスレッドで ActiveRecord を使う場合、with_connectionで囲ってあげて、connection poolからconnectionを取ってきて、終わったら返すというのをやらないといけないんですね。
例えばgrpc gemのWorkerの数を30にして、connection poolの数を5にすると、律速になるのは5のほうです。connection poolから5個取ってきたら6個目からブロックするようになって、「なんかぜんぜんパフォーマンスでないっスね」「やっぱシングルプロセス、ダメですわ」みたいに笑っていたら、そうじゃなくて。ふつうにconnection poolで死んでたみたいなのがあって、こういう地味なところにハマってたりもしました。
「grpc gem、つらくない?」ということで、CPUがそもそも使いきれないんですね。
先ほどの問題が解消されたところで、ほかのアプリに比べてオートスケールがぜんぜんうまくいかなくて。スケールアウトする前に、gRPCのResourceExhaustedという「Workerが完売したよ」というエラーが返ってきてしまいます。
あとGraceful Shutdownがないです。先ほどの「シグナルを受けるとると死ぬ」ですが、まぁふつうにシグナルをうけると死ぬんですよ。シグナルを受けて、ストップ用のメソッドがあって、「ストップ」を押してもGracefulに死んでくれなくて、リクエストを受けているときに勝手に死にます。
いまだとEnvoyを使って、RubyのgRPCアプリケーションをいったんサービスアウトして止めるというふうにしています。当然ストリーム系のGraceful Shutdownもないので、Server streaming で1回リクエストを送って、サーバーがリクエストを返しているときでも急に死ぬっていう感じです。
ここに書いてあるだけじゃなくて、けっこう厳しいことがいっぱいあって、「つらいね」となります。それで、新しくgRPCのgemを作り始めました。nghttp2を使うとH2のレイヤーをいい感じにやってくれているので、それを使ってgriffinを作っています。grpc gemとの互換性を気にして作っていて、Protocol Buffersでgrpc toolsから生成されるコードをそのまま使用できるようにして、grpc gemが改善されたらすぐに戻りたいという気持ちで作っています。
社内アプリではもう動き始めていて、現在移行を進めています。ベンチマークをとったら実はgriffinのほうがちょっと速かったんです。
まとめとしては、gRPCでサービス間通信をクックパッドでもするようになってきました。Envoyを活用してgRPCのサービス基盤を構成しています。RailsとgRPCを組み合わせて使っていて、つらかったので新しいgrpc gemを作って、ただいま検証中です。
今後はEnvoyの機能を使ったカナリアリリースとか、あとプロダクションに入ってないのでgriffinをプロダクションに入れるのと、gRPCゲートウェイみたいなのもやっていこうかなと思ってます。今日の資料はこちらに公開しております。以上です。
司会者:ありがとうございました。
(会場拍手)
続きを読むには会員登録
(無料)が必要です。
会員登録していただくと、すべての記事が制限なく閲覧でき、
スピーカーフォローや記事のブックマークなど、便利な機能がご利用いただけます。
すでに会員の方はこちらからログイン
名刺アプリ「Eight」をご利用中の方は
こちらを読み込むだけで、すぐに記事が読めます!
スマホで読み込んで
ログインまたは登録作業をスキップ
関連タグ:
2025.09.08
部下が不幸になる上司のNG行動5選 マネジメントは「自律と統制」のバランスでうまくいく
2025.09.10
人生の差は20代で決まる “指示待ち人間”で終わらないために積むべき4つの経験
2025.09.16
日本人が英語学習で苦戦する根本的原因 「言いたいことの順番」が真逆になる英語と日本語
2025.09.10
「やりたいこと」はないが「課題解決」自体を楽しめる人 Googleの「優秀なエンジニア」の定義
2025.09.04
「管理職になりたくない問題」の原因は上司にもある 部下の昇進意欲を削ぐ行動
2025.09.16
“できる仕事のキャパが10倍になった” 東証上場社長を変えた習慣「ピッパの法則」の効果
2025.09.11
自分の得意・不得意がわかるワーク 人生を再設計する「ライフキャリア」の見つけ方
2025.09.17
英語ネイティブは「would」をどう使っているか? 「Do you like〜」と「Would you like〜」の違い
2025.09.12
“起業が向いている人”と”経営が向いている人”は違う DMM亀山会長が語る、新規事業の生み出し方
2025.09.09
“指示待ち社員”から「自分で考え、動く社員」に育てる方法 セルフリーダーシップの発揮に重要な3つのアプローチ
管理職は罰ゲームではなかった!マネジメントスキル、リーダーシップは財産に!
2025.07.31 - 2025.07.31
後回しを断ち切り“すぐやる人”になる最速メソッド|東証上場社長実践の後回し撲滅法
2025.06.24 - 2025.06.24
「因数分解! 売れない理由は、“売り方”じゃなく “見方”にある」 ~マーケティング×ビジネス数学で、売上を動かす本質をつかむ~
2025.08.06 - 2025.08.06
【板挟みに苦しむ管理職へ】忙しさから“本当に抜け出す”唯一の方法
2025.07.09 - 2025.07.09
「英語OS」を身につけよ! −思考プロセスをアップデートし、英語学習の遠回りを終わらせよう!
2025.07.05 - 2025.07.05