TT Lab
ブログ

한국어 English 日本語

はじめる
ブログ

ブログ

KubernetesにおけるgRPC:1つのPodに偏る理由とヘルスチェック

ステータス:ドラフト(2026-10-17)、一部実測。 基準バージョン:grpc/grpc-go v1.83.2、kubernetes v1.34.10、cilium v1.20.1、grpc/grpc v1.84.0 ドキュメント。 実測は2026-10-17 03時台(KST)にPrometheus・kubectl get・Ciliumエージェント内の「 cilium-dbg status 」を読むことだけで取得しました。 負荷の偏りを直接再現したわけではありません(未実測)。再現するにはテスト用のgRPCサーバーのイメージとデプロイメントをクラスターに上げる必要がありますが、今回の作業は 読むだけにとどめました。必要な事項は第8節に記載しています。

目次

1. 直感:タクシー乗り場

Service(ClusterIP)はタクシー乗り場の配車係です。客(コネクション)が来ると空きタクシー(Pod)を1台割り当てます。HTTP/1.1クライアントは短距離を頻繁に 乗り降りするため、配車係が均等に割り振る機会が多くあります。gRPCクライアントは「 タクシー1台を終日貸し切り 」にします。そのタクシーの中で何千回 移動(呼び出し)しても、配車係が関与したのは最初の1回だけです。タクシーを10台に増やしても、すでに貸し切りにした客は最初のタクシーに乗り続けます。

2. 図

연결 단위 분산과 호출 단위 분산 ① ClusterIP(L4): 연결을 처음 열 때 한 번 고른다 클라이언트 채널 1개 서비스 IP eBPF/iptables 파드 1 · 호출 전부 파드 2 · 0 파드 3 · 0 TCP 하나가 오래 살면 새 파드가 늘어도 그 연결은 그대로다. ② 헤드리스 + 클라이언트 LB(round_robin) 클라이언트 채널 DNS: 파드 IP 3개 서브채널 3개 호출마다 고름 파드 1 · 1/3 파드 2 · 1/3 파드 3 · 1/3 새 파드는 DNS 를 다시 풀어야 보인다(grpc-go 는 연결이 끊길 때 등). ③ L7 프록시(메시·게이트웨이·xDS) 클라이언트 Envoy 등 HTTP/2 스트림 단위 파드 1 파드 2 파드 3 xDS 를 쓰면 프록시 없이 gRPC 라이브러리가 직접 설정을 받는다. 점선 상자: 놀고 있는 파드. 굵은 실선: 실제 호출이 흐르는 길.
図:コネクション単位の分散、ヘッドレスとクライアントLB、L7プロキシの比較

3. なぜ偏るのか

この問題について、Kubernetesブログは2018年に「gRPC Load Balancing on Kubernetes without Tears」という記事でまとめています(Buoyantの記事、 kubernetes.io/blog).

4. 3つの解決策

4.1 ヘッドレスサービス + クライアント側 round_robin

clusterIP: None であるヘッドレスサービスは、DNSでPodのIPをそのまま返します。クライアントが「 dns:///scoreboard.tt.svc.cluster.local:50051 」でチャネルを作成し、サービス設定で「 round_robin 」を選択すると、チャネルがPodごとにサブチャネルを作成し、「 呼び出しごと 」に切り替えながら送信します。

{"loadBalancingConfig": [{"round_robin": {}}]}

grpc-goでは「 grpc.WithDefaultServiceConfig(...) 」として指定するか、DNS TXTレコードから取得できます(Kubernetes DNSはTXTレコードを提供しないため、通常は前者の方法をとります)。

落とし穴が1つあります。「 いつDNSを再問い合わせするか」という点です。grpc-goのDNSリゾルバは一度成功すると定期的に再問い合わせせず、誰かが「 ResolveNow 」を呼び出すまで待機します(ただし直前のクエリから30秒以上経過している必要があります)。

var nextResolutionTime time.Time
if err == nil {
	// Success resolving, wait for the next ResolveNow. However, also wait 30
	// seconds at the very least to prevent constantly re-resolving.
	backoffIndex = 1
	nextResolutionTime = internal.TimeNowFunc().Add(MinResolutionInterval)
	select {
	case <-d.ctx.Done():
		return
	case <-d.rn:
	}
}

出典:grpc/grpc-go v1.83.2 internal/resolver/dns/dns_resolver.go 218〜229行抜粋(watcher)、Apache-2.0

ResolveNow は主に「 コネクションが切断されたり接続に失敗したりしたとき 」に呼び出されます(clientconn.go:1366·1515)。 そのため、Podが減ると(コネクションが切断されるため)すぐに気づきますが、 増えても誰も気づきません 。既存のPodに問題がなければコネクションが切断される理由がないためです。

4.2 サーバーの MaxConnectionAge による定期的な切り替え

解決策は、サーバーがコネクションに寿命を持たせることです。grpc-goサーバーの「 keepalive.ServerParameters.MaxConnectionAge 」を設定すると、その時間(±10%のジッター)が経過したコネクションに GOAWAY を送信し、「 MaxConnectionAgeGrace 」の経過後に切断します(keepalive.go のコメント)。デフォルト値はいずれも無限です。GOAWAYを受信したクライアントは進行中の呼び出しを完了させ、新しい呼び出しは新しいコネクションに送信します。また、コネクションが切断されたついでにDNSを再解決します。ClusterIPの後ろであっても、新しいコネクションを開くときに再度配車が行われるため、偏りが徐々に解消されます。ジッターが存在する理由は、すべてのクライアントが同じタイミングで集中して再接続するのを防ぐためです。

4.3 L7プロキシ:Envoy、メッシュ、Cilium

HTTP/2を理解するプロキシは、「 ストリーム(呼び出し)単位で バックエンドを選択できます。サイドカーメッシュ(Istio・Linkerd)やゲートウェイがこの処理を行いますが、クライアントコードを変更する必要はありません。その代償として、1ホップの追加とプロキシのCPU・メモリ消費が発生します。

Ciliumは、ノード上のEnvoyによりL7でgRPCを処理できます。gRPCメソッドが POST /패키지.서비스/메서드 という点を利用して、 パスによる許可リストを作成するネットワークポリシーが可能になり、拒否された呼び出しはパケットを破棄する代わりにPERMISSION_DENIEDステータスを返します(Cilium gRPCポリシーガイド).

rules:
  http:
  - method: "POST"
    path: "/tt.v1.Scoreboard/Hit"

小さなクラスターのエージェントは Proxy Status: OK … 0 redirects active でした(cilium-dbg status)。現在L7ポリシーが適用されている箇所がないため、gRPCをL7で検査するパスが使用されていないことを意味します。

4.4 xDS: プロキシレスメッシュ

gRPCライブラリが 直接xDS(Envoyの設定API)と通信する 方式です(gRFC A27)。クライアントは xds:///서비스:포트 でチャネルを作成し、コントロールプレーン(例:istiod)からリスナー・ルート・クラスター・エンドポイントを受け取って呼び出し単位で分散します。Istioはこれを「proxyless gRPC」として提供し、エージェントがブートストラップファイルを書き込んでxDSを代行中継します(Istioブログ 2021).

注意すべき点は、gRPCがxDSの 一部のみ を実装しているという点です。サポートされていない設定はログも出さずに無視され、サポートされているフィールドにサポートされていない値が指定された場合は設定全体が拒否(NACK)されます(ズgrpc_xds_features.md)。Envoyサイドカーで機能していた設定が、プロキシレスgRPCでは何のエラーもなく効かなくなることがあります。

小さなクラスターのistiodは、3つのEnvoyプロキシとxDSで接続されており(pilot_xds 3)、起動以降のプッシュ回数はEDS 44,798回、RDS 21,150回、CDS・LDSが各21,102回でした(sum by (type) (pilot_xds_pushes))。エンドポイント(EDS)が最も頻繁に変更されるのは、Podが頻繁に出入りするためです。プロキシレスgRPCクライアントは存在しませんでした。

4.5 選択基準

方式 クライアントの変更 新Podへの反映 追加コスト 適したケース
ClusterIPのまま なし 新規接続から なし クライアントが多く、それぞれの呼び出しが少ないとき
+ サーバーのMaxConnectionAge サーバー設定 接続の寿命ごと 再接続のコスト 上記状況で偏りが確認されるとき
ヘッドレス + round_robin サービス設定 接続が切れたとき(+寿命) なし クライアントとサーバーの両方を自社で開発する場合
L7プロキシ・メッシュ なし 即時(xDS) ホップ、プロキシのリソース 言語が混在し、ポリシーやオブザーバビリティが必要なとき
プロキシレスxDS ターゲット文字列、ブートストラップ 即時 コントロールプレーン ホップを減らし、機能の制限を受け入れるとき

5. ヘルスチェック: grpc.health.v1

gRPCには標準のヘルスサービスがあります(health-checking.md).

6. kubeletのgRPCプローブ

Kubernetesは1.27から grpc プローブが安定版となっています。ポートとオプションのサービス名を指定すると、kubeletが grpc.health.v1.Health/Check を呼び出します。ドキュメントに記載されている制限は以下の通りです(プローブ設定ドキュメント):名前付きポートは使用できず、デフォルトは平文接続であり、TLSは最新のドキュメントにおいて GRPCContainerProbeTLS 機能ゲートとして追加されています(1.34にはなく、証明書は検証されません)。

kubelet 1.34.10の実装はシンプルです。

opts := []grpc.DialOption{
	grpc.WithUserAgent(fmt.Sprintf("kube-probe/%s.%s", v.Major, v.Minor)),
	grpc.WithBlock(),
	grpc.WithTransportCredentials(insecure.NewCredentials()), //credentials are currently not supported
	...
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := grpc.DialContext(ctx, addr, opts...)
...
resp, err := client.Check(metadata.NewOutgoingContext(ctx, make(metadata.MD)), &grpchealth.HealthCheckRequest{
	Service: service,
})

出典: kubernetes v1.34.10 pkg/probe/grpc/grpc.go の56〜90行目を抜粋(Probe)、Apache-2.0

6.1 実測:誰がgRPCプローブを使っているか

kubectl get pods -o json のプローブの種類を集計しました(ユーザー業務のネームスペース1つは除外、Pod 271個)。

プローブの種類 数
httpGet 368
exec 38
tcpSocket 20
grpc 3

gRPCプローブ3つはすべてhubble-relayのLiveness・Startup・Readinessプローブです(ポート4222、サービス名空文字、周期はLiveness・Readiness 10秒、Startup 3秒)。リレーのログにも起動時に4222で「gRPC health server」を開くことが記録されます。結果の指標は次の通りでした。

sum by (probe_type, result) (prober_probe_total{namespace="kube-system", container="hubble-relay"})
プローブ 成功 失敗
Liveness 32,161 0
Readiness 32,162 0
Startup 1 8

7. 制限事項

8. 小規模クラスターとの関連

ティーの一言。 タクシーを終日チャーターすると、配車係がどんなに賢くても意味がありません。たまには降りて(MaxConnectionAge)並び直してこそ、新しいタクシーにも乗れますよ!

参考

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

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

コメント

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

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