ステータス: ドラフト(2026-10-17)、コードリーディング。 基準版 grpc/grpc-go v1.83.2(コミット 030ee8becb20、ローカルの Go モジュールキャッシュから読み込み)。最新リリースは v1.84.0 のため、行番号が 少し異なる場合があります。本文中の
파일:줄は v1.83.2 の関数宣言の行番号です。この編での新しい実測はなく、第6編の数値との対照のみを行います。
目次
- 1. 一行要約: 会社の組織図
- 2. 構造図
- 3. コードマップ
- 4. コアパスを辿る
- 5. 第6編の実測値との対照
- 6. どこから読むべきか
- 7. 初めてのコントリビューションにおすすめの場所
- 8. 制限事項
- 参考
1. 一行要約: 会社の組織図
チャネルは チームリーダーです。仕事(呼び出し)を受け取ると自ら行わず、アドレス帳担当(リゾルバ)から渡された名簿を割り当て担当(バランサー)に渡し、割り当て担当が従業員(サブチャネル)を準備しておき、仕事が来るたびにその日の割り当て表(ピッカー)から1人を選びます。従業員は自身の電話回線(HTTP/2 接続)を1つ持っています。割り当て表は丸ごと差し替える紙であるため、仕事を選ぶ瞬間は誰ともロックを競合させません。
2. 構造図
3. コードマップ
| パス | 内容 | 最初に読む場所 |
|---|---|---|
clientconn.go |
チャネル、サブチャネル(addrConn)、アイドルモード、リトライ制御 |
NewClient(183), addrConn(1240) |
call.go, stream.go |
単項・ストリーム呼び出し、リトライ試行(csAttempt) |
invoke(65), newClientStream(213) |
resolver_wrapper.go, resolver/, internal/resolver/ |
リゾルバのラップと dns・passthrough・unix の実装 | ccResolverWrapper.UpdateState(129) |
balancer_wrapper.go, balancer/ |
バランサーのラップ、pick_first・round_robin・ringhash・weightedroundrobin など | ccBalancerWrapper.NewSubConn(180) |
picker_wrapper.go |
呼び出しごとのサブチャネル選択、待機 | pickerWrapper.pick(105) |
internal/transport/ |
HTTP/2 クライアント・サーバー、フロー制御、keepalive、コントロールバッファ | http2_client.go, http2_server.go |
rpc_util.go |
5バイトのプレフィックス、圧縮、受信上限、コールオプション | msgHeader(879), parser.recvMsg(785) |
server.go |
サーバー、サービス登録、ディスパッチ | Serve(888), handleStream(1800) |
codes/, status/ |
ステータスコードとステータスオブジェクト | codes.go |
health/, reflection/, channelz/, admin/ |
付加サービス(第5・第8編) | health/server.go |
xds/, internal/xds/ |
xDS リゾルバ・バランサー(第5編) | 規模が大きいため後回し |
4. コアパスを辿る
4.1 チャネルの作成: まだ何も接続しない
NewClient はオプションを適用し、ターゲット文字列を解釈してリゾルバビルダーを選択した後(initParsedTargetAndResolverBuilder、デフォルトスキーム dns)、アイドル状態を準備して終了します。アイドル状態の準備(initIdleStateLocked)がリゾルバのラップ(newCCResolverWrapper)とバランサーのラップ(newCCBalancerWrapper)を作成しますが、リゾルバはまだ動作しません。最初の呼び出しが来ると exitIdleMode がチャネルを CONNECTING に変更し、リゾルバを開始します。30分間呼び出しがない場合、 enterIdleMode がリゾルバ・バランサーおよび接続を破棄し、再びアイドル状態に戻ります。
4.2 アドレスの到着時: リゾルバ → バランサー → サブチャネル → ピッカー
- リゾルバがアドレスを見つけると
ccResolverWrapper.UpdateStateを呼び出し、チャネルのupdateResolverStateAndUnlockがサービス設定を適用した後に(applyServiceConfigAndBalancer)バランサーに渡します(updateClientConnState). - )。バランサー(例: round_robin)はアドレスごとに
NewSubConnを呼び出し、これがnewAddrConnLockedを通じてaddrConnを作成します。このaddrConnがサブチャネルの実体です。 addrConnはresetTransportAndUnlock→tryAllAddrs→createTransportの順序で接続し、失敗した場合はバックオフの後に再試行します。失敗するか接続が切断された場合は、リゾルバにResolveNowを要求します(第5編 4.1節)。- サブチャネルの状態が変わるたびに、バランサーは新しいピッカーを作成して
ccBalancerWrapper.UpdateStateに渡し、pickerWrapper.updatePickerがピッカーを アトミックに差し替えます。待機していた呼び出しは、古いピッカーに紐づくチャネルが閉じられることで目を覚まします。
なぜこのように分割したのでしょうか。アドレスを見つける作業(DNS・xDS・Kubernetes API など)と、選択する作業(最初・ラウンドロビン・重み付き・ハッシュ)を互いに認識させないことで、 任意のリゾルバと任意のバランサーを組み合わせられるようにするためです。バランサー側の更新は直列エグゼキューター(grpcsync.CallbackSerializer, balancer_wrapper.go のコメント)によって順次処理され、呼び出しが通過するホットパス(pick)ではアトミックな値を1つ読み取るだけにするため、2つの世界が互いのロックを待たなくて済むようになっています。
4.3 単一の呼び出し: Invoke からストリームまで
- 生成されたスタブが
ClientConn.Invokeを呼び出し、インターセプターを経由してinvokeがnewClientStream→newClientStreamWithParamsへ向かいます。 単項呼び出しも内部的には「メッセージを1つ送信し、1つ受信するストリーム」です。 - 試行を1つ作成し(
newAttemptLocked),getTransportがピッカーからサブチャネルを選びます。選択されたサブチャネルのトランスポートに対してcsAttempt.newStream→http2Client.NewStreamを用いて HEADERS を送信します(第3編のcreateHeaderFields). SendMsgがシリアライズ・圧縮・5バイトのプレフィックスを付与してcsAttempt.sendMsgに書き込み、RecvMsg→recv→parser.recvMsgで応答を読み取ります。失敗した場合、withRetryが第4編のshouldRetryを参照して新しい試行を作成し、バッファに蓄積された送信済みメッセージを再送します。
4.4 ピッカー: 待つか、即座に失敗するか
呼び出しが来たものの READY なサブチャネルが存在しない場合、どうなるでしょうか。 pickerWrapper.pick の分岐がその答えとなります。
pickResult, err := p.Pick(info)
if err != nil {
if err == balancer.ErrNoSubConnAvailable {
continue
}
if st, ok := status.FromError(err); ok {
...
return pick{}, dropError{error: err}
}
// For all other errors, wait for ready RPCs should block and other
// RPCs should fail with unavailable.
if !failfast {
lastPickErr = err
continue
}
return pick{}, status.Error(codes.Unavailable, err.Error())
}
出典: grpc/grpc-go v1.83.2 picker_wrapper.go 157〜176行抜粋、Apache-2.0
- まだ接続中であれば(
ErrNoSubConnAvailable) 新しいピッカーが来るまで 待機します。これが接続中のチャネルで呼び出しが失敗しない理由です。 - すべてのサブチャネルが TRANSIENT_FAILURE の場合、ピッカーはエラーを返し、デフォルト(fail fast)は 即座に UNAVAILABLE です。
grpc.WaitForReady(true)を指定した呼び出しのみ deadline まで待機します (wait-for-ready.md の定義の通りです)。 - ステータスエラーを返すと(ドロップ)、リトライもしません。制御プレーンが「この呼び出しは破棄せよ」と言うときに使う経路です。
運用の現場において、この違いは次のように現れます。サーバーを再起動する数秒間、fail fast クライアントは UNAVAILABLE を乱発し(第6篇 Collector の 10〜13の記録のように)、 wait-for-ready クライアントは静かに待機し、deadline 内にサーバーが復旧すれば成功します。
4.5 サーバー: Serve からハンドラーまで
Server.ServeがAcceptするたびにhandleRawConnをゴルーチンとして起動します。- 資格情報のハンドシェイクの後、
NewServerTransportが HTTP/2 の設定を交換し、serveStreamsがフレームを読み取ります。同時ストリーム数のデフォルト値は無制限(math.MaxUint32,server.go:189)です。 - HEADERS が来ると、
operateHeadersが:path,content-type,grpc-timeoutを読み取って ストリームを作成し(不正な content-type は 415)、handleStreamがパスをサービス名とメソッド名に分割してRegisterServiceに登録されたテーブル(第1篇のServiceDesc)から探します。なければ UNIMPLEMENTED。 - 単項であれば
processUnaryRPC、ストリームであればprocessStreamingRPCが メッセージを読み込み、インターセプターとハンドラーを呼び出した後、writeStatusでトレーラーを書き込みます。 GracefulStopは GOAWAY を送信して新しいストリームをブロックし、進行中の処理が終了するのを待ちます。第5篇の MaxConnectionAge も同じ GOAWAY を使用します。
5. 第6篇の実測との対照
| 第6篇で測定したもの | この図において |
|---|---|
| apiserver → etcd TCP 247個 | etcd クライアント 247個 = ClientConn 247個、それぞれ READY な addrConn が1つ(対象が 127.0.0.1 のみであるため) |
| ノード A のオープンな Watch ストリーム 242 | 各 ClientConn の http2Client 上の長期生存ストリーム1つ |
| kubelet → containerd チャネル 2個 | ランタイム・イメージ ClientConn の2つ、 passthrough ではない Unix ドメインソケット |
| hubble-relay のノード8個への接続 | ノードごとに ClientConn(リレーのピアプールが管理、推測) |
| kubelet gRPC プローブ | プローブごとに ClientConn を作成・終了(第5篇第6節) |
同じ grpc-go であっても、 チャネルをいくつ作成しどのように使うかは利用側のコードが決定し、その選択が接続数・メモリ・負荷分散を決定づけます。
6. どこから読むべきか
Documentation/のkeepalive.md·concurrency.md·anti-patterns.mdを最初に読みます。anti-patterns.mdは、かつてのDial·WithBlockをなぜ避けるべきなのかを説明しています。call.go→stream.goのnewClientStreamWithParams·csAttemptで1つの呼び出しを追跡します。picker_wrapper.go→balancer_wrapper.go→balancer/roundrobinで選択する側を、resolver_wrapper.go→internal/resolver/dnsで探索する側を確認します。internal/transport/http2_client.goのNewStream·reader·keepalive,controlbuf.goの書き込みループに降ります。- サーバーは
server.goのhandleStream→processUnaryRPCを読むだけでもディスパッチの構造が見えてきます。
7. 最初に貢献しやすい場所
- grpc-go では、初めて貢献する人に向けて担当者のいない「Status: Help Wanted」イシューをお勧めしています(2026年10月17日時点でオープンなもの11個、CONTRIBUTING)。動作や API を変更する作業は 事前にイシューで合意をとり、複数の言語で同時に変更が必要な機能については gRFC から始めます。
- 生成 AI ポリシーが存在します。リポジトリ内のすべてのコミュニケーション(イシュー・PR の本文・コメント)は人間が直接記述する必要があり(翻訳や文章校正の補助は例外)、自動エージェントが GitHub に直接投稿しては ならず、AI が生成したコードは提出者が完全に理解し責任を負う必要があります(master の CONTRIBUTING 「Generative AI Policy」)。
- 読み進める中で見つかった TODO の3つ:
stream.go:634の hedging、clientconn.go1364行目の「再解釈リクエストを pick_first 内に移動する」(#7534)、createHeaderFieldsのヘッダー数事前カウントベンチマーク。 3つとも設計の議論が必要であり、最初の PR にするには大きいですが、イシューを読むための入り口としては最適です。 - ドキュメント側の候補は第8篇第7節にまとめています。
8. 制限
- xDS(
xds/·internal/xds/)や ringhash・weightedroundrobin・RLS のような高度なバラランサーについては読んでいません。 controlbuf.goの書き込みスケジューリング、メモリプール(mem/)、統計ハンドラー(OpenTelemetry 連携)については取り上げていません。- 行番号は v1.83.2 を基準としています。v1.84.0 との違いは確認していません(未確認)。
ティティの一言。 割り当てテーブル(ピッカー)をごっそり丸ごと差し替えるので、お客さんが殺到しても担当者と列のことで揉める心配はありません。その代わり、スタッフ全員が倒れたときに待つか(WaitForReady)追い返すか(fail fast)はお客さんが決めます!
参考
すべての資料は 2026年10月17日に閲覧しました。
- grpc/grpc-go v1.83.2:
clientconn.go,call.go,stream.go,picker_wrapper.go,balancer_wrapper.go,resolver_wrapper.go,server.go,internal/transport/http2_client.go,internal/transport/http2_server.go,rpc_util.go,Documentation/, CONTRIBUTING.md(master) - grpc/grpc v1.84.0:
doc/wait-for-ready.md,doc/load-balancing.md - 引用コード: grpc-go v1.83.2, Apache License 2.0。