1
/
5

実践: Ruby gRPC API サーバ

こんにちは、Wantedly Visit のバックエンドエンジニアをしている鴛海です。

本稿は、WANTEDLY TECH BOOK 8 から「実践: Ruby gRPC API サーバ」という章を抜粋し加筆修正を加えたものです。ウォンテッドリーでは WANTEDLY TECH BOOK のうち最新版を除いた電子版を無料で配布しています。ぜひ読んでみてください。

過去の WANTEDLY TECH BOOK を入手する

以下、本文です。

1: はじめに

本稿では、gRPC の Quick Start から一歩先に進むために何をすればいいかを、Ruby gRPC API サーバを運用した経験を元に紹介します。「2: gRPC の概要」で gRPC とは何か、どういう利点があるのかを紹介した後、「3: Ruby で gRPC API サーバを作る」で Ruby を使って gRPC サーバを作る際のベストプラクティスを紹介します。「4: 新しい API を作る」では gRPC サーバを立てた後、実際にどのように開発を進めていくか、またその中で知っておいた方がよい情報を紹介します。

2: gRPCの概要

gRPC とは、high-performance で様々な環境で動作することを謳った RPC フレームワークです。Protocol Buffers (protobuf) を IDL として使用して、定義した .proto ファイルからサーバ・クライアントコードを生成することができます。コード生成は現在 Go、Ruby、Python など様々な言語でサポートされており、複数の言語のサーバが存在するマイクロサービス群でも利用することができます。また、gRPC は HTTP/2 を利用することによって双方向の通信も使用することができます。

2.1: RPC とは

RPC とは Remote Procedure Call の略で、ネットワーク越し(=遠隔)にメソッドやサブルーチンなどの手続きを呼び出す技術です。多くの RPC は、あらかじめインターフェイスを決めることによって遠隔にある手続きを呼び出すことを可能にしています。

2.2: REST vs RPC

REST と RPC では API の設計方針が異なります。REST はリソース指向のアーキテクチャスタイルですが、RPC はあえて言うならメソッド指向のアーキテクチャスタイルです。

チャットサービスのメッセージを送信する API を考えてみます。REST API では POST https://foobar.com/message のような URI を参照して message というリソースを作成することでメッセージを送信します。REST は使える動詞を GETPOSTPUTDELETE という HTTP メソッドだけに制限することで API の命名に一貫性を持たせようとする狙いがあります。そのため、URI には動詞を含めないのが良い URI だと言われています(参考: RESTful API Design: nouns are good, verbs are bad, https://cloud.google.com/blog/products/api-management/restful-api-design-nouns-are-good-verbs-are-bad)。しかし、HTTP メソッドだけでは表現力が乏しいため先ほどのメッセージ送信の例の POST https://foobar.com/message という URI だと直感的にメッセージ送信をすることが分かりにくいという場合も出てきます。

対して RPC では SendMessage() という(サーバにある)メソッドを呼ぶことでメッセージを送信する API を設計します。RPC はメソッドを呼んでいるだけなので命名もメソッドと同じような 動詞を重視した命名
になります。つまり、REST とは対照的に一貫性でなく表現力を上げる命名規則になっています


REST と RPC のどちらを使うのがよいかという議論をする際に重要になってくるのが命名による学習コストと表現力のトレードオフ です。REST は動詞が限定されているおかげで学習コストが少ないというメリットがありますが、表現力が乏しいためより的確な命名は難しくなります。一方で RPC はドメインによって動詞が変わるので学習コストが高くなってしまいますが、そのドメインの用語を使えることでドメインを知っている人にとってはより正確に理解できる名前となります。

学習コストと表現力のどちらを取るかという問題は状況によって変わってきます。クライアントが不特定多数になる公開された API を作る場合は REST の方が向いているでしょう。公開された API では、すべてのクライアントがドメインを熟知している訳ではありません。そのため学習コストが多くの人にのしかかってしまいます。

逆に組織内の API サーバのようなクライアントが限定される場合は、RPC の方が向
いていると言えるでしょう。組織内であれば既にドメイン知識を共有している場合が多いため学習コストはそこまで大きくならずに済みます。ただ、様々な動詞が使えてしまうと REST のような一貫性がなくなってしまうのは事実としてあるので、普段プログラミングでメソッドを作るときのように命名規則を決めてある程度学習コストを低減することも重要です。

3: Ruby で gRPC API サーバを作る

この章では、実際に Ruby で gRPC サーバを作る際のベストプラクティスを説明します。まだ Ruby で gRPC サーバを立てたことがない方は gRPC の Ruby Quick Start を行うことをおすすめします。この章ではこの Quick Start から一歩進んだ、実際に開発をした中で学んだ開発のしやすい構成を説明します。

3.1: ディレクトリ構成

まずはディレクトリ構成について説明します。基本的に Ruby on Rails をまねて作られています。

.
├── app
│   ├── messages
│   ├── models
│   ├── protos
│   ├── servers
│   └── services
├── grpc_server.rb
├── lib
│   └── interceptors
├── bin
├── config
├── db
└── protos

特徴的な部分をいくつか紹介します。app/servers に protobuf から生成したサービスクラスを継承したサービスクラスを置いていてクラス名は *Server としています。これは protobuf のサービスクラスがサービス層と被っているため、より明確に区別するために Server にしてあります。 protos ディレクトリには .proto ファイルが置かれていて、 app/protos ディレクトリには protobuf によって生成されたコードが置かれています。app/messages ディレクトリには、 .proto ファイルの中で定義したメッセージの中でリソースであるものをラップし ActiveModel として扱うためのクラスが入っています。詳しくは次節で解説します。

3.2: メッセージオブジェクトをラップする

protobuf から生成されたメッセージオブジェクトは便利ですが、独自のメソッドを生やしたくなる場合があります。Ruby は Open Class なのでいじることは可能ですが、完全にラップした方がコードの追いやすさや見やすさの点でよいと考えて別のクラスとして作っています。例えば、 Book というメッセージがあった場合以下のように定義します。

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :title, :string
  ...

  def to_pb_message
    Foo::BarPb::Book.new(
      title: title,
      ...
    )
  end
end

#to_pb_message というメソッドは protobuf の生成したメッセージオブジェクトに変換するメソッドです。特に Timestamp を扱う場合は #to_pb_message 内で Timpstamp から protobuf で扱うことが可能な google.protobuf.timestamp 型への変換を行うと便利です。

3.3: インターセプターで呼び出されたメソッドの前後に処理を挟む

インターセプターとは、RPC メソッドが呼び出される前後に処理を挟むことができる
仕組みです。 RpcServer のインスタンスを作る際に GRPC::RpcServer.new(interceptors: [FooInterceptor, BarInterceptor]) のように指定して使用します。例えばエラーを捕捉し通知するインターセプターは以下のようになります。

class ErrorNotificationInterceptor < GRPC::ServerInterceptor
  def request_response(request:, call:, method:)
    begin
      yield
    rescue StandardError => e
      notify_error(e)
      raise e
    end
  end
end

#request_response メソッドは引数にリクエストメッセージ( request )、呼び出しに関する情報( call )、呼び出されたメソッドの情報( method )の 3つを受けとります。このメソッド内で yield して呼び出し先の RPC メソッドを呼ぶことで、RPC メソッドを呼び出す前後に処理を挟むことができます。上の例のようなエラー通知の他にも、アクセスロギングやエラークラスの変換( ActiveRecord::RecordNotFound
から GRPC::NotFound への変換等)などに使うことができます。

4: 新しい API を作る

新しい API (RPC メソッド) を作る作業は大きく分けて「インターフェイスの定義」と「RPC メソッドの実装」に分けることができます。この章では「インターフェイスの定義」と「RPC メソッドの実装」の2つを詳しく説明していきます。

4.1: インターフェイスの定義

「2: gRPCの概要」で説明したように、gRPC サーバでは Protocol Buffers を使用してインターフェイスを定義します。この節ではインターフェイスを定義する方法とベストプラクティスについて解説していきます。

4.1.1: Protocol Buffers によるインターフェイスの定義


Protocol Buffers では以下のような構文でインターフェイスを定義することができます。

service BookShelf {  
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
}  

message ListBooksRequest {
  enum Category {
    CATEGORY_UNSPECIFIED = 0;
    SCIENCE_FICTION = 1;
    COMEDY = 2;
  }

  Category category = 1;
} 

message ListBooksResponse {
  repeated Book books = 1;
}



この BookShelf というサービスには、 ListBooksRequest というメッセー
ジを受け取って ListBooksResponse というメッセージを返す ListBooks というメソッドが存在することが分かります。さらに詳しい Protocol Buffers の記法やどんな型を使えるのかは Protocol Buffers 公式のリファレンスが役に立ちます。このようにインターフェイスを見れば何を渡せばいいか、何が返ってくるのかが明確になりクライアントにとっては非常に助かります。また、インターフェイスが定義されていることによってサーバの実装がされていなくともクライアントを実装することができる、いわゆる スキーマ駆動開発 が可能になります。

4.1.2: gRPC API サーバのためのインターフェイス設計ベストプラクティス

  • 命名規則
    gRPC はその名の通り RPC フレームワークなので、「2.2: REST vs RPC」で説明した「表現力の高い動詞」が重要になると共に、学習コストを減らすための命名規則が必要です。Google の API 設計ガイドにある「命名規則」のページは非常に役に立ちます。このページに書かれていることからいくつか自分の経験上役に立ったものを紹介します。
    • メソッド名
      「2.2: REST vs RPC」で説明したように RPC API における動詞の命名は最も重要です。メソッド名は VerbNoun すなわち「動詞 + 名詞」の形で表現することが推奨されています。ただし、 VerbNoun でも IsBookPublisherApproved のような直説法ではなく、 CheckBookPublisherApproved のような命令法を使用することが 推奨 されています。また、メソッド名には前置詞を 含めないことが推奨されています。前置詞を使う代わりに既存のメソッドにフィールドを追加したり、別の動詞を使うなどしてメソッド名をシンプルにする必要があります。
    • リクエストメッセージとレスポンスメッセージ
      RPC メソッドのリクエストメッセージとレスポンスメッセージはそれぞれ RequestResponse という接尾辞にすることが 推奨 されています。ただし、 Delete* メソッドのレスポンスで使用する google.protobuf.Empty のような空のメッセージや Get* メソッドでのレスポンスで使用するリソースタイプは例外となります。
    • 列挙型(Enum)
      列挙型の名前は UpperCamelCase にし、列挙値は CAPITALIZED_NAMES_WITH_UNDERSCORES を使用することが推奨されています。また、列挙型の最初の値はデフォルト値となるため ENUM_TYPE_UNSPECIFIED という名前にすることが推奨されます。
enum FooBar {
  // The first value represents the default and must be == 0.
  FOO_BAR_UNSPECIFIED = 0;
  FIRST_VALUE = 1;
  SECOND_VALUE = 2;
}

列挙型は C++ と同じようなスコープになっているため、列挙値は列挙型が定義されているレベルでのスコープとなります。例えば次の定義では、Sample1 Sample2 は同レベルに存在するため同じ BAR という名前を使用することができません。Sample1 Test.Sample3 のようにレベルが違う場合は同じ名前を使用することができます。特にトップレベルに列挙型を定義する場合にはスコープに気を付けて定義してください。

enum Sample1 {
  FOO = 0;
  BAR = 1;
}
enum Sample2 {
  BAR = 0; // Error: "BAR" is already defined.
  BAZ = 1;
}

message Test {
  emum Sample3 {
    BAR = 0; // OK
  }
}


  • Service の分割
    gRPC の Service は複数ハンドルさせることできます。
    gRPC に限った話ではありませんが、Service が大きくなると保守が大変になります。
    Service が肥大化してきた場合はサブドメインに切るなど分割し、複数のサービス
    をハンドルすることをおすすめします。


4.2: RPC メソッドの実装

RPC メソッドはリクエストメッセージを受け取ってレスポンスメッセージを返すただのメソッドです。
以下のように protobuf が生成したサービスクラスを継承したクラスにメソッドを定義します。

class ShelfService < Foo::Bar::Shelf::Service
  def list_books(req, call)
    # Something to do
    Foo::Bar::ListBooksResponse.new
  end
end

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

4.2.1: エラーハンドリング
gRPC におけるエラーハンドリングは単純で、 ただ raise GRPC::XXX とするだけです。使えるエラーステータスは grpc/statuscodes.md を参照してください。このとき気をつける必要があるのは gRPC のエラーステータスは HTTP のステータスコードと一対一対応していないことです。例えば、 FAILED_PRECONDITION INVALID_ARGUMENT OUT_OF_RANGE は HTTP で言えばどれも 400 Bad Request に当てはまります。FAILED_PRECONDITION は「削除するディレクトリが空ではなかった」などシステムが操作を行うのに必要な状態でないときに使用し、INVALID_ARGUMENT は「不正なファイル名」などシステムの状態に関わらない無効な引数の場合に使用します。OUT_OF_RANGE は「生年月日として現在よりも先の日時が指定された」など値の有効範囲を越えた場合に使用します。「INT64 の範囲を越えた」場合には INVALID_ARGUMENT を使用します。

4.2.2: RPC メソッドをテストする
RPC メソッドのテストはただメソッドテストをするだけです。実際にサーバを立てローカルでリクエストを送って確かめたい場合は evans が便利です。 .proto ファイルを元に REPL やCLI 形式でリクエストを投げることができます。

5: おわりに

gRPC は本稿で解説した部分以外にも高速な通信や双方向通信が可能になるなどの強みがあります。しかしいざ Ruby での gRPC サーバを運用しようと思っても、まだまだ普及しておらず、情報が少ない状況です。本稿が Quick Start から一歩前に進むためのものとなれば幸いです。

Wantedly, Inc.では一緒に働く仲間を募集しています
24 いいね!
24 いいね!
同じタグの記事
今週のランキング
Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?