状態:草案 (2026-10-17)、一部実測。 基準バージョン grpc/grpc v1.84.0 (ドキュメント)、grpc/grpc-go v1.83.2 (コミット 030ee8becb20)。 実測は2026-10-17 03時台 (KST) に小さなクラスターからPrometheusを 読み取る だけで取得しました (クエリは第5節)。 作成規則:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表記します。
目次
- 1. 直感:電話の代わりにフォーマット
- 2. 図解
- 3. なぜJSON over HTTP/1.1ではないのか
- 4. コード生成が提供するもの
- 5. 実測:小さなクラスターでのgRPC通信
- 6. チャネルの寿命:接続は長く、呼び出しは短く
- 7. 制限事項
- 8. 小さなクラスターとのつながり
- 参考資料
1. 直感:電話の代わりにフォーマット
隣のチームに仕事を頼むとき、電話で話せば早いですが誤解が生じます。そのため、会社では フォーマットを作成します。欄の名前と順序、どの欄が 数値であるかが決まっていれば、受け取る側はフォーマットを見るだけで処理でき、フォーマットが少し変わっても(欄が1つ追加されるなど)古いフォーマットを使っている人と引き続き仕事ができます。
gRPCはこのフォーマットを .protoファイルに記述し、そのフォーマットから 両方の言語のコードを自動的に生成します。呼び出す側は client.Hit(ctx, req) のように
普通の関数を呼び出し、その下でライブラリがリクエストをバイト列に変換してHTTP/2接続で送信し、レスポンスを再びオブジェクトに戻します。
Googleが社内で10年以上使っていたRPC基盤(Stubby)の次世代版を2015年3月にオープンソース化することになったのが始まりであり、CNCFには
2017-02-16にインキュベーティング段階として参加しました(grpc.io/about, CNCFプロジェクトページ).
| 部品 | 責任 | このシリーズでの |
|---|---|---|
| IDL(.proto) | サービス・メソッド・メッセージの契約。フィールド番号が下位互換性のカギ | 第2部 |
| コード生成 (protoc + プラグイン) | 言語別のメッセージ型、クライアントスタブ、サーバーインターフェース | 第1部 第4節 |
| チャネル (ClientConn) | 名前解決、ロードバランス、接続維持、リトライ、deadline | 第4・5・7部 |
| HTTP/2トランスポート | 1回の呼び出し = 1つのストリーム、5バイトのメッセージプレフィックス、トレーラーのステータスコード | 第3部 |
2. 図解
- 上の行は ビルド時 の処理です。
.protoをprotocと各言語のプラグインに渡すと、メッセージ型、スタブ、サーバーインターフェースが生成されます。 - 下の行は 実行時 の処理です。クライアントスタブはチャネルに対して「このメソッドパスで、このバイト列を送ってほしい」と伝えるだけです。チャネル内で 名前解決(resolver)がアドレスのリストを作成し、ロードバランスポリシーがサブチャネル(1つのアドレスへの接続)を選択し、そのサブチャネルのHTTP/2接続に 新しいストリームがオープンされます。
- 太い線1本が1つのTCP接続です。その上で複数の呼び出しが同時に流れます。これが第5部の「負荷が1つのポッドに偏る」問題の根源です。
3. なぜJSON over HTTP/1.1ではないのか
RESTとJSONでもリモート呼び出しは可能です。gRPCが存在する理由を、契約・バイト列・接続・障害の4つに分けて整理すると次のようになります。
| 軸 | 一般的なREST/JSON | gRPC | 代償 |
|---|---|---|---|
| 契約 | OpenAPIは任意であり、コードと乖離しやすい | .protoがコードの信頼できる唯一の情報源(SSOT) | ビルドにコード生成ステップが含まれる |
| バイト列 | フィールド名を毎回文字列で送信する | フィールド番号 + バイナリ値。第2部の例では21バイト vs JSONは72バイト | 人間が直接読めない (第8部のツールが必要) |
| 接続 | HTTP/1.1は接続ごとに1リクエスト | HTTP/2ストリームにより1つの接続で多数の呼び出し、双方向ストリーミングが可能 | L4ロードバランスが接続単位のため偏りが生じる (第5部) |
| 障害 | HTTPステータスとレスポンスボディの形式がサービスごとに異なる | 17種類のステータスコード、deadline伝播、リトライポリシーがライブラリ側に存在 | 適切なステータスコードを選択する責任はアプリケーションに残る (第3部) |
ブラウザのJavaScript APIではHTTP/2フレームやトレーラーを直接扱えないため、gRPCをそのまま使用することは困難です。そのため、gRPC-WebやHTTP/JSON変換 ゲートウェイといった別のブリッジが存在します (このシリーズでは扱いません)。
4. コード生成が提供するもの
契約書の例です。シリーズを通じてこのサービスを使用します (卓球のラリー1回を記録する仮想的なサービス)。
syntax = "proto3";
package tt.v1;
message Rally { int32 rally_id = 1; string server = 2; repeated int32 hits = 3; bool won = 4; sint32 delta = 5; }
message Score { int32 home = 1; int32 away = 2; }
service Scoreboard {
rpc Hit(Rally) returns (Score); // 단항
rpc Watch(Rally) returns (stream Score); // 서버 스트리밍
}
この契約における Hit のメソッドパスは /tt.v1.Scoreboard/Hit です。第3部のフレームの図もこのパスを使用します。
生成されたクライアントスタブが行うことは驚くほど少ないです。実際にprotocを実行せず、grpc-goのリポジトリに含まれる 実際に生成されたファイル (ヘルスチェックサービス)を見ると、1つのメソッドは次のようになっています。
const Health_Check_FullMethodName = "/grpc.health.v1.Health/Check"
func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthCheckResponse)
err := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
出典:grpc/grpc-go v1.83.2 health/grpc_health_v1/health_grpc.pb.go(protoc-gen-go-grpc v1.6.2により生成)、Apache-2.0
- スタブは メソッドパスの文字列とリクエスト・レスポンスオブジェクトをチャネルの
Invokeに渡すだけです。パスは/+ パッケージ.サービス +/+ メソッドであり、 この文字列がそのままHTTP/2の:pathになります (仕様書 PROTOCOL-HTTP2.md 付録A)。 - サーバー側の生成コードは
ServiceDescというテーブルです。サービス名の下に単項メソッドのリスト(Methods)とストリームのリスト(Streams、サーバー・クライアント ストリーミングであるかの表示)があり、各項目がハンドラー関数を指しています。サーバーは受け取った:pathでこのテーブルを検索してハンドラーを呼び出します (第7部)。 - 生成されるコードは言語ごとに異なりますが、ワイヤーフォーマットは同じです。これがGoのクライアントがC++のサーバーを呼び出すことができる理由です。
5. 実測:小さなクラスターでのgRPC通信
2026-10-17 03:00〜03:04 (KST)、Prometheusに次のクエリを送信しました (Kubernetes APIサーバーのサービスプロキシ経由のGET、読み取りのみ)。
count by (__name__, job) ({__name__=~"grpc_.*|.*_grpc_.*|rpc_.*"})
sum by (grpc_service, grpc_method, grpc_type) (grpc_client_started_total{job="apiserver"})
sum by (operation_type) (kubelet_runtime_operations_total)
メトリックに残ったgRPCサービス名をまとめると次のようになります (値の詳細については第6部)。
| 誰から → 誰へ | gRPCサービス (メトリックラベル) | 根拠 |
|---|---|---|
| kube-apiserver → etcd | etcdserverpb.KV·Watch·Lease·Maintenance |
apiserverの grpc_client_* メトリック |
| kubelet → containerd | runtime.v1.RuntimeService·ImageService |
kubelet_runtime_operations_total のoperation_type 21種 |
| アプリ → OTel Collector → Tempo | OTLP TraceService/Export |
Collectorの transport="grpc"、エクスポート先 :4317 |
| Tempo内部 | /tempopb.Pusher/PushBytesV2, /grpc.health.v1.Health/Check |
tempo_request_duration_seconds のrouteラベル |
| Argo CD UI・CLI → argocd-server | application.ApplicationService などの114個のメソッド |
grpc_server_started_total |
| サイドカー → istiod | istio.v1.auth.IstioCertificateService/CreateCertificate |
istiodの grpc_server_* |
| hubble-relay → ノードごとのHubble | observer.Observer·peer.Peer |
リレーログ、gRPCプローブ (第5・6部) |
名前が etcdserverpb.KV のように protoパッケージ.サービス の形式になっているのは、第4節のパスの規則がそのままメトリックラベルとして引き継がれているからです。
逆にkubeletはgRPCの共通メトリックを使用せず、 list_containers のように独自の名前でカウントしています。同じgRPCであっても、観測方式は実装によって異なります。
6. チャネルの寿命:接続は長く、呼び出しは短く
- チャネルは 作成時には接続しません。 grpc-goの
NewClientは アイドル(IDLE)状態で開始し、最初の呼び出し時に名前解決を行って接続します。呼び出しがない場合、30分後に再びアイドル状態に戻ります (デフォルト値dialoptions.go:729). - チャネルの状態には、IDLE、CONNECTING、READY、TRANSIENT_FAILURE、SHUTDOWNの5つがあります(connectivity-semantics-and-api.md).
- そのため、 チャネルは再利用するのが原則です。公式のパフォーマンスガイドでも、チャネルやスタブを再利用することを推奨しています(Performance Best Practices)。 呼び出しごとにチャネルを新しく作成すると、TCP、TLS、HTTP/2の設定ネゴシエーションを毎回行うことになります。第5部で見るとおり、kubeletのgRPCプローブはまさにそのように動作しています。
- デフォルトの名前解決もバージョンによって異なります。
NewClientはdns、古いDial·DialContextはpassthroughです (dialoptions.go:730,clientconn.goのコメント)。KubernetesのソースコードがDialからNewClientへ移行する際にpassthrough:///を付与する理由はこれです (第5部第6節)。
7. 制限事項
- 他の言語の実装 (C++コア、Java) の内部構造については触れていません。このシリーズでコードを読む対象はgrpc-goのみです。
- gRPC-Web、JSON変換ゲートウェイ、ALTSなどの認証方式については扱っていません。
- 第5部のリストは、Prometheusがスクレイピングしている対象のみをカウントしています。メトリックを出力しない、またはスクレイピングされないgRPC (例:CSIドライバーのUnixドメインソケット、kubeletプラグインの登録など) は 含まれていません (未測定)。
8. 小さなクラスターとのつながり
クラスターを毎日触っていながらgRPCを直接書いたことがない場合でも、実はすでに1日あたり何億回も使用しているという点から確認するとよいでしょう。 コントロールプレーンノードAのetcdは起動以降Rangeを約8,333万回処理しており、8台のkubeletはCRIのリスト取得だけでも合計約6,900万回呼び出していました (第6部)。 次の部では、その呼び出し一つひとつがどのようなバイト列として送信されているのかを見ていきます。
ティティの一言: フォーマットさえしっかり作っておけば、相手がGoだろうとC++だろうと関係ありません。欄の番号だけは変えないようにしてくださいね。番号こそが約束事ですから!
参考資料
すべての資料は2026-10-17に閲覧しました。
- grpc.io About, Performance Best Practices
- CNCFプロジェクトページ:gRPC
- grpc/grpc v1.84.0:
doc/PROTOCOL-HTTP2.md,doc/connectivity-semantics-and-api.md - grpc/grpc-go v1.83.2:
clientconn.go,dialoptions.go,health/grpc_health_v1/health_grpc.pb.go - 引用コード:grpc-go v1.83.2, Apache License 2.0.