Tonic
を用いてgRPC開発する際、サーバープロセスの構築以外に、protobufを適切に組込むことが必要になります。
Tonicの場合は、
PROST!
を利用しており、protocコマンドで生成するフローとやや異なります。
なお、本記事のロジックと同等のコード生成CLIツール、 Alain をオープンソースで配布しています。
protoインクルードの構造
gRPC開発では、protobufの定義と各言語の変数との対応を意識することが重要です。
以下のようなprotobufのコードを組込む例を考えます。
syntax = "proto3";
package example.duck;
service ExampleDuck {
rpc LameDuck(Dummy) returns (Dummy) {}
}
message Dummy {}
まず、マクロを用いてprotoをインクルードします。
pub mod example {
pub mod duck {
tonic::include_proto!("example.duck");
}
}
この際、modの階層をprotobufのpackage名前空間に揃えて定義することが重要です。
構造を間違えている場合、
superがネストし過ぎる
コードが出力されるといった非常に分かりにくいエラーに直面します。
proto定義の参照
インクルードしたproto定義は、mod構造に沿ってrustプロジェクトの名前空間にアサインされます。
先ほどの例では、crate::example::duck::Dummyのような名前でインポートされます。
なお、rpcはcrate::example::duck::lame_duck()のようにスネークケースになります。
この他、サーバーなどの構造体もツールの命名規約に沿って自動生成されています。target/ディレクトリに自動生成されたファイルがあり、個別のケースはコードを確認することになります。
- example::duck::LameDuckServer
- example::duck::LameDuckClient
- example::duck::LameDuckService
サーバー実装では、protoに定義したrpcをすべて実装しないとコンパイル時エラーになります。
RequestとResponseの構造
Requestは、tonic::Request<example::duck::Dummy>のようにprotobufメッセージをtonic::Requestでラップしたものです。
メッセージはinto_inner()で取り出せます。
またResponseは、Result<tonic::Response<example::duck::Dummy, tonic::Status>のように、tonic::Responseまたはtonic::Statusを返すResult型です。
ビルドスクリプト
tonicは、cargo build実行時にprotobufをrustコードにコンパイルするフローを想定しています。
そのため、以下のようなbuild.rsをプロジェクトルートに置きます。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/example_duck.proto")?;
Ok(())
}
なお、protobufコンパイルにはtonic-buildクレートが必要であるため、Cargo.tomlのbuild-dependenciesに追加しておきます。
[build-dependencies]
tonic-build = { version = "0.4", features = ["prost"] }
複数のserviceを提供するServer
gRPCではserviceがrpcをまとめる単位となり、proto内に複数のserviceを定義できます。
tonicはserviceを同名のトレイトに対応させています。
各トレイトは分割実装できないためservice内のrpcの数が増えると、長大なトレイト定義をimplすることになります。
serviceを論理的に分割できる場合には、各トレイト実装を別のファイルに定義できます。
各サービスはエントリポイントのServer構築プロセスで、
add_service()
を用いて追加します。
add_service()メソッドは単一のサーバーに複数指定できるため、単一ポートに複数のサービスを統合できます。
gRPC-Web over HTTP/3
gRPC-WebはgRPCサービスをブラウザから利用するためのプロキシプロトコルで、ブラウザが対応する素朴なhttpリクエストをベースに構築されています。
ネイティブgRPCはHTTP/2を前提としており、 HTTP/3については議論中 です。
gRPC-WebのHTTP/3対応は自明ではありません。ブラウザ対応の目的から、もとよりHTTP/2に限らずHTTP/1.1上でも動作しています。
gRPC-Webはプロトコルにアップデートがなく、規格策定が先行する様子もありません。
どうやらgRPC-WebはHTTP/3上で動作する
現実には、フロントエンドプロキシがHTTP/3を提供する場合、gRPC-WebでもHTTP/3上で動作する挙動になります。
たとえば
Googleのロードバランサ
のうちアプリケーションロードバランサはHTTP/3を提供しており、その背後でgRPC-Webサービスを提供するとHTTP/3で接続しています。
個別の接続でHTTPのどのバージョンを採用するかはブラウザしだいです。gRPC-WebのクライアントライブラリはfetchAPIを用いて実装しているケースが多いと推測しますが、Chromeの場合fetch()にも積極的にHTTP/3を利用するようです。
HTTP/3はQuic/UDPで接続するため、WANプロトコルとしては性能向上を期待できます。ただしこのブラウザ挙動は、gRPC-Webの個別サービスがHTTPバージョンを制御する手段がないということでもあります。
運用中のproductionクラスタは意図せずgRPC-Web orver HTTP/3構成になってしまいましたが、サーバサイド・ストリーミングを含めて不具合はまったく起きていません。
実構成を再現するテストスイートも長期の実績があり、結果的に安定動作していると言える状況です。
gRPC-Web over HTTP/3の構成
GCPロードバランサの機能により自動的にgRPC-WebがHTTP/3に対応しているので、必要な構成は検証していません。しかし、ロードバランサはプロトコル変換とわずかなHTTPヘッダーの操作にとどまります。
gRPC-Webのプロキシにはenvoyを採用することが多いでしょう。構成済みのUpstream設定にくわえて、HTTP/3のDownstream設定を定義することによりgRPC-WebもHTTP/3上で配信する挙動になると推測しています。
具体的には、envoyのUDPポートDownstreamにHTTP/3を設定し、HTTP/2のTCPポートでalt-svcをアドバタイズすることで足りると考えられます。