やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

RubyでgRPCをサクッと試してみた

最近はマイクロサービスを検討することが多いです。
マイクロサービスにおいて、よく問題となるのはサービス同士のやりとりをどのように行うかです。オーソドックスな選択肢としてはAPIを作成してHTTP通信で呼び出すという方法が候補に上がりますが、リクエスト数・量が増えてくれば、通信のオーバーヘッド、データ効率といった点で苦しくなってきます。
次に選択肢に上がるのはRPC、近年だとHTTP2を使用するgRPCでしょうか。
HTTP通信と比べると通信のオーバーヘッドも少なく、データ効率が良いため頻繁にやりとりが必要となるマイクロサービスとの相性が良いとされています。ただ、RubyでgRPCを使う...という話をあまり聞いたことなく試したことがなかったです。
今後、一つの選択肢としてgRPCが使えるように簡単なコードを動かしてみました。

実装の流れ

サンプルがgRPCの公式レポジトリにて公開されているのですが、初めて触るにはややオーバースペックかなと感じたので、最小限のシンプルな実装を行いました。 以下にRubyでgRPCを使ったサーバー・クライアントを実装するまでの流れを記載します。

  • .protoファイルの作成とデータ定義
  • protocによるコード生成
  • サーバーの実装
  • クライアントの実装

なお、実装にあたり「さくらのナレッジ」にて公開されている内容を参考にさせて頂きました。
素敵な記事をありがとうございます。とても内容が分かりやすかったです。

knowledge.sakura.ad.jp

プロジェクトの作成

適当なディレクトリを作成してからbundle initでGemfileを作成してください。
Gemfileには以下を貼り付けておけばOKです。

# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"

gem "grpc"
gem "grpc-tools"

bundle installを実行後、libprotosディレクトリを作成すれば完了です。

├── Gemfile
├── Gemfile.lock
├── /lib
└── /protos

.protoファイルの作成

まずは拡張子が.protoのファイルを作成します。
今回はユーザー情報に関するサービスを作成するためuser.protoとしました。.protoファイルの書き方については長くなるため省略します。先ほど添付した「さくらナレッジ」の記事を参照ください。

proto/user.proto

syntax = "proto3";

message Picture {
  uint32 id = 1;
  uint32 width = 2;
  uint32 height = 3;
  enum PictureType {
    PNG = 0;
    JPEG = 1;
    GIF = 2;
  }
  PictureType type = 4;
}

message User {
  uint32 id = 1;
  string nickname = 2;
  string mail_address = 3;
  enum UserType {
    NORMAL = 0;
    ADMINISTRATOR = 1;
    GUEST = 2;
    DISABLED = 3;
  }
  UserType user_type = 4;
  repeated Picture user_icon = 5;
  uint32 default_picture = 6;
}

message UserRequest {
  uint32 id = 1;
}

message UserResponse {
  bool error = 1;
  string message = 2;
  User user = 3;
}

service UserManager {
  rpc get (UserRequest) returns (UserResponse) {}
}

特に重要となるのは下から3行のserviceに関する定義です。
ここではUserManagerがリクエストで受け取ったidを持つユーザー情報を返すRPCを定義しています。
後にこのサービスを実装することになります。つまり、必要なサービスはこの時点で.protoファイルに定義されているということになります。

protoc によるコード生成

protocコマンドを使用することで.protoから型情報などを持つファイルを自動生成することができます。
コマンドは事前にインストールが必要でした。自分はMacbookなのでbrewを使ってインストールしました。

$ brew install protobuf

Protocol Buffer Compiler Installation | gRPC

今回はRubyのファイルを作成したいのでprotocコマンドの--ruby_outオプションを指定する必要があります。
以下のコマンドはprotoディレクトリ以下のuser.protoをRubyのファイルとしてlibディレクトリに吐き出してねという指定をしています。

$ bundle exec grpc_tools_ruby_protoc -I ./protos --ruby_out=lib --grpc_out=lib ./protos/user.proto

実行後、libディレクトリ直下に2つのファイルが作成されます。

  • user_pb.rb
  • user_services_pb.rb

Rubyの場合にはuser_pb.rbには型の情報が。user_services_pb.rbにはサービスの情報がクラスに変換されています。

lib/user_services_pb.rb

module UserManager
  class Service

    include ::GRPC::GenericService

    self.marshal_class_method = :encode
    self.unmarshal_class_method = :decode
    self.service_name = 'UserManager'

    rpc :get, ::UserRequest, ::UserResponse
  end

  Stub = Service.rpc_stub_class
end

これで無事にコードの生成が完了しました。

サーバーの実装

作成したサービス・型情報を使用してサーバー側の実装をします。
クラス名は適当にServerImplとしました。重要なのは先ほど出力したサービス情報UserManager::Serviceクラスを継承することです。

grpc_server.rb

# frozen_string_literal: true

require_relative './lib/user_services_pb'

class ServerImpl < UserManager::Service
  def get(user_request, _call)
    user = User.new(
      id: user_request.id,
      nickname: 'Michael',
      mail_address: 'example.com',
      user_type: 0,
      user_icon: [
        Picture.new(id: 1, width: 100, height: 100, type: 0),
        Picture.new(id: 2, width: 100, height: 100, type: 1),
        Picture.new(id: 3, width: 100, height: 100, type: 2),
      ],
      default_picture: 1,
    )
    UserResponse.new(error: false, message: '', user: user)
  end
end

そして.protoファイルの指定に従ってRPCであるgetを定義します。
このRPCはUserRequestを受け取りUserResponseを返す必要があるので、型を満たすように関数を定義します。 やはり、こういった点で動的型付け言語との相性が悪いなぁ...と感じますね。

同ファイルにサーバー起動の処理も追記しておきます。

port = '0.0.0.0:50051'
s = GRPC::RpcServer.new
s.add_http2_port(port, :this_port_is_insecure)
GRPC.logger.info("... running insecurely on #{port}")
s.handle(ServerImpl.new)

s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])

クライアントの実装

最後にクライアントを実装します。
サーバー側と同じように出力したサービス情報を利用してクライアントを作成するのみです。
リクエストパラメーターは同じように型を満たす必要があります。

# frozen_string_literal: true

require_relative './lib/user_services_pb'

user_stub = UserManager::Stub.new('localhost:50051', :this_channel_is_insecure)
resp = user_stub.get(UserRequest.new(id: 1))

puts resp.class
puts resp

動作確認

これで全ての実装が完了したので、動作を確認してましょう。
サーバー側を立ち上げておきます。

$ bundle exec ruby grpc_server.rb

クライアント側のコードを実行して、サーバーへリクエストを行います。

$ bundle exec ruby grpc_server.rb
UserResponse
<UserResponse:
  error: false,
  message: "",
  user:
    <User:
      id: 1,
      nickname: "Michael",
      mail_address: "example.com",
      user_type: :NORMAL,
      user_icon: [
        <Picture: id: 1, width: 100, height: 100, type: :PNG>,
        <Picture: id: 2, width: 100, height: 100, type: :JPEG>,
        <Picture: id: 3, width: 100, height: 100, type: :GIF>
      ],
      default_picture: 1
    >
>

無事にユーザー情報が取得できました!

感想

慣れない実装だったのでコードが動くまでに時間がかかってしまいました。
しかし、慣れてくれば.protoファイルの定義からコード実装までサクサクできるようになると思います。
型情報からコードを自動生成できるのは本当にありがたいですね。スキーマ駆動開発で開発が効率化されると言われれるのがよく分かりました。

github.com

ただ、個人的にはgRPCと動的型付け言語との相性は、やはり静的型付けと比べると劣ると感じました。
Rubyの場合、型がクラス情報として扱われるため仮にフィールドを指定し忘れていても、実行時までエラーになることはありません。 残る疑問はRailsとgPRCの相性はどうなんだろうという点です。
基本的にRubyでAPI...となるとRails上で構築されるものが多いため、gRPCは多くの場合Rails経由で扱うことになると思います。 今回はそこまで調べられていないので、また別の記事でトライしてみたいと思います。

少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。