RubyではじめるgRPC
gRPCについては以前から興味があっていくつか記事を斜め読みしてましたが、結局自分で動かしてみないと分からないため、Rubyで動かしてみようと思います。
gRPCとは?
Googleの内部で使われていたRPCで、メッセージのシリアライズ方式としてProtocol Bufferを使う。HTTP/2ベース。 Client / Server の内部実装を問わないので、gRPC ServerをGoで書いて、Rubyから使うといったことが可能。
より詳しい説明は公式のWhat is gRPC?を見るべし。
Quick Start
gRPCがオフィシャルにRubyによるQuick Startを提供しているので、まずはここから始めました。 お約束のHello Worldです。
.proto ファイルにserviceを定義し、grpc_tools_ruby_protoc コマンドでRubyのコードをジェネレートするというgRPCのお作法が分かったので、次は自分で(しょうもない)マイクロサービスを作ってみることにしました。
マイクロサービスをつくる
ほぼHelloworldと大差ないですが、オウム返しをするParroterというサービスをつくってみることにしました。
.proto
リクエストとして文字列msgを受け取り、レスポンスはmsgとその回数countを返します。
syntax = "proto3"; package parroter; service ParrotService { rpc say(ParrotRequest) returns (ParrotResponse) {} } message ParrotRequest { string msg = 1; } message ParrotResponse { string msg = 1; int32 count = 2; }
.protoからRubyコードを出力します。
grpc_tools_ruby_protoc -Iproto --ruby_out=lib --grpc_out=lib proto/parroter.proto
とりあえず生成されたファイルをrequireすれば、gRPC Client / Server をつくれるのですが、できればインターフェースと実装の分離をしておきたい。
Building Microservices using gRPC on Ruby – Shiladitya Mandal – Software Developer に書かれているように、private な gem を生成するのがよさそうです。ディレクトリレイアウトは以下のようになりました。
$ tree . . ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin │ ├── console │ └── setup ├── lib │ ├── parroter │ │ └── version.rb │ ├── parroter.rb │ ├── parroter_pb.rb │ └── parroter_services_pb.rb ├── parroter.gemspec └── proto └── parroter.proto
https://github.com/kotaroito/grpc-parroter-service
gRPC Server
作成した private な gem を使って、https://shiladitya-bits.github.io/Building-Microservices-from-scratch-using-gRPC-on-Ruby を参考にしながら、gRPC Serverを作成してみました。
Gemfile
source 'https://rubygems.org' gem 'parroter',:git => "https://github.com/kotaroito/grpc-parroter-service",:branch => 'master' gem 'grpc', '1.7.0.pre1'
bin/start_server.rb
#!/usr/bin/env ruby require 'grpc' require 'parroter_services_pb' class ParrotServer class << self def start start_grpc_server end private def start_grpc_server @server = GRPC::RpcServer.new @server.add_http2_port("0.0.0.0:50052", :this_port_is_insecure) @server.handle(ParrotService) @server.run_till_terminated end end end class ParrotService < Parroter::ParrotService::Service def initialize @count = {} end def say(parrot_req, _unused_call) p parrot_req Parroter::ParrotResponse.new(msg: parrot_req.msg, count: count_msg(parrot_req.msg)) end private def count_msg(msg) @count[msg] = 0 unless @count[msg] @count[msg] += 1 end end ParrotServer.start
bin/test_parrot_service
#!/usr/bin/env ruby require 'grpc' require 'parroter_services_pb' def test_single_call stub = Parroter::ParrotService::Stub.new('0.0.0.0:50052', :this_channel_is_insecure) req = Parroter::ParrotRequest.new(msg: 'Hello gRPC.') resp_obj = stub.say(req) p resp_obj end test_single_call
gRPC Serverを起動後に、何度かClientを実行すると...
$ bundle exec bin/test_parrot_service <Parroter::ParrotResponse: msg: "Hello gRPC.", count: 1> $ bundle exec bin/test_parrot_service <Parroter::ParrotResponse: msg: "Hello gRPC.", count: 2>
こうなります。
気になることをつらつらと
RubyでgRPC Serverを書けそうなことは分かったので、気になることをいくつか調べてみました。
2017年10月現在、(Rubyで書くなら)サーバーは"grpc" gemに同梱されているGRPC::RpcServer 一択だと思われます。 充実したドキュメントはなく、設定オプションを知りたければGithubのソースを追いかけるのがよさそう。
https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/generic/rpc_server.rb#L205-L210 を読むと、スレッドのpool_size や poll_periodなどが設定できそうです。
Interceptor
Rack Middleware に相当するものは、gRPCだとInterceptorと呼ばれるようです。 grpc gemの1.6.7にはありませんでしたが、1.7.0.pre1 から実装されていました。 ただし、EXPERIMENTAL API とのこと。
https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/generic/rpc_server.rb#L200-L203
すごく簡素ですが、Interceptorを使ってみました。
class ParrotServer class << self def start start_grpc_server end private def start_grpc_server @server = GRPC::RpcServer.new(interceptors:[HelloInterceptor.new]) @server.add_http2_port("0.0.0.0:50052", :this_port_is_insecure) @server.handle(ParrotService) @server.run_till_terminated end end end class ParrotService < Parroter::ParrotService::Service ...snip... end class HelloInterceptor < ::GRPC::ServerInterceptor def request_response(request:, call:, method:) p "Received request/response call at method #{method}" \ " with request #{request} for call #{call}" call.output_metadata[:interc] = 'from_request_response' p "[GRPC::Ok] (#{method.owner.name}.#{method.name})" yield end end ParrotServer.start
感想
gRPCはインターフェースを明確に宣言でき、しかもカンタンなのが良い。 JSON Schemaの経験があるだけに余計に。。。
現時点では詳しいドキュメントないので、ソースコードを読みさえすれば、Rubyでもプロダクションで使えるgRPC Serverを書けそうな気がしてる。