Wantedly Engineering Handbook
  • Wantedly Engineering Handbook
  • まえがき
  • 第一部:開発チームへの案内
    • 技術とアーキテクチャ
    • プロダクト概要(未執筆)
    • 開発チームの構造
    • コミュニケーションの全体
    • ドキュメンテーション(未執筆)
    • カレンダー
    • 障害対応の心構え
    • 効率的な社内知識の調べ方
    • 外部発信(執筆中)
  • 第二部:技術領域への案内
    • Apps
      • アプリを提供するプラットフォーム
      • デザインシステム入門
      • Web アプリのアーキテクチャ
      • プロダクトデザイナーと上手に協働するための心得
      • Web アプリのデザインシステムライブラリ
      • Web アプリ共通ライブラリ "React Shared Component" の紹介
      • モバイルアプリのアーキテクチャ
      • モバイルアプリのデザインシステムライブラリ(未執筆)
    • The System
      • protobuf スキーマと gRPC 通信
      • 実践: gRPC in Ruby
      • 実践: gRPC in Go
      • GraphQL Gateway - アプリ向けに API を公開する
      • Wantedly Visit で BFF GraphQL サーバーを辞めた理由
      • 実践: GraphQL スキーマ設計(未執筆)
      • API での認可処理 (Authorization)
      • マイクロサービス共通ライブラリ "servicex" の紹介
      • 非同期メッセージング処理入門(未執筆)
      • バッチ処理入門(未執筆)
    • Infrastructure
      • Infrastructure Squad
      • プロダクト開発のための Kubernetes 入門
      • インフラ構成概要
      • リリース・デプロイ戦略を支える技術
      • SaaS を活用する:New Relic, Honeybadger, Datadog
    • Data
      • データ基盤入門
      • レコメンデーション
      • Looker 入門
      • 推薦システムの開発に使っているツール
    • 開発プロセス
      • Git の慣習
      • ポストモーテムの取り組み
      • 負債返済日の取り組み
      • プロダクトの課題発見及び解決
      • ソフトウェアデザインの基礎
      • コードレビュー
      • コーディング規約
      • リリース・デプロイ戦略
      • 上長承認が必要な作業
      • アーキテクチャディシジョンレコード(ADR)
      • 作業ログを残す意味
      • 多言語化対応(i18n)
      • メール開発
    • 開発ツール
      • 社内利用している開発ツールの最新状況
      • kube
      • Code Coverage
      • Kubefork
  • おわりに
    • ロードマップ(未執筆)
    • Handbook の書き方
    • コントリビューター
  • 付録
    • 社内用語集
    • 主要な GitHub レポジトリのリスト(未執筆)
    • 今後の挑戦・未解決イシュー(未執筆)
    • プロダクト開発組織のバリュー(未執筆)
    • 採用についての考え方(未執筆)
GitBook提供
このページ内
  • Ruby で gRPC Server を作る
  • Rails としてのディレクトリ構成
  • Rails としてのアーキテクチャ
  • wantedly/apis - gRPC のインタフェース定義
  • RPC の実装
  • the_pb と pb-serializer によるメッセージの組み立て
  • エラーハンドリング
  • テスト
  • 起動・デバッグ
  • Ruby で gRPC Client を使う
  • gRPC Client のエラーハンドリング
  • gRPC Client のモック
  • ライブラリ・ツールの紹介

役に立ちましたか?

  1. 第二部:技術領域への案内
  2. The System

実践: gRPC in Ruby

前へprotobuf スキーマと gRPC 通信次へ実践: gRPC in Go

最終更新 10 か月前

役に立ちましたか?

本章では、 から一歩先、「Wantedly で Ruby を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。

Ruby で gRPC Server を作る

Rails としてのディレクトリ構成

まずはディレクトリ構成について説明します。基本的に Ruby on Rails の上で作ることを想定しています。

.
├── app
│   ├── grpc_services
│   ├── models
│   └── pb_serializers
├── bin
├── config
└── db

特徴的な部分を紹介します。

  • 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 を書く必要はありません。

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 オブジェクトとして返します。

# Protobuf で `wantedly.foo.bar` というパッケージがあったとき、
# Ruby では `W::Foo::BarPb` というモジュールとする規約になっている
class ProfileGrpcService < W::UsersPb::ProfileService::Service
  # @param [W::UsersPb::BatchGetProfilesRequest] req
  # @param [GRPC::ActiveCall::SingleReqView] call
  # @return [W::UsersPb::BatchGetProfilesResponse]
  def batch_get_profiles(req, call)
    # ...

    W::UsersPb::BatchGetProfileResponse.new(profiles: profiles)
  end
end

ここで引数に渡ってくる req はリクエストメッセージで、 call は呼び出しに関する情報を持つ GPRC::ActiveCallのオブジェクトです (正確には GRPC::ActiveCall::SingleReqView または GRPC::ActiveCall::MultiReqViewのどちらか)。call.metadata とするとメタデータが取得することができます。

the_pb と pb-serializer によるメッセージの組み立て

Ruby では通常は以下のようにして Protobuf object を作ります。

message = W::UsersPb::Profile.new(
  user_id: profile.user_id,        # uint64
  name: profile.name,              # string
  avatar_url: profile.avatar_url,  # string
)
message.to_proto  # => binary

先程例に出した Profile object の serializer であれば以下のように実装できます。

# @attr_reader [::Profile] object
class ProfilePbSerializer < Pb::Serializer::Base
  message W::UsersPb::Profile

  attribute :user_id
  attribute :name
  # avatar_url が google.protobuf.StringValue だったとしても、よしなに変換してくれる
  attribute :avatar_url, allow_nil: true
end

ProfilePbSerializer.new(profile).to_pb
#  => <W::UsersPb::Profile ...>

the_pb と pb-serializer をうまく活用することで、gRPC server 実装時の手間を大きく削減することが出来ます。 pb-serializer の詳しい使い方についてはリポジトリ の README や examples を参照してください。

エラーハンドリング

Ruby における gRPC server の実装では、GRPC::BadStatus というエラークラスを raise することでクライアントにエラーを返すことになります。 また、エラーコードごとに GRPC::BadStatus のサブクラスが用意されており、それを利用することも出来ます。 以下2行は同じ意味になります。

raise GRPC::BadStatus, Google::Rpc::Code::NOT_FOUND, "user not found"
raise GRPC::NotFound, "user not found"

複雑なエラー表現

さて、GRPC::BadStatus と Google::Rpc::Code を利用することでメッセージとエラーコードからなる単純なエラーを表現することは出来ました。 しかし、これでは実際のアプリケーションで表現力が足りません。 例えば単純なフォームのバリデーションエラーでも、フィールドごとにエラーメッセージを保持できる必要があります。

gRPC におけるエラーは実際には google.rpc.Status に定義されるような Protobuf message で表されます。

message Status {
  // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
  int32 code = 1;

  // A developer-facing error message, which should be in English. Any
  // user-facing error message should be localized and sent in the
  // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
  string message = 2;

  // A list of messages that carry the error details.  There is a common set of
  // message types for APIs to use.
  repeated google.protobuf.Any details = 3;
}

Ruby でのエラー詳細の扱いに関しては、the_pb gem で .new_rpc_error という関数が提供されており、それを使うと便利です。

raise Pb.new_rpc_error(
  "post is invalid",
  code: Google::Rpc::Code::INVALID_ARGUMENT,
  details: [
    Google::Rpc::BadRequest.new(
      field_violations: [{ field: 'title', description: 'タイトルは必須です' }],
    )
  ],
)

テスト

gRPC の RPC の実装は見ての通り普通のメソッドなので、最も単純には普通にメソッド呼び出しをして、返り値に対してテストをすることが出来ます。

require 'rails_helper'

RSpec.describe ProfileGrpcService do
  let(:grpc_service) { described_class.new }
  let(:call) { instance_double("GRPC::ActiveCall::SingleReqView", metadata: {}) }

  describe '#batch_get_profiles' do
    let(:request) { W::UsersPb::BatchGetProfilesRequest.new(user_ids: user_ids) }
    let(:response) { grpc_service.batch_get_profiles(request, call) }
    let(:user_ids) do
      # ...
    end

    it 'returns user profiles' do
      # ...
    end
  end
end

ただ、これだと Protobuf の marshal / unmarshal 含め gRPC の内部実装を全く介さないため、 もう少し丁寧にやるなら GrpcTesting (private library) の利用を検討するといいでしょう。

require 'rails_helper'

RSpec.describe ProfileGrpcService do
  include GrpcTesting
  let(:stub) { test_stub_class(described_class.superclass.module_parent).new(described_class.new) }

  describe '#batch_get_profiles' do
    let(:response) { stub.batch_get_profiles(request, call) }

    # ...
  end
end

起動・デバッグ

次に、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 相当のもの)の設定なども行う必要があります。

grpc-server コマンドは以下のように利用することができます。

$ bundle exec grpc-server
handling /grpc.health.v1.Health/Check with #<Method: Grpc::Health::Checker#check>
handling /grpc.health.v1.Health/Watch with #<Method: Grpc::Health::Checker(Grpc::Health::V1::Health::Service)#watch>
handling /wantedly.users.UserService/ListUsers with #<Method: UsersGrpcService#list_users>
.
.
.
gRPC server starting...
* Listening on tcp://0.0.0.0:6046
* Environment: development
Use Ctrl-C to stop

gRPC server にリクエストを送ってデバッグする

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 を単純に利用するだけであれば、以下のコードで動きます。

client = W::UsersPb::ProfileService::Stub.new(url, :this_channel_is_insecure)

ただし、Production の Microservices で利用する上では「必ずセットして欲しい gRPC interceptor(gRPC の拡張機能。後述するが、Faraday Middleware 相当のもの)」などが存在するので、個々の開発者が意識しなくてもそれらの設定が自動で行われる様に、servicex gem の中で .stub_for という「gRPC Client 生成用のメソッド」を用意しています。

client = Servicex::Grpc.stub_for(W::UsersPb::ProfileService, grpc_url)

.stub_for の実装はだいたい以下のようになっています。 元々、HTTP/1.1 の通信では servicex gem の中で Faraday Middleware などの設定を行なった API Client を提供する様にしていました。 .stub_for は同様の体験を gRPC でも提供することが意図されています。 Observability を確保するための interceptor のセットや User Agent の設定なども .stub_for で自動で行われるようになっています。

# @example
#    Servicex::Grpc.stub_for(W::UsersPb::ProfileService, UsersApi.base_url)
# @param service_class [Class<Grpc::GeneralService>]
# @param url [String]
# @yield [opts] To modify options for instantiating a Stub
# @yieldparam opts [Hash] Default options
def stub_for(service_class, url)
  opts = {
    channel_args: {
      'grpc.primary_user_agent' => Servicex.user_agent,
    },
    interceptors: Servicex::Grpc.client_interceptors,
  }
  yield opts if block_given?
  service_class::Stub.new(url, :this_channel_is_insecure, opts)
end

gRPC Client については、基本的にやる事はこれだけです。 「apis と servicex を gem として load すれば、どのマイクロサービスからでも簡単に必要な設定が行われた状態で gRPC での通信が出来る」という環境を作っています。

最終的にはだいたい以下のようなコードを書くことになります(次節で説明するエラーハンドリングをしない場合のパターンです)。

client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,  # servicex が提供する、ほかのマイクロサービスの URL を取得する関数
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

data = client.batch_get_profiles(req)
data  # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>

gRPC Client のエラーハンドリング

マイクロサービス・アーキテクチャでの開発を行う際には、障害の分離のために、通信の失敗について考慮する必要があります。 通信が失敗した際、全体を失敗させるのではなくなんらか部分的に処理が継続可能な場合は、メソッド呼び出しで errors に想定しているエラーの種類を列挙してください。

これは、

client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

res = client.batch_get_profiles(req, errors: [:deadline_exceeded])

これは、GRPC のエラーの種類に対応するシンボルとして指定できます。例えば、GRPC のエラーである DEADLINE_EXCEEDED に対応するシンボルは :deadline_exceeded です。 また、サーバー側に原因があると考えられる UNKNOWN, INTERNAL, UNAVAILABLE を :server_errors でまとめて指定できるようにしています。

多くのケースでは、errors: [:server_error, :deadline_exceeded] を書くことをまず考えるのが良いプラクティスでしょう。コードレビューの際にも、そのことをチェックしてください。

errors を指定すると成功と失敗を表現するオブジェクトが返ってくるので、パターンマッチで処理しましょう。

res = client.batch_get_profiles(req, errors: [:server_errors, :deadline_exceeded])
case resp
in Result::Success(data)
  data # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
in Result::Failure(error)
  # Failure の場合は自動で Honeybadger に送られます。
  error # => [<W::Users::ProfileServiceError: ...>]
end

エラーの種類によって個別の処理をしたい場合は、こういう風にパターンマッチを書くと良いでしょう。

res = client.update_profile(req, errors: [:server_errors, :deadline_exceeded, :permission_denied])
case resp
in Result::Success(data)
  data
in Result::Failure(type: :permission_denied, error: _error)
  Pb::Visit::ToastError.new(message: "このデータを更新する権限がありません")
in Result::Failure(_error)
  Pb::Visit::ToastError.new(message: "更新が行なえませんでした。しばらくしてからもう一度やり直してください。")
end

gRPC Client のモック

before do
  GrpcMock.stub_request('/wantedly.users.ProfileService/BatchGetProfiles').to_return do |req, call|
    W::UsersPb::BatchGetProfilesResponse.new(
      profiles: [],
    )
  end
end

ライブラリ・ツールの紹介

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 を実現する

    • RPC が返しているオブジェクトが正しい Protobuf messaage かどうかを検証する interceptor

    • gRPC server を起動するためのボイラプレートを隠蔽した gem

    • 簡単に gRPC server を叩ける CLI

話を聞きに行きたい

もっと知りたい

  • Wantdly の Rails のアーキテクチャについて理解したい

  • gRPC について知りたい

インタフェース定義におけるルール・tips などはを参照してください。

上記は最も単純な例です。で紹介されているラッパー型など、 いわゆる が入ってくると、もっと面倒なコードを書くことになります(面倒なのでここには書きません)。

ここで活躍するのが と という2つの gem です。

the_pb は Ruby オブジェクトを前述した Well-Known Types に変換するユーティリティを提供する gem です。 pb-serializer は のような DSL で Protobuf object の Serializer を実装できる gem です。内部では the_pb に依存しています。

この「エラーコード」は google.rpc.Code に定義されている Protobuf の Enum になります。 エラーコードの使い分けなどについてはを参照してください。

この google.rpc.Status のうち、 Any の配列である details にエラーの詳細を詰めることが可能です。 では に定義されるメッセージの利用が推奨されており、Wantedly でもそれに則っています。

こういった「どの repository でも共通で必要になる処理」については、個々の開発者が気にする必要がないよう、 と呼ばれる社内 library (gem) の機能として提供するようにしています。具体的には、grpc-server という名前のコマンドを用意しており、それを実行するだけで「gRPC Service Class の load および Handler としての登録、その他に共通で必要になる各種設定」が行われるようにしています。

この grpc-server コマンドのうち、Health Checker やシグナルハンドリングなどの設定を隠蔽したものは gem として公開されています。

JSON/HTTP な API であれば cURL コマンドや , といったツールで実際にリクエストを送ってデバッグすることがあるかもしれません。 gRPC server では ほか、 や といった 3rd party のツールが存在します。 一方でこれらのツールでは「元になった .proto ファイル」もしくは「Server Reflection に対応した gRPC server」が必要になり、Wantedly の環境では少し使いづらいという問題がありました。

そこで Wantedly では というツールを利用し、なるべく少ない手順・依存で gRPC server にリクエストを送れるような環境になっています。詳しくは もしくは を参照してください。

gRPC における通信の失敗は gRPC のエラーコードとして表されます。 ( にその種類と、デフォルトで考慮すべきものがどれかの指針が書かれているので、確認しておいてください。)

多くの場合、テストコード中ではマイクロサービス間通信を含む外部へのリクエストを禁止したくなります。 HTTP であれば gem を使うことが多いですが、gRPC では という gem を利用しています。

see also

Slack:

gRPC in Ruby の Quick Start
protobufスキーマとgRPC通信
protobufスキーマとgRPC通信
Well-Known Types
the_pb
pb-serializer
ActiveModelSerializers
protobufスキーマとgRPC通信
Google API Design Guide の Errors
google/rpc/error_details.proto
servicex (internal)
grpc_server
Insomnia
Postman
標準でコマンドラインツールが提供されている
grpcurl
evans
giro
giro の解説をしているブログ
giro の使い方ドキュメント (internal)
Handbook - gRPC 通信のエラーハンドリング
webmock
grpc_mock
the_pb
pb-serializer
computed_model
grpc_newrelic_interceptor
grpc_opencensus_interceptor
grpc_access_logging_interceptor
reloader_interceptor
grpc_typechecker: A dynamic type checker for gRPC methods
gRPC Ruby でハマらないための型チェッカー | Wantedly Engineer Blog
grpc_server
rerost/giro
Server Reflectionが実装されていないgRPCサーバーでも簡単に叩けるCLIを作る | Wantedly Engineer Blog
#rubyist
マイクロサービス・アーキテクチャと共存する Ruby on Rails のアーキテクチャ的拡張 - その事例と可能性 / The Architectural Extension of Ruby on Rails to fit to microservices - Speaker Deck
Real World Performance of gRPC - gRPC 利用による劇的なパフォーマンス改善 | Wantedly Engineer Blog
gRPC Internal - gRPC の設計と内部実装から見えてくる世界 | Wantedly Engineer Blog