TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

grpc-go コードマップ: ClientConn から http2Client まで、Serve からハンドラーまで

ステータス: ドラフト(2026-10-17)、コードリーディング。 基準版 grpc/grpc-go v1.83.2(コミット 030ee8becb20、ローカルの Go モジュールキャッシュから読み込み)。最新リリースは v1.84.0 のため、行番号が 少し異なる場合があります。本文中の 파일:줄 は v1.83.2 の関数宣言の行番号です。この編での新しい実測はなく、第6編の数値との対照のみを行います。

目次

1. 一行要約: 会社の組織図

チャネルは チームリーダーです。仕事(呼び出し)を受け取ると自ら行わず、アドレス帳担当(リゾルバ)から渡された名簿を割り当て担当(バランサー)に渡し、割り当て担当が従業員(サブチャネル)を準備しておき、仕事が来るたびにその日の割り当て表(ピッカー)から1人を選びます。従業員は自身の電話回線(HTTP/2 接続)を1つ持っています。割り当て表は丸ごと差し替える紙であるため、仕事を選ぶ瞬間は誰ともロックを競合させません。

2. 構造図

grpc-go 1.83 의 클라이언트·서버 계층 클라이언트 ClientConn clientconn.go · NewClient ccResolverWrapper → resolver resolver_wrapper.go · dns/passthrough/xds ccBalancerWrapper → balancer balancer_wrapper.go · pick_first/round_robin pickerWrapper.pick picker_wrapper.go · 호출마다 서브채널 선택 addrConn (SubConn) clientconn.go · 연결·재연결·백오프 http2Client internal/transport/http2_client.go 호출 경로: Invoke → newClientStream → pick → NewStream 서버 Server.Serve → handleRawConn server.go · 연결마다 고루틴 http2Server.operateHeaders http2_server.go · :path 해석, 스트림 생성 handleStream → processUnaryRPC server.go · 서비스 맵에서 핸들러 찾기 공용: rpc_util.go(5바이트 접두), codes·status, keepalive
図: grpc-go 1.83 クライアント・サーバーのレイヤーとファイル

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 アドレスの到着時: リゾルバ → バランサー → サブチャネル → ピッカー

  1. リゾルバがアドレスを見つけると ccResolverWrapper.UpdateState を呼び出し、チャネルの updateResolverStateAndUnlock がサービス設定を適用した後に(applyServiceConfigAndBalancer)バランサーに渡します(updateClientConnState).
  2. )。バランサー(例: round_robin)はアドレスごとに NewSubConn を呼び出し、これが newAddrConnLocked を通じて addrConn を作成します。この addrConn がサブチャネルの実体です。
  3. addrConn は resetTransportAndUnlock → tryAllAddrs → createTransport の順序で接続し、失敗した場合はバックオフの後に再試行します。失敗するか接続が切断された場合は、リゾルバに ResolveNow を要求します(第5編 4.1節)。
  4. サブチャネルの状態が変わるたびに、バランサーは新しいピッカーを作成して ccBalancerWrapper.UpdateState に渡し、 pickerWrapper.updatePicker がピッカーを アトミックに差し替えます。待機していた呼び出しは、古いピッカーに紐づくチャネルが閉じられることで目を覚まします。

なぜこのように分割したのでしょうか。アドレスを見つける作業(DNS・xDS・Kubernetes API など)と、選択する作業(最初・ラウンドロビン・重み付き・ハッシュ)を互いに認識させないことで、 任意のリゾルバと任意のバランサーを組み合わせられるようにするためです。バランサー側の更新は直列エグゼキューター(grpcsync.CallbackSerializer, balancer_wrapper.go のコメント)によって順次処理され、呼び出しが通過するホットパス(pick)ではアトミックな値を1つ読み取るだけにするため、2つの世界が互いのロックを待たなくて済むようになっています。

4.3 単一の呼び出し: Invoke からストリームまで

  1. 生成されたスタブが ClientConn.Invoke を呼び出し、インターセプターを経由して invoke が newClientStream → newClientStreamWithParams へ向かいます。 単項呼び出しも内部的には「メッセージを1つ送信し、1つ受信するストリーム」です。
  2. 試行を1つ作成し(newAttemptLocked), getTransport がピッカーからサブチャネルを選びます。選択されたサブチャネルのトランスポートに対して csAttempt.newStream → http2Client.NewStream を用いて HEADERS を送信します(第3編の createHeaderFields).
  3. 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

運用の現場において、この違いは次のように現れます。サーバーを再起動する数秒間、fail fast クライアントは UNAVAILABLE を乱発し(第6篇 Collector の 10〜13の記録のように)、 wait-for-ready クライアントは静かに待機し、deadline 内にサーバーが復旧すれば成功します。

4.5 サーバー: Serve からハンドラーまで

  1. Server.Serve が Accept するたびに handleRawConn をゴルーチンとして起動します。
  2. 資格情報のハンドシェイクの後、 NewServerTransport が HTTP/2 の設定を交換し、 serveStreams がフレームを読み取ります。同時ストリーム数のデフォルト値は無制限(math.MaxUint32, server.go:189)です。
  3. HEADERS が来ると、 operateHeaders が :path, content-type, grpc-timeout を読み取って ストリームを作成し(不正な content-type は 415)、 handleStream がパスをサービス名とメソッド名に分割して RegisterService に登録されたテーブル(第1篇の ServiceDesc)から探します。なければ UNIMPLEMENTED。
  4. 単項であれば processUnaryRPC、ストリームであれば processStreamingRPC が メッセージを読み込み、インターセプターとハンドラーを呼び出した後、 writeStatus でトレーラーを書き込みます。
  5. 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. どこから読むべきか

  1. Documentation/ の keepalive.md·concurrency.md·anti-patterns.md を最初に読みます。 anti-patterns.md は、かつての Dial·WithBlock をなぜ避けるべきなのかを説明しています。
  2. call.go → stream.go の newClientStreamWithParams·csAttempt で1つの呼び出しを追跡します。
  3. picker_wrapper.go → balancer_wrapper.go → balancer/roundrobin で選択する側を、 resolver_wrapper.go → internal/resolver/dns で探索する側を確認します。
  4. internal/transport/http2_client.go の NewStream·reader·keepalive, controlbuf.go の書き込みループに降ります。
  5. サーバーは server.go の handleStream → processUnaryRPC を読むだけでもディスパッチの構造が見えてきます。

7. 最初に貢献しやすい場所

8. 制限

ティティの一言。 割り当てテーブル(ピッカー)をごっそり丸ごと差し替えるので、お客さんが殺到しても担当者と列のことで揉める心配はありません。その代わり、スタッフ全員が倒れたときに待つか(WaitForReady)追い返すか(fail fast)はお客さんが決めます!

参考

すべての資料は 2026年10月17日に閲覧しました。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます