実践: gRPC in Ruby
本章では、gRPC in Ruby の Quick Start から一歩先、「Wantedly で Ruby を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。
Ruby で gRPC Server を作る
Rails としてのディレクトリ構成
まずはディレクトリ構成について説明します。基本的に Ruby on Rails の上で作ることを想定しています。
特徴的な部分を紹介します。
app/grpc_services
protobuf から生成したサービスクラスを継承したサービスクラスを置きます
ファイル名およびクラス名にはそれぞれ
_grpc_service
,GrpcService
という suffix を付与します
app/pb_serializers
Protobuf Message object を生成する pb-serializer の実装クラスを配置します
ファイル名およびクラス名にはそれぞれ
_pb_serializer
,PbSerializer
という suffix を付与します
Rails としてのアーキテクチャ
このセクションは主に Rails 経験者のための解説です。先ほどのディレクトリ構成の背後にあるアーキテクチャと、デフォルトの Rails との差分について軽く触れておきます。図を見てもらいつつ、二段階に分けて説明します。
まず、デフォルトの Rails の MVCアーキテクチャ(①)に対して、JSON / HTTP を話す API サーバーにするために ActionView を取り去って ActiveModelSerializers などのシリアライズ機構を導入します。
次に、②のアーキテクチャに対して、Protobuf / gRPC を話せるように各責務に対応するコンポーネントを差し替えたのが Wantedly における gRPC サーバーの推奨のアーキテクチャ(③)です。
一見大きく変わったように見えるかもしれませんが、モデルやシリアラズなどの概念は同じなので案外馴染みやすいのではないかと思います(付け加えると、ここで書いていないコンポーネント、たとえば ActiveJob や ActionMailer などについては特に変わることはありません)。どちらかというと実際の開発体験としては、要素技術の変更よりは API のスキーマ情報を先に更新して gem を取り込むというワークフローが入ることが大きな変化になると思います。
ということで、gRPC のインタフェース定義から具体的に説明していきます。
wantedly/apis - gRPC のインタフェース定義
apis は Wantedly で .proto file を集約するための repository です。apis へ PR を作るだけで、CI によって「各言語向けのコード生成」が自動で行われるようになっており、「protoc のセットアップ」などの煩雑な作業を個々の開発者がしなくても良い仕組みになっています。
Ruby であれば wantedly/apis-ruby というリポジトリに生成コードがコミットされ、private gem として配信されるようになっています。 各マイクロサービスリポジトリでは、Gemfile に apis を記述した後に bundle install を行って利用するというフローになっています。 以前は apis 内のコードは明示的に require を書かなければ読み込まれなかったのですが、バージョン 2021.12.22.1128 以降から自動的に読み込まれるようになったので、現在では個々の開発者が require を書く必要はありません。
インタフェース定義におけるルール・tips などはprotobufスキーマとgRPC通信を参照してください。
RPC の実装
さて、apis repository によって「gRPC Service Class」と「Protocol Buffers Message Class」のコードを生成して配信することができました。gRPC Server が意味のある response を返すには、これらを利用して ロジックを実装する必要があります。
一例として、ProfileService
の BatchGetProfiles
という RPC の処理を見てみましょう。 以下のように、apis で生成された gRPC Service Class である W::UsersPb::ProfileService::Service
を継承した ProfileGrpcService
Class で、 batch_get_profiles
method を実装します。 この例は簡略化して書いてますが、実際にはメソッド内でDB への問い合わせるなどして意味のあるデータを保持したオブジェクトを作成、 最終的に W::UsersPb::BatchGetProfilesResponse
オブジェクトとして返します。
ここで引数に渡ってくる req
はリクエストメッセージで、 call
は呼び出しに関する情報を持つ GPRC::ActiveCall
のオブジェクトです (正確には GRPC::ActiveCall::SingleReqView
または GRPC::ActiveCall::MultiReqView
のどちらか)。call.metadata
とするとメタデータが取得することができます。
the_pb と pb-serializer によるメッセージの組み立て
Ruby では通常は以下のようにして Protobuf object を作ります。
上記は最も単純な例です。protobufスキーマとgRPC通信で紹介されているラッパー型など、 いわゆる Well-Known Types が入ってくると、もっと面倒なコードを書くことになります(面倒なのでここには書きません)。
ここで活躍するのが the_pb と pb-serializer という2つの gem です。
the_pb は Ruby オブジェクトを前述した Well-Known Types に変換するユーティリティを提供する gem です。 pb-serializer は ActiveModelSerializers のような DSL で Protobuf object の Serializer を実装できる gem です。内部では the_pb に依存しています。
先程例に出した Profile
object の serializer であれば以下のように実装できます。
the_pb と pb-serializer をうまく活用することで、gRPC server 実装時の手間を大きく削減することが出来ます。 pb-serializer の詳しい使い方についてはリポジトリ の README や examples を参照してください。
エラーハンドリング
Ruby における gRPC server の実装では、GRPC::BadStatus
というエラークラスを raise することでクライアントにエラーを返すことになります。 また、エラーコードごとに GRPC::BadStatus
のサブクラスが用意されており、それを利用することも出来ます。 以下2行は同じ意味になります。
この「エラーコード」は google.rpc.Code
に定義されている Protobuf の Enum になります。 エラーコードの使い分けなどについてはprotobufスキーマとgRPC通信を参照してください。
複雑なエラー表現
さて、GRPC::BadStatus
と Google::Rpc::Code
を利用することでメッセージとエラーコードからなる単純なエラーを表現することは出来ました。 しかし、これでは実際のアプリケーションで表現力が足りません。 例えば単純なフォームのバリデーションエラーでも、フィールドごとにエラーメッセージを保持できる必要があります。
gRPC におけるエラーは実際には google.rpc.Status
に定義されるような Protobuf message で表されます。
この google.rpc.Status
のうち、 Any の配列である details
にエラーの詳細を詰めることが可能です。 Google API Design Guide の Errors では google/rpc/error_details.proto に定義されるメッセージの利用が推奨されており、Wantedly でもそれに則っています。
Ruby でのエラー詳細の扱いに関しては、the_pb gem で .new_rpc_error
という関数が提供されており、それを使うと便利です。
テスト
gRPC の RPC の実装は見ての通り普通のメソッドなので、最も単純には普通にメソッド呼び出しをして、返り値に対してテストをすることが出来ます。
ただ、これだと Protobuf の marshal / unmarshal 含め gRPC の内部実装を全く介さないため、 もう少し丁寧にやるなら GrpcTesting
(private library) の利用を検討するといいでしょう。
起動・デバッグ
次に、gRPC Server Process を起動します。gRPC Server を起動して Request を処理させるには、app/grpc_services
に定義した「gRPC Service Class を継承した Class」を Handler として登録する必要があります。また、Wantedly では、マイクロサービスは Kubernetes 上で Docker Container として動かしているので、Heath Check の為に gRPC Health Checking Protocol を実装した Handler の登録も必要です。その他、Observability を高めるための gRPC interceptor(gRPC の拡張機能。後述するが、Rack Middleware 相当のもの)の設定なども行う必要があります。
こういった「どの repository でも共通で必要になる処理」については、個々の開発者が気にする必要がないよう、servicex (internal) と呼ばれる社内 library (gem) の機能として提供するようにしています。具体的には、grpc-server という名前のコマンドを用意しており、それを実行するだけで「gRPC Service Class の load および Handler としての登録、その他に共通で必要になる各種設定」が行われるようにしています。
grpc-server
コマンドは以下のように利用することができます。
この grpc-server
コマンドのうち、Health Checker やシグナルハンドリングなどの設定を隠蔽したものは grpc_server gem として公開されています。
gRPC server にリクエストを送ってデバッグする
JSON/HTTP な API であれば cURL コマンドや Insomnia, Postman といったツールで実際にリクエストを送ってデバッグすることがあるかもしれません。 gRPC server では標準でコマンドラインツールが提供されている ほか、grpcurl や evans といった 3rd party のツールが存在します。 一方でこれらのツールでは「元になった .proto ファイル」もしくは「Server Reflection に対応した gRPC server」が必要になり、Wantedly の環境では少し使いづらいという問題がありました。
そこで Wantedly では giro というツールを利用し、なるべく少ない手順・依存で gRPC server にリクエストを送れるような環境になっています。詳しくは giro の解説をしているブログ もしくは giro の使い方ドキュメント (internal) を参照してください。
Ruby で gRPC Client を使う
gRPC Client を開発する際にも、gRPC Server 開発と同様に .proto file を集約する apis repository や servicex と呼ばれる社内 library (gem) を活用するようにしています。
gRPC Client としては、apis repository で自動生成した gRPC Service Class のコードを load したうえで、その Stub Class を利用します。gRPC Client を単純に利用するだけであれば、以下のコードで動きます。
ただし、Production の Microservices で利用する上では「必ずセットして欲しい gRPC interceptor(gRPC の拡張機能。後述するが、Faraday Middleware 相当のもの)」などが存在するので、個々の開発者が意識しなくてもそれらの設定が自動で行われる様に、servicex gem の中で .stub_for
という「gRPC Client 生成用のメソッド」を用意しています。
.stub_for
の実装はだいたい以下のようになっています。 元々、HTTP/1.1 の通信では servicex gem の中で Faraday Middleware などの設定を行なった API Client を提供する様にしていました。 .stub_for
は同様の体験を gRPC でも提供することが意図されています。 Observability を確保するための interceptor のセットや User Agent の設定なども .stub_for
で自動で行われるようになっています。
gRPC Client については、基本的にやる事はこれだけです。 「apis と servicex を gem として load すれば、どのマイクロサービスからでも簡単に必要な設定が行われた状態で gRPC での通信が出来る」という環境を作っています。
最終的にはだいたい以下のようなコードを書くことになります(次節で説明するエラーハンドリングをしない場合のパターンです)。
gRPC Client のエラーハンドリング
gRPC における通信の失敗は gRPC のエラーコードとして表されます。 (Handbook - gRPC 通信のエラーハンドリング にその種類と、デフォルトで考慮すべきものがどれかの指針が書かれているので、確認しておいてください。)
マイクロサービス・アーキテクチャでの開発を行う際には、障害の分離のために、通信の失敗について考慮する必要があります。 通信が失敗した際、全体を失敗させるのではなくなんらか部分的に処理が継続可能な場合は、メソッド呼び出しで errors に想定しているエラーの種類を列挙してください。
これは、
これは、GRPC のエラーの種類に対応するシンボルとして指定できます。例えば、GRPC のエラーである DEADLINE_EXCEEDED に対応するシンボルは :deadline_exceeded
です。 また、サーバー側に原因があると考えられる UNKNOWN, INTERNAL, UNAVAILABLE を :server_errors
でまとめて指定できるようにしています。
多くのケースでは、errors: [:server_error, :deadline_exceeded]
を書くことをまず考えるのが良いプラクティスでしょう。コードレビューの際にも、そのことをチェックしてください。
errors を指定すると成功と失敗を表現するオブジェクトが返ってくるので、パターンマッチで処理しましょう。
エラーの種類によって個別の処理をしたい場合は、こういう風にパターンマッチを書くと良いでしょう。
gRPC Client のモック
多くの場合、テストコード中ではマイクロサービス間通信を含む外部へのリクエストを禁止したくなります。 HTTP であれば webmock gem を使うことが多いですが、gRPC では grpc_mock という gem を利用しています。
ライブラリ・ツールの紹介
Ruby で gRPC を利用するときに利用する内製の gem やツールの一覧です。 pb-serializer や giro のように直接的に利用するものもあれば、servicex 内で利用されているものもあります。
Ruby で Protocol Buffers を扱う際に便利なユーティリティを集めた gem
ActiveModelSerializer ライクな DSL で Ruby オブジェクトを Protobuf に変換することができる
pb-serializer と組み合わせることで、データソースを抽象化したデータの読み込みや GraphQL ライクな field selector が実装できる
Observability を支える interceptor たち
gRPC server 開発で Rails のような hot-reloading を実現する
grpc_typechecker: A dynamic type checker for gRPC methods
RPC が返しているオブジェクトが正しい Protobuf messaage かどうかを検証する interceptor
gRPC server を起動するためのボイラプレートを隠蔽した gem
簡単に gRPC server を叩ける CLI
話を聞きに行きたい
Slack: #rubyist
もっと知りたい
最終更新