TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

Kubernetes 中的 gRPC:为什么流量会涌向单一 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 服务器镜像和 Deployment,但本次工作决定 仅进行读取。所需内容已写在第 8 节中。

目录

1. 直观理解:出租车候客区

Service(ClusterIP)就像是出租车候客区的调度员。当有乘客(连接)到来时,它会指定一辆空出租车(Pod)。HTTP/1.1 客户端由于短途出行频繁, 上下车次数多,调度员有充足的机会进行均等分配。而 gRPC 客户端则是 包下一辆出租车整整一天 。即便在车内进行数千次 出行(调用),调度员也只在最初参与了一次。即使将出租车增加到十辆,已经包车的乘客仍会继续乘坐第一辆出租车。

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. 三种解决方法

4.1 无头服务 + 客户端侧 round_robin

clusterIP: None 作为 dns:///scoreboard.tt.svc.cluster.local:50051 的无头服务通过 DNS 原样返回 Pod 的 IP。如果客户端使用 round_robin 创建通道并在服务配置中选择 ,通道将为每个 Pod 创建子通道,并 按每次调用

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

轮流发送。 grpc.WithDefaultServiceConfig(...) 在 grpc-go 中,可以通过

传入或通过 DNS TXT 记录接收(由于 Kubernetes DNS 不提供 TXT,通常使用前一种方法)。 这里有一个陷阱:什么时候重新查询 DNS ResolveNow 。grpc-go 的 DNS 解析器一旦成功就不会定期重新查询,而是会一直等到有人调用

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:
	}
}

(尽管距离上次查询必须过去至少 30 秒)。 来源: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 增加则无人知晓。 因为只要现有 Pod 运转正常,连接就不会断开。

4.2 通过服务器的 MaxConnectionAge 周期性轮换

解决方案是让服务器为连接设定寿命。如果设定了 grpc-go 服务器的 keepalive.ServerParameters.MaxConnectionAge ,则会向超过该时间(±10% 的随机抖动)的连接发送 GOAWAY,并在 MaxConnectionAgeGrace 后关闭(keepalive.go 的注释)。两者的默认值都是无限。收到 GOAWAY 的客户端会完成正在进行的调用,并将新调用发送到新连接,同时趁着连接断开的机会重新解析 DNS。即使在 ClusterIP 背后,打开新连接时也会重新进行调度,从而逐渐缓解流量倾斜。之所以存在抖动,是为了防止所有客户端在同一瞬间涌入并重新连接。

4.3 L7 代理:Envoy、Mesh、Cilium

能够理解 HTTP/2 的代理可以 按流(调用)级别 选择后端。Sidecar Mesh(Istio·Linkerd)或网关可以完成此工作,且无需修改客户端代码。代价增加了一次网络跳数(Hop)以及代理的 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 策略的地方,因此不会使用在 L7 查看 gRPC 的路径。

4.4 xDS:无代理的 Mesh

这是 gRPC 库 直接使用 xDS(Envoy 的配置 API) 的方式(gRFC A27)。客户端使用 xds:///서비스:포트 创建通道,从控制平面(例如 istiod)接收监听器、路由、集群和端点,并在调用级别进行分发。Istio 将其作为“proxyless gRPC”提供,由代理编写引导文件并代为转发 xDS(Istio 博客 2021).

)。 需要注意的是,gRPC 只实现了 xDS 的 一部分 。不支持的配置会被静默忽略,如果支持的字段中出现了不支持的值,则会拒绝整个配置(NACK)(grpc_xds_features.md)。在 Envoy Sidecar 中生效的配置在无代理 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 代理·Mesh 无 即时(xDS) 网络跳数、代理资源 语言混杂且需要策略与可观测性时
无代理 xDS 目标字符串、引导程序 即时 控制平面 想要减少网络跳数并能承受功能限制时

5. 健康检查:grpc.health.v1

gRPC 拥有标准的健康检查服务(health-checking.md).

6. kubelet 的 gRPC 探针

Kubernetes 从 1.27 版本开始将 grpc 探针作为稳定版。只要提供端口和可选的服务名称,kubelet 就会调用 grpc.health.v1.Health/Check 。 文档中提及的限制如下(探针配置文档):不能使用具名端口(Named Port),默认是明文连接,最新文档中通过 GRPCContainerProbeTLS 功能门控(Feature Gate)添加了 TLS 支持(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 统计了探针的类型(未计算用户业务的一个命名空间,共 271 个 Pod)。

探针类型 数量
httpGet 368
exec 38
tcpSocket 20
grpc 3

3 个 gRPC 探针全部属于 hubble-relay 的启动、存活和就绪探针(端口 4222,服务名称为空字符串,周期为:存活与就绪 10 秒,启动 3 秒)。Relay 日志中也会在启动时留下打开 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. 与小型集群的连接

Titi 的一句话。 包下一辆出租车开一整天的话,调度员再聪明也没用。偶尔下车(MaxConnectionAge)重新排队,才能坐上新出租车!

参考资料

所有资料均查阅于 2026-10-17。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论