はじめまして、Wantedly VisitのMatching Squadで推薦基盤の改善をしている一條です。好きな食べ物は二郎系のラーメンです。普段は推薦基盤の整備や、障害対応などをしています。そのため普段はGo、ときどきRuby, PythonでgRPCを書いています。その際にgRPCサーバのデバックが辛くなってツールを作成したのでその説明をします。
この記事は以前技術書典10でTech Bookとして出した内容を加筆・修正したものになります。
gRPCを叩くためには
gRPCでリクエストを送るためには.protoファイルを元にデータをエンコードしサーバに送る必要があります。そのため、
- 手元に.protoファイル、もしくはそこから生成されたコードを用意し、それを利用してデータをエンコードする
- 後述するServer Reflectionという機能を利用し、データをエンコードする
この2つのどちらかの方法を利用してリクエストを送る必要があります。
こういったことをやりやすくするために、grpc_cliやevansといったCLIがあります
これはデータをエンコードする部分をCLIとして簡単に実現できるようにしてくれていて、gRPCでもcurlなどのCLIと同じようにリクエストを送ることができるようになっています。
grpc_cliは「2」の方法で、evansは「1」と「2」の方法でリクエストが送れるようになっています。
Server Reflectionという機能は端的に説明すると、protocコマンドでサーバを生成する際に利用していた.protoファイルと同等のものを返してくれるAPIです。これにより手元に.protoファイルがなくてもエンコードを行うことができます。
ただし、このServer Reflectionは言語ごとに実装の有無は異なります。たとえばGoやPythonには実装が存在しますが、Rubyには存在しません。
社内で生じていた問題
Wantedlyでは社内のマイクロサービス間通信ではgRPCを利用しています。また多くのサービスがRubyで書かれています。
これによりほとんどのサービスではServer Reflectionが実装されておらず、手元に.proto ファイルを用意する必要があります。
この方法ではローカルの状態に依存してしまうため、具体的に次の点で自分は困っていました。
- 障害対応時などにデバックのためkubernetes上にPodを建ててそこからアクセスする、ということをよく行うが.protoファイルのコピーをする手間が発生する
- データサイエンティストにAPIを提供する際にコピペで実行できないため、まずは叩いてもらうためのステップがかなり高くなる
また、個人的に困っていた問題としてはgrpc_cliが便利で使っていたのですが、これがRubyサーバを叩くときだけ別のツールを使わなければいけない、というのがストレスになっていました。
Server Reflectionを分離する
これらの課題が自分的にはかなりきつかったため、Server Reflectionの有無に関係せず、ローカルではファイルを持たない形でデバックできるツールを作ることにしました。
また、コンセプトとしてはRubyにServer Reflectionを実装するまでのつなぎとして作ることにし、またgrpc_cliとの親和性をできるだけ保つことにしました。
ここで置いている前提としては「すべてのサービスの.proto ファイルはある1つのレポジトリに集約されている」です。
その前提ですべての.protoファイルの内容を知るServer Reflection用のサーバを生成し、それを叩くという方針にしました。
これはアイディアとしては単にServer Reflectionの機能と他のgRPC Serviceの実装を分離をする、ということをしているだけです。
そのため、次を作る必要があります。
- Server Reflectionが別のサーバとなる場合に対応したCLI
- .protoファイルからServer Reflectionを返すだけのサーバを生成するツール
実装する
に実装があります。giroと書いてギロと呼んでいます。
- Server Reflectionが別のサーバとなる場合に対応したCLI => giro
- .protoファイルからServer Reflectionを返すだけのサーバを生成するツール => protoc-gen-reflection-server
と実装があります。.protoファイルからの生成はprotocのpluginとして書くほうがパースした結果が得られて楽だったのと、扱いやすかったので利用しています。
また、protoc-gen-reflection-server の挙動としては、protoc-gen-goで生成されたものを使いServer Reflectionを返すmain.goを吐くだけのものとなっています。
以下に図の中で利用している単語とその意味を書きます。
リクエストの流れとしては次のようになります。
実際に動かすと次のような形になります
$ export REFLECTION_SERVER=localhost:5000
$ giro ls
grpc.reflection.v1alpha.ServerReflection
rerost.giro.v1.HostService
rerost.giro.v1.TestService
$ giro call --rpc-server=localhost:5001 rerost.giro.v1.TestService/Echo {"message":"Test"} --metadata=key1:val1:key2:val2
{"message":"Test","metadata":{"metadata":{":authority":{"value":["localhost:5000"]},"content-type":{"value":["application/grpc"]},"key1":{"value":["val1"]},"key2":{"value":["val2"]},"user-agent":{"value":["grpc-go/1.27.0"]}}}}
実際導入してみて
Wantedlyの一部エンジニアにも導入してもらったところ、バックエンドエンジニアはもちろんデータサイエンティストにも利用してもらえています。
ただ、毎回gRPCのリクエストの中身を書くなどが面倒なので、コピペとなっているケースが多いようでしたが、目的だったコピペしやすいものを作るというのは実現できているんじゃないかなと思います。
また僕としても、もうRubyで書かれているサーバなのかGoやPythonで書かれているサーバなのかを意識しなくて済んでとても楽です。