2019.06.05
gRPC Client Interceptor入門 with Ruby
こんにちは。サーバサイドエンジニアの三瓶です。印刷ECサイトの ラクスル の開発保守を担当するチームに所属しています。
ラクスルでは Raksul Platform Project (RPP) と称して技術負債の返済活動を継続的に行っているのですが、その流れの一環として印刷ECチームでは巨大化したアプリケーションから商品仕様に関わる部分を別サービスとして切り出す、という作業を最近行いました。
この切り出したサービスでは通信方式として gRPC を、言語としてはRubyを採用しています。
実際に導入してみて、gRPCはまだドキュメントも多くはないと感じたので、本記事ではgRPCが備える機能の1つである Interceptor についてチュートリアル形式でご紹介したいと思います。言語はもちろんRubyを使います。
※ gRPC導入に至った背景や経緯について興味のある方は弊社エンジニアの二串のスライドをご参照ください
Interceptorとは何か
Interceptorとは、RPCコールの前後に任意の処理を差し込める機能・レイヤーのことです。Railsアプリケーションに馴染みがある方は Rack Middleware のようなものを想像してもらうと理解しやすいかと思います。
Interceptorを使えば、認証やロギング、データのフィルタリングなど各通信で共通の処理をひとまとめにできます。
実際に、我々のプロジェクトではリクエストを一意に特定するためのIDをmetadataに付与するInterceptorやリクエストしたメソッド・パラメータ・処理時間をログに残すInterceptor、例外をCatchしてSentryに通知するInterceptor等を自作して、本番環境で稼働しています。
以下はClient → Server間の通信に2つのInterceptorが介在したときのイメージ図です。
作成するClient Interceptorの仕様を決める
ClientのInterceptorと一口に言っても、以下のパターンが考えられます。
- リクエスト処理の「前」に差し込む。リクエストデータに何か手を入れたい場合など
- リクエスト処理の「後」に差し込む。レスポンスデータに何か手を入れたい場合など
- リクエスト処理の「前後」に差し込む。両方に手を入れたい場合や処理全体に何かをしたい場合など
このうち、今回はリクエストの「前」に差し込んでリクエストデータを大文字化する UpcaseInterceptor と、リクエストの「前後」に差し込んで処理時間を計測する PerformanceLoggingInterceptor の2つを作ってみることにします。
以下、それぞれのインターセプターの簡単な仕様です。
- UpcaseInterceptor
- リクエストの前に差し込む
- リクエストオブジェクトに
name
というキー名のデータあればその値を大文字化する
- PerformanceLoggingInterceptor
- リクエストの前後に差し込む
- リクエスト全体にかかった時間を計測し、標準出力へ出力する
- 時間は小数点第3位以下を四捨五入する
また、動作の流れは次の図のようになります。赤い線の箇所でインターセプターの処理を挟みこむイメージです。
Client Interceptorを実装する
※ 注意:gRPCのRuby実装では、IntercptorのAPIインターフェースは EXPERIMENTAL という扱いのようです。そのため、今後のバージョンアップで今回紹介するものとはAPIインターフェースが変わっている、ということがありえますのでお気をつけください。
Interceptorを実装するのはとても簡単で、今回の仕様であれば数行で作れてしまいます。
UpcaseInterceptor
UpcaseInterceptorの実装は次のようになりました。
# upcase_interceptor.rb class UpcaseInterceptor < GRPC::ClientInterceptor def request_response(request: nil, call: nil, method: nil, metadata: nil) request.name = request.name.upcase yield end end
いくつかポイントとなる部分を説明します。
(1) GRPC::ClientInterceptor
を継承する
インターセプタークラスは GRPC::ClientInterceptor
クラスを継承する必要があります。
継承しない場合、GRPC::InterceptorRegistry::DescendantError
例外が発生します。
(2) 通信方式に合わせてメソッドをオーバーライドする
メソッドは利用する通信方式に合わせてオーバーライドします。gRPCには以下4つの通信方式があり、それぞれに対応するメソッドがあります。
- Unary RPC :
#request_response
- Server streaming RPC :
#server_streamer
- Client streaming RPC :
#client_streamer
- Bidirectional streaming RPC :
#bidi_streamer
今回は、1リクエストに対して1レスポンスが返ってくる一番シンプルな通信方式であるUnary RPCを使うため、request_response
メソッドをオーバーライドしています。
※ 各通信方式の詳細が気になる方は https://grpc.io/docs/guides/concepts/ をご参照ください
(3) yieldで次の処理を呼び出す
次にメソッドの中ですが、yield
を呼び出すことで次のインターセプターまたはリクエスト処理の本体を呼び出すことになります。
UpcaseInterceptorはリクエスト実行の前に呼び出したいので、yield
をコールする前に変換処理をしています。
PerformanceLoggingInterceptor
同様に、処理時間を計測して出力するPerformanceLoggingInterceptorは次のようになりました。
# performance_logging_interceptor.rb class PerformanceLoggingInterceptor < GRPC::ClientInterceptor def request_response(request: nil, call: nil, method: nil, metadata: nil) start_time = Time.now.to_f resp = yield request_time = Time.now.to_f - start_time puts "duration: #{request_time.round(3)} sec" resp end end
PerformanceLoggingInterceptorはリクエスト処理の全体を計測したいため、リクエストの前後に処理を差し込みます。
前述のとおり、リクエスト処理は yield
を呼び出すことで伝播されていくため、その前後を囲う形で時間を取れば良いということになります。
返り値は yield
で受け取ったレスポンスオブジェクトをそのまま返します。
Interceptorを使う
それでは、作成した2つのInterceptorを実際に使ってみましょう。
gRPC公式のチュートリアル Ruby Quick Start にあるHelloWorldアプリケーションに、今回作成したインターセプターを組み込んで動かしてみます。
Client Interceptorは、Stub
と呼ばれるクライアントオブジェクトを初期化するときに引数として渡します。
# greeter_client.rb require 'grpc' require 'helloworld_services_pb' # ★ 作成したインターセプターをrequireする require_relative './performance_logging_interceptor' require_relative './upcase_interceptor' def main stub = Helloworld::Greeter::Stub.new( 'localhost:50051', :this_channel_is_insecure, interceptors: [UpcaseInterceptor.new, PerformanceLoggingInterceptor.new] # ★ ここでインターセプターを設定する ) # 省略.. end main
interceptors
引数に渡す順序でインターセプターの実行順序が決まります。
配列の末尾から先頭へと順に実行されるため、上の書き方の場合、PerformanceLoggingInterceptor
→ UpcaseInterceptor
の順で実行されることになります。当初のイメージ図の通りの順序ですね。
それでは実行してみましょう。
まず、比較対象としてインターセプターを使わなかった場合の結果を見てみます。
$ bundle exec ruby greeter_client.rb "Greeting: Hello world"
チュートリアルの通りであれば、上のように “Greeting: Hello world” という結果が出力されると思います。
次にインターセプターを有効化した状態で実行してみます。
$ bundle exec ruby greeter_client.rb duration: 0.002 sec "Greeting: Hello WORLD"
UpcaseInterceptor
によって “world” が大文字化されて、PerfomanceLoggingInterceptor
によって実行にかかった時間が出力されました!
まとめ
以上、簡単ではありますが、RubyでのgRPC Client Interceptorの仕組みや使い方についてのご紹介でした。
アプリケーションを本格的に運用するにつれて様々な要求が発生してくるかと思いますが、Interceptorが役に立つ・解決手段となるケースもあるのではないかと思います。そのようなときにこの記事が参考になれば幸いです。
gRPC Client Interceptor入門 with Ruby
Featured posts
in #Technology