Tonicによるrust gRPCサーバー開発

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をアドバタイズすることで足りると考えられます。

⁋ 2021/05/17↻ 2026/10/13