Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
スターティングgRPC_読書メモ

gRPC と REST の違い

gRPC とは?

gRPC は RPC フレームワークのひとつ。
Remote Procedure Call の略で、「あるプログラムがネットワーク上の異なる場所に配置されたプログラムを呼び出して実行すること」を指す。

そもそも RPC とは?

あるサービスから別のサービスのアプリケーションの処理を呼び出す技術。
RPC を使うことで、違うアプリケーションのロジックを、あたかも自分のアプリケーションの中に実装されているかのように扱うことができる。

RPC でよく使われている技術には、gRPC のほかに以下がある。

  • JSON-RPC
  • SOAP
  • Apache Thrift

REST と gRPC の比較

REST には「リソース志向を強く打ち出している」という思想的な違いがある。
(リソース(オブジェクト)を中心に考え、これに対して HTTP メソッドで操作していく、という考え方。)
RPC ではメソッドの呼び出しが基点となり、データはあくまでその副産物であるので、考え方としては REST の逆になる。

REST は規格が厳密に決められたものではなく、シンプルでスケーラブルな API を作るための「設計原則」。
そのため、REST では原則にしたがって自分で仕様を決めて実装することが求められる。
一方 RPC フレームワークは、規格や仕様に沿って実装されたライブラリやフレームワークとして提供される。

REST と比較した場合の gRPC の長所

  • HTTP/2 による高速な通信
  • Protocol Buffers
  • 柔軟なストリーミング形式

HTTP/2 による高速な通信

HTTP/2 では通信時にデータがテキストではなくバイナリにシリアライズされて送られる。
そのため、小さい容量で転送でき、ネットワーク内のリソースをより効率的に使用することができる。

また、HTTP/2 では一つのコネクションで複数のリクエスト・レスポンスをやりとりできる。
そのため gRPC でも、コネクションは常時貼られっぱなしの状態になる。
リクエストの度に接続と切断を行う必要がなく、またヘッダーを都度送る必要がないので、より効率的な通信になる。

高速であるという点において、gRPC はマイクロサービスの内部 API の通信規格として評価されている。

Protocol Buffers

gRPC では、Protocol Buffers のフォーマットにシリアライズしてデータをやりとりする。

Protocol Buffers の一番の特徴は .proto ファイルという IDL(インタフェース記述言語)。
.proto ファイルを書いて、コンパイラを実行すると任意の言語のサーバ・クライアント用コードが自動生成される。
自分で API インタフェースを実装したり、シリアライズされたデータのエンコード・デコード処理を書く必要がないため便利。

gRPC ではスキーマが最初に書かれるため、.proto ファイルを見れば API の仕様は常に明確な状態になる。
そのため、開発のスタイルが必然的にスキーマファーストになる。

また、 .proto ファイルでは静的型付けを行う。
IDL で記述された仕様は、各言語のコード生成時に適切な型へと変換される。

Protocol Buffers の各言語への対応状況は以下の通り。

柔軟なストリーミング方式

gRPC では API で一般的な、ひとつのリクエストに対してひとつのレスポンスを送る形式以外に、単方向・双方向のストリーミング RPC に対応している。

  • Unary RPCs
    • クライアントから送られた 1 つのリクエストに対して、レスポンスを 1 つ返す方式
  • Server Streaming RPCs
    • クライアントから送られたリクエストに対して、レスポンスを複数回に分けて返す方式
    • お知らせの通知やタイムラインのリアルタイム更新など
  • Client Streaming RPCs
    • クライアントからリクエストを分割して送り、サーバ側がすべてのリクエストを受け取ってからレスポンスを返す方式
    • 大きなデータを分割してアップロードしたい場合などに有効
  • Duplex Streaming RPCs
    • 双方向のストリーミング通信を行う方式
    • チャットやオンラインゲームなど

REST と比較した場合の gRPC の短所

  • HTTP/2 非対応である危険性
    • たとえば、2020/3 時点で AWS の ALB は gRPC に対応していない
  • ブラウザの対応状況が不十分
  • 言語によって機能の実装状況にばらつきがある
    • Go や Java と比較して、他の言語は機能追加が遅くなる傾向にある
  • バイナリにシリアライズすると人間が読めない
  • REST も十分速い
  • gRPC は確かに速いが、速度が何倍も改善する訳ではない

.proto ファイルを書いてみよう

gRPC ではシリアライズフォーマットとして Protocol Buffers を使う。
Protocol Buffers では .proto を拡張子として持ったファイル上にスキーマ定義を行い、protoc コマンドで各言語用のコードを生成する。

Protocol Buffers

Protocol Buffers は安定性の面から proto3 を使うことが推奨。

proto ファイルはこんな感じで書くことができる。

syntax = "proto3";

package myapp;

service AddressBookService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

message SearchRequest {
  string name = 1;
}

message SearchResponse {
  Person person = 1;
}

message Person {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated PhoneNumber phone_numbers = 4;

  enum PhoneType {
    UNKNOWN = 0;
    MOBILE = 1;
    HOME = 2;
    WORK = 3;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType phone_type = 2;
  }
}

バージョン

先頭に proto のバージョンを明記する。

syntax = "proto3";

パッケージ定義

名前の衝突を避けるために、proto ファイルにパッケージ名を指定できる。

package myapp;

import

他のパッケージから、パッケージを指定して違う proto ファイルで定義したメッセージ型を使うことができる。

import "../myapp/addressbook.proto";

message OtherApp {
  myapp.Person = 1;
}

サービスと RPC メソッド定義

API におけるサービスを定義する。
サービスには複数の RPC メソッドを定義することができる。

service AddressBookService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

RPC メソッドの引数と返り値の前に stream を付加すると、単方向・双方向ストリーミング RPC を表現することができる。

// サーバストリーミング RPC
rpc Search (SearchRequest) returns (stream SearchResponse);

// クライアントストリーミング RPC
rpc Search (stream SearchRequest) returns (SearchResponse);

// 双方向ストリーミング RPC
rpc Search (stream SearchRequest) returns (stream SearchResponse);

スカラー型

Protocol Buffers ではすべての値が型を持つ。
型はスカラー型とメッセージ型に大別される。
スカラー値には数値、文字列、真偽値、バイト配列がある。

IDL 注記
double
float
int32 可変長エンコーディング。負の数を扱う場合は sint32 推奨
int64 可変長エンコーディング。負の数を扱う場合は sint64 推奨
uint32 可変長エンコーディング
uint64 可変長エンコーディング
sint32 可変長エンコーディング。符号付き整数値。通常の int32 よりも効率的に負の数をエンコードできる
sint64 可変長エンコーディング。符号付き整数値。通常の int64 よりも効率的に負の数をエンコードできる
fixed32 4 バイトの固定長。値が 2 の 28 乗より大きい場合は、uint32 よりも効率的
fixed64 8 バイトの固定長。値が 2 の 56 乗より大きい場合は、uint64 よりも効率的
sfixed32 4 バイトの固定長
sfixed64 8 バイトの固定長
bool
string UTF8 もしくは 7bit ascii 文字列
bytes 任意のバイト配列

メッセージ型

複数のフィールドを持ったメッセージ型を定義できる。

message Person {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

タグナンバー

フィールドの右側の数字をタグナンバーと呼ぶ。

タグナンバーは同じメッセージの中で一意である必要がある。
タグナンバーはシリアライズされたフォーマットでフィールドを識別するために使われる。
そのため運用中にフィールドを削除した場合、思わぬ不具合や障害の発生を避けるため、一度使ったタグナンバーは再利用せず廃番にする必要がある。

reserved 識別しを使って廃番にしたタグナンバーを明記しておくと、他の開発者が誤って再利用することを防げる。

message Person {
  // (中略)
  reserved 7, 8, 10 to 19;
}

また deprecated オプションを付与することで、廃止予定かつ非推奨であるフィールドを明示的にすることができる。

message Person {
  // (中略)
  string phone_number = 6 [deprecated = true];
}

1〜15 までの数字は 1 バイトでエンコードされるが、16〜2047 までのタグナンバーは 2 バイトでエンコードされる。
なるべく 1〜15 までのタグナンバーを使っておくと効率的。

repeated

フィールドの型に repeated とつけることで配列を表現できる。

message Person {
  int32 id = 1;
  repeated PhoneNumber phone_numbers = 2;
}

列挙型

列定義の先頭に enum とつけることで列挙型を定義できる。

enum PhoneType {
  UNKNOWN = 0;
  MOBILE = 1;
  HOME = 2;
  WORK = 3;
}

message PhoneNumber {
  string number = 1;
  PhoneType phone_type = 2;
}

要素の先頭は必ず 0 にする必要がある。
(Protocol Buffers におけるデフォルト値の仕様によるもの。)
0 は必ずしも未定義とする必要はないが、API の仕様を揃える意味でも未定義値都することを推奨する。

allow_alias オプションを設定すると、同じ値に異なるラベルを割り当てることができる。

enum PhoneType {
  option allow_alias = true;
  UNKNOWN = 0;
  MOBILE = 1;
  HOME = 2;
  OFFICE = 3;
  SCHOOL = 3;
}

マップ

マップ(連想配列)を使いたい場合、次のような記法がある。

map<key_type, value_type> map_field = N;

key_type にキーとなる型を、value_type に値となる型を定義する。
キーにできるのは整数値、文字列、真偽値のみ。

message Person {
  int32 id = 1;
  map<string, AddressBook> address_books = 2;
}

oneof

フィールドの先頭に oneof と付与することで、「複数の型の中からどれかひとつ」という定義を行うことができる。

message GreetingCard {
  int32 id = 1;
  oneof message {
    string text = 2;
    Image image = 3;
    Video video = 4;
  }
}

message Image { ... }
message Video { ... }

oneofrepeated にすることができず、oneof の中でも repeated を使うことはできない。

デフォルト値

スカラー型はすべてデフォルト値を持つ。
(つまり、必ず値が入るので未定義状態にはできない。)

デフォルト
string 空文字
bytes 空配列
bool false
数値 0
enum 一番最初に定義された値。0でなくてはならない
メッセージ型 フィールドはセットされない。実装に依存する
repeated 空配列

どうしても未定義を表現したい場合は google.protobuf.wrappers.proto で定義された型を使うことができるが、実装が複雑になるため、そもそも未定義の区別が必要かどうかを検討した方が良い。

Well Known Types

Google が定義した便利なメッセージ型(Well Known Types)がいくつかある。
proto ファイルをインポートすることで使うことができる。

以下の Well Known Types がよく使われる。

  • google.protobuf.Timestamp
  • google.protobuf.Duration
  • google.protobuf.Empty
  • google.protobuf.Any

コメント

文のはじめに // と打つことで、コメントを埋め込むことができる。
コメントを入れておくと、自動生成されたボイラープレートコードにも同じコメントを付加してくれるので便利。

各言語で実際にどのようなコードが生成されるか

生成されたコードを読んで確認することもできるが、可読性はあまりよくない。
詳細は公式ドキュメントに記載されているため、実装前に確認しておくのがオススメ。

Go 言語でつくる gRPC サーバ

Go 言語でのサーバー側の実装と解説

gRPC のエラー表:

エラー ステータス 意味
OK 0 成功
Canceled 1 クライアントが処理をキャンセルした
Unknown 2 不明なエラー。ハンドリングされないエラー
InvalidArgument 3 送られたパラメータが不正である
DeadlineExceeded 4 サーバ側の処理がタイムアウトした
NotFound 5 クライアントがリクエストしたリソースは存在しない
AlreadyExists 6 作成しようとしたリソースは既に存在している
PermissionDenied 7 操作を行う権限がない
ResourceExhausted 8 Rate Limit を超えた。リソースが足りない
FailedPrecondition 9 不正な処理のため拒否された。リトライ禁止
Aborted 10 処理が中断された。要リトライ
OutOfRange 11 範囲外へのリクエストを試みた
Unimplemented 12 未実装である
Internal 13 システム内部エラー
Unavailable 14 システムが一時的に利用できない。リトライ可
Dataloss 15 データが欠損もしくは破損している
Unauthenticated 16 認証に失敗した

CLI ツールで動作確認する

gRPC サーバの挙動を確認する方法いろいろ:

サーバリフレクションを使うと、protoc で生成したコードを使わなくても Protocol Buffers の定義をサーバから直接取得したり、RPC メソッドを実行することができる。
(grpc_cli を使う場合は必須。)

インターセプタでログや認証を追加してみよう

インターセプタとは?

インターセプタは gRPC におけるミドルウェア。
インターセプタを使うことで、RPC メソッドがリクエストを受け取る前、あるいはレスポンスを返した後のタイミングで、任意の処理を割り込ませることができる。

以下のような、複数の RPC 間で共通して行いたい処理を実装したい場合に便利。

  • 認証
  • ログ処理
  • 監視
  • バリデーション

Go 言語で開発する場合、go-grpc-middleware を使うことで、よく使われるインターセプタを簡単に追加することができる。

  • 認証:grpc_auth
  • ログ:grpc_ctxtags、grpc_zap、grpc_logrus
  • 監視:grpc_prometheus、grpc_opentracing
  • クライアント用:grpc_retry
  • サーバ用:grpc_validator、grpc_recovery

protoc でプラグインを使ってみる

バリデータ:

message InnerMessage {
  int32 some_integer = 1 [(validator.field) = {int_gt:0, int_lt:100}];
  double some_float = 2 [(validator.field) = {float_gt:0, float_lt:1}];
}

grpc_gateway プラグインを設定すると、gRPC サーバの前段に REST API の proxy を立てることができる。
HTTP 1.1 のロードバランサを経由する必要がある場合など、どうしても gRPC を使えない場合に便利。

import "google/api/annotations.proto";

service YourService {
  rpc Echo(StringMessage) returns (StringMessage) {
    option (google.api.http) = {
      post: "/v1/example/echo"
      body: "*"
    };
  }
}

単方向ストリーミングでつくる画像アップロード API

ストリーミングを使う gRPC アプリケーションの設計

gRPC では一度に送信できる容量はデフォルトで 4MB までとされている。
設定を変更することでこの制限は増やすことができるが、もともと Protol Buffers 自体が小さなデータセットのシリアライズに特化した仕様になっていることを考えると、安易に設定を変えない方が好ましい。

大容量のデータセットを送りたい場合、gRPC ではリクエストを分割してストリーミングさせながら送ることができる。

Google API に学ぶ proto スタイルガイド

Google の設計規則

Google Cloud API では、REST だけでなく gRPC のインタフェースも提供されている。

Google Cloud API の gRPC のインタフェースでは、Protocol Buffers でスキーマ定義する場合の命名規則が決められ、また公開されている。
(これが必ずしも正解というわけではないが)スキーマ定義を決める際の参考にすると良い。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment