gRPC の概要を掴むために、特徴、アーキテクチャ、ライフサイクルを理解する
はじめに
gRPC の概要を掴むために、特徴、アーキテクチャ、ライフサイクルについて理解した内容を整理しました。
gRPC ってそもそもなに?という方は前回のエントリを読んで頂くと理解が深まると思います。
gRPC を使いためには「サービス定義」が必要
多くの RPC システムと同じように、 gRPC も 「サービスを定義」 するという思想に基づいています。
サービス は メソッド を所有しています。
メソッド はリモートから呼び出されることを想定していて、1 つ以上のパラメータを受け取り、1 つ以上の値を返却します。
「サービス定義」の構成要素を視覚化すると以下のようになります。
- サービス X
- メソッド A
- パラメータ(インプットとなるメッセージ)
- 戻り値(アウトプットとなるメッセージ)
- メソッド B
- メソッド C
...
- サービス Y
- サービス C
...
gRPC では インターフェイス定義言語(IDL) として Protocol Buffers を利用します。
IDL を使って、「サービス」のメソッドインターフェイスと、サービスメソッドのパラメータや戻り値となる「メッセージ」のデータ構造を定義します。
IDL ファイルの拡張子は proto
とします。
そのため、サービス定義ファイルを proto ファイルと呼ぶ事が多いです。
proto ファイルのサンプルコードを掲載します。
syntax = "proto3";
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
掲載したコードに記載されている内容を箇条書きすると以下のとおりです。
- IDL である Protocol Buffers の構文バージョン
proto3
HelloService
サービスとそのメソッドであるSayHello
SayHello
メソッドのパラメータメッセージHelloRequest
SayHello
メソッドの戻り値メッセージHelloResponse
サービスメソッドには 4 つの種類がある
gRPC では、4 種類のサービスメソッドのいずれかを選んで定義します。
ここからは、4 種類のサービスメソッドのそれぞれを紹介します。
Unary RPC ( 単項 RPC )
クライアントはサーバに対して「リクエスト」を 1 つ送信します。
その結果、「メッセージ」を 1 つ受信します。
通常の関数呼び出しに似ていてイメージしやすいですね。
rpc SayHello(HelloRequest) returns (HelloResponse);
+------+ +------+
|Client|----->|Server|
| |<-----| |
| | | |
| | | |
+------+ +------+
Server streaming RPC ( サーバストリーミング RPC )
クライアントはサーバに対して「リクエスト」を 1 つ送信します。
その結果、「ストリーム」を 1 つ受信します。
クライアントは「ストリーム」から 1 つ以上の「メッセージ」を読み取ることができます。
クライアントが送信し、その後受信するという順序は担保されます。
また、「ストリーム」から取得される複数のメッセージの順序は保証されます。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
+------+ +------+
|Client|----->|Server|
| |<-----| |
| |<-----| |
| |<-----| |
+------+ +------+
Client streaming PRC ( クライアントストリーミング RPC)
クライアントはサーバに対して「ストリーム」を 1 つ送信します。
クライアントは「ストリーム」に対して 1 つ以上の「メッセージ」を書き込むことができます。サーバがストリームから全ての「メッセージ」を読み取るまで待たされます。
その結果、「メッセージ」を 1 つ受信します。
クライアントが送信し、その後受信するという順序は担保されます。
また、「ストリーム」から取得される複数のメッセージの順序は保証されます。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
+------+ +------+
|Client|----->|Server|
| |----->| |
| |----->| |
| |<-----| |
+------+ +------+
Bidirectional streaming RPC ( 双方向ストリーミング RPC )
クライアントはサーバに対して「ストリーム」を 1 つ送信します。
その結果、「ストリーム」を 1 つ受信します。
お互いに「ストリーム」を渡し合うということは、お互いに複数の「メッセージ」を送り合う事ができます。
送信と受信の間に順序はなく、それぞれが任意のタイミングで「メッセージ」を送り合います。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
+------+ +------+
|Client|----->|Server|
| |<-----| |
| |<-----| |
| |----->| |
| |<-----| |
+------+ +------+
例えば、サーバは
- クライアントから送信されるメッセージをすべて受け取るのを待ってから自分のメッセージを送信することができます
- あるいは、クライアントから送信されるメッセージ1つ1つを受け取ったタイミングで直ちに自分の返答メッセージを送り返すこともできます
- さらには、全く異なる方法を取ることもできます(メッセージ 2 つ受け取って、メッセージ 1 つ返す、など)
「ストリーム」から取得される複数のメッセージの順序は保証されます。
.proto
ファイルを使って API のソースコードを生成する
.proto
ファイルに「サービス」とサービスが持つ「メソッド」の定義ができたら、
gRPC で提供されている Protocol Buffers コンパイラツール ( protoc
) を使って、
"クライアントサイド"と"サーバサイド"のソースコードを生成します。
gRPC の利用者は基本的には"クライアントサイド"から API を呼び出すようにして、
"サーバサイド"に API のロジックを実装します。
- サーバサイドは...
- サーバプロセスを実行し、
.proto
ファイルで定義したサービスメソッドをクライアントに公開します。 - gRPC はリクエスト・メッセージをデコードし、サービスメソッドを実行し、サービスのレスポンスメッセージをエンコードして返却します。
- サーバプロセスを実行し、
- クライアントサイドは...
- 生成されたスタブコードを格納しておきます。スタブコードにはサービスと同じメソッドが実装されています。
- クライアントはスタブコード内のメソッドを呼び出すだけで、サーバとの通信が始まります。Protocol Buffers の難しい実装は隠蔽化されており、意識する必要はありません。
- リクエストメッセージをサーバに送信すると、gRPC により Protocol Buffers を使って通信が行われ、サーバのレスポンスメッセージを取得します。
同期的な RPC 、 非同期的な RPC のいずれにも対応している
同期的な RPC 呼び出しは、レスポンスがサーバから到達するまでブロックされます。
これは、RPC が目指す プロシージャ呼び出しの抽象化 に限りなく近い機能です。
一方、ネットワーク通信というものは本質的に 非同期 です。
さらに多くのケースで、現在のスレッドをブロックせずに RPC 呼び出しができると便利だったりします。(少々時間のかかる処理をリクエストだけして、後で様子を見に来る、など)
殆どの言語では、gRPC API は「同期的」と「非同期的」の両方の実装が可能です。
RPC のライフサイクル
ここからは、クライアントが gRPC サーバメソッドを呼び出したときに起こることをもう少し詳細に見ていきます。
プログラミングコードレベルの具体的な実装は、各言語ごとに異なるのでここでは取り上げません。
前述した 4 つのサービスメソッドごとにライフサイクルを見ていきましょう。
Unary RPC のライフサイクル
まずは、最もかんたんな RPC である Unary RPC について見ていきます。
前述の通り、クライントが「リクエスト」を 1 つ送り、「レスポンス」を 1 つ受け取るパターンです。
- クライアントがスタブメソッドを呼び出します。サーバはクライアントからのメソッド呼び出しを検知します。同時に、「メタデータ」「メソッド名」「(指定されていた場合は)デッドライン」の情報を受け取ります。 ( デッドラインについては後述 )
- サーバはすぐに初期メタデータを返却するか、クライアントのリクエストメッセージを待つかのいずれかの処理を行います。( 最初の挙動がどちらになるかはアプリケーションの仕様次第 )
- サーバはクライアントのリクエストメッセージを受け取ったら、応答のために必要なすべての処理を実行します。(処理に成功したら)クライアントに対して メッセージ(レスポンス) を送信します。同時に、「ステータスコード」「ステータスメッセージ(任意)」「メタデータ(任意)」といった レスポンスステータス も送信します。
- レスポンスステータス が正常だったら、クライアントは メッセージ を取得し、クライアントサイドの呼び出し処理は完了です。
Server streaming RPC のライフサイクル
こちらも前述の通り、クライントが「リクエスト」を 1 つ送り、「ストリーム」を 1 つ受け取るパターンです。
サーバは「ストリーム」にすべてのメッセージを送信します。
これが完了するとようやく、「ステータスコード」「ステータスメッセージ(任意)」「メタデータ(任意)」といった レスポンスステータス を送信します。
クライアントはすべてのメッセージを受信し、呼び出し処理は完了します。
Client streaming RPC のライフサイクル
またまた前述の通り、クライントが「ストリーム」を 1 つ送り、「メッセージ(レスポンス)」を 1 つ受け取るパターンです。
サーバがメッセージ(とステータスコード、ステータスメッセージ、メタデータ)を返却するのは、クライアントのストリームからすべてのメッセージを受け取ったあとである場合がほとんどです。
ただし、すべてのメッセージを受信する前にメッセージを返却することも可能です。
Bidirectional streaming RPC ( 双方向ストリーミング RPC ) のライフサイクル
クライアントの「メソッド呼び出し」とサーバの「メタデータ」「メソッド名」「デッドライン」の受信が一番初めに発生します。
次に、サーバはすぐに初期メタデータを返却するか、クライアントのストリームを待つかのいずれかの処理を行います。
クライアントサイド、サーバサイドのストリームがどう進行していくかはアプリケーションごとに定義できます。
2 つのストリーが完全に独立したものとすることもできますし、クライアントとサーバがストリーム内のメッセージを受け取り合いながら同期をとることもできます。
例えば、サーバがクライアントからのすべてのメセージ受信を待つこともできます。また、サーバとクライアントが「ピンポン」ゲームをするようにメッセージを順番に1つずつ送受信しあうこともできます。
RPC 呼び出し時にクライアント指定する「デッドライン」
クライアントは、RPC の実行を最大どれだけ待つかを指定できます。これを デッドライン といいます。 問題発生時には DEADLINE_EXEEDED
エラーが発生します。
RPC の終了
gRPC では、クライアントとサーバの双方は独立しています。
RPC 呼び出しが成功したかどうかはクライアント、サーバそれぞれで結論を出すという仕組みです。
そのため、双方で終了ステータスに不一致が起こる場合があります。
これにより、次のようなことが起こりえます。
- 例 1
- サーバサイドは「自分はすべてのレスポンスを送信し終わった!正常終了だ!」
- クライアントサイドは「デッドライン以降にレスポンスが到達した!異常終了だ!」
- 例 2
- サーバサイドは「現時点で受信したメッセージで十分。正常終了!」
- クライアントサイドは「まだストリームにすべてのメッセージを流しきってないのに中断された!異常終了!」
今までの Web アプリの感覚からするとあまり腑に落ちないですが、こういうことが起こり得る仕組みということです。
RPC のキャンセル
クライアント、サーバ双方ともに、実行中の RPC をキャンセルすることができます。
キャンセルにより RPC は直ちに終了されます。実行中の処理も停止されます。
キャンセル前に行われた変更はロールバックされません。
ディスカッション
コメント一覧
まだ、コメントがありません