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です。

grpc.io

.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を書けそうな気がしてる。

参考資料