HTTP/2 多路复用踩坑记:gRPC 负载均衡方案对比

在 Kubernetes 环境中部署 gRPC 服务时,发现明明有多个服务端 Pod,但所有请求都只打到了其中一个 Pod,导致负载严重不均。本文将分析这个问题的根本原因,并介绍几种主流解决方案,结合实际代码演示,帮助你在不同场景下做出最佳选择。

一、背景

在 Kubernetes 集群中部署了一个 gRPC 服务,配置如下:

  • 服务端:3 个 Pod 副本(grpc-server-0, grpc-server-1, grpc-server-2)
  • 客户端:1 个 Pod,持续发送请求
  • Service:使用默认的 ClusterIP Service

观察结果:所有请求都路由到了 grpc-server-0,其他两个 Pod 完全没有流量,这样会导致以下问题:

  • 资源浪费:其他 Pod 闲置,无法充分利用集群资源
  • 性能瓶颈:单个 Pod 过载,响应延迟增加
  • 可用性风险:如果该 Pod 故障,所有流量中断

二、根因分析

gRPC 基于 HTTP/2 多路复用:同一 TCP 连接可并行承载多个请求,连接一旦建立即持续复用。Kubernetes Service 的 kube-proxy 仅在连接建立时执行一次 L4 负载均衡,后续所有请求均沿该连接转发,不再重新选择后端,因而无法实现请求级分摊,导致流量集中于单个 Pod。

三、解决方案

方案一:gRPC 内置 Round-Robin 负载均衡

3.1.1 核心原理

将客户端的 LoadBalancingPolicy 从默认的 pick_first 改为 round_robin

关键变化

  • gRPC 会为每个后端地址创建独立的 Sub-Connection
  • 请求在这些子连接间轮询发送
  • 等于”单 client 同时保持 N 条 HTTP/2 连接”
  • 天然打散到 N 个 Pod
3.1.2 代码实现

参考:client.go

// 方案 1:Round-Robin
opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
}

// 使用 Headless Service(返回所有 Pod IP)
conn, err := grpc.Dial("dns:///grpc-server-headless:8000", opts...)

关键点

  • 必须使用 Headless ServiceclusterIP: None
  • Headless Service 在 DNS 查询时返回所有 Pod IP
  • 使用 dns:/// resolver 让 gRPC 获取所有地址
3.1.3 Kubernetes 配置
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: grpc-server-headless
namespace: blazehu
labels:
app: grpc-server
spec:
type: ClusterIP
clusterIP: None # Headless Service - 返回所有 Pod IP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: grpc
selector:
app: grpc-server
3.1.4 特点分析
维度说明
核心原理客户端为每个 Pod 创建独立连接,请求轮询分发
适用场景Kubernetes 环境,需要简单快速的解决方案
部署成本极低:一行代码配置
改造成本极低:只需修改客户端连接配置
性能损失几乎为 0(相对直连 100% QPS)
优点简单、高效、无需额外组件
缺点需要 Headless Service,不支持高级路由(权重、灰度)

方案二:gRPC xDS 直连(Proxyless)

3.2.1 核心原理

仍然使用 gRPC 自己的负载均衡,但把”后端列表”来源从 DNS 换成 xDS 协议

工作流程

  1. Istio/Pilot 以 LDS/RDS/CDS/EDS 形式把 Pod IP 列表推给 gRPC 客户端
  2. 客户端在本地做请求级 RR/wRR(加权轮询)
  3. 不再依赖长连接,而是”每次 pick 一个 Sub-Connection

3.2.2 代码实现

参考:client_xds.go

// 方案 2:Proxyless xDS
import _ "google.golang.org/grpc/xds" // 启用 xDS resolver

// 设置 bootstrap 文件路径(由 grpc-agent 生成)
os.Setenv("GRPC_XDS_BOOTSTRAP", "/etc/istio/proxy/grpc-bootstrap.json")

opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}

// 使用 xds:// scheme
conn, err := grpc.Dial("xds:///grpc-server.blazehu.svc.cluster.local:8000", opts...)

关键点

  • 使用 xds:/// scheme 启用 xDS resolver
  • 需要 Istio grpc-agent 生成 bootstrap.json
  • 支持高级路由:权重、子集(灰度标签)

3.2.3 Kubernetes 配置

# Deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-client
spec:
template:
metadata:
annotations:
sidecar.istio.io/inject: 'true'
inject.istio.io/templates: 'grpc-agent' # 注入 grpc-agent
spec:
containers:
- name: grpc-client
env:
- name: GRPC_XDS_BOOTSTRAP
value: /etc/istio/proxy/grpc-bootstrap.json

grpc-agent 的作用

  • 自动生成符合 Istio 要求的 bootstrap.json
  • 提供 SDS(Secret Discovery Service)证书
  • 维护正确的 node.id 格式

为快速验证 xDS 通路,手动将 bootstrap.json 以 ConfigMap 挂载到客户端 Pod,未注入 grpc-agent;只要 node.id 格式正确且 URI 与后端服务匹配,istiod 即会返回 xDS 响应。

3.2.4 特点分析

维度说明
核心原理xDS 协议直接下发后端列表,客户端本地负载均衡
适用场景已使用 Istio 服务网格,需要高级流量管理
部署成本中等:需要 Istio 控制平面
改造成本中等:需要配置 xDS bootstrap
性能损失几乎为 0(无代理开销)
优点支持权重、灰度、子集路由,无 Sidecar 延迟
缺点依赖 Istio,需要 grpc-agent 支持

方案三:Sidecar(Envoy)HTTP/2 七层负载均衡

3.3.1 核心原理

在 client Pod 内注入 Envoy Sidecar

工作流程

  1. 客户端建立 1 条 HTTP/2 连接到 Sidecar
  2. Envoy 把”单条 HTTP/2 连接”拆成逻辑 Stream
  3. 按配置的负载均衡算法(RR、Least Request、Maglev…)挑选 upstream Pod
  4. 对 client 而言仍是 1 条连接,但 Envoy 内部做了”单连接 → 多 Pod“的映射

Sidecar(Envoy)主要在“客户端侧”负责把单条 HTTP/2 连接的 stream 逐条分发到后端上游(即实现请求级的负载均衡)。因此,为了实现请求级分摊,只在 client Pod 注入 sidecar 即可达到目的,server Pod 是否注入 sidecar 并非必须。

3.3.2 配置示例
# 默认注入 Envoy Sidecar(不指定模板)
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-client
spec:
template:
metadata:
annotations:
sidecar.istio.io/inject: 'true' # 默认注入 Envoy
spec:
containers:
- name: grpc-client
# 客户端直接连接 Service,Envoy 自动拦截
3.3.3 特点分析
维度说明
核心原理Envoy Sidecar 在七层做请求级负载均衡
适用场景已使用服务网格,需要统一的流量管理策略
部署成本中等:需要 Sidecar,增加资源消耗
改造成本:对客户端透明,无需代码修改
性能损失5-10%(Sidecar 代理开销)
优点对客户端透明,支持多种 LB 算法,统一管理
缺点需要 Sidecar,增加延迟和资源消耗

方案四:Istio Gateway(Ingress/Gateway-API)

3.4.1 核心原理

把流量先打到 Ingress Gateway

工作流程

  1. Gateway 本身就是 Envoy,但只经过一次代理
  2. Gateway 同样能把 HTTP/2 stream 打散到 N 个 Pod
  3. client → Gateway(1 条 HTTP/2)→ 多 Pod
3.4.2 配置示例
# Gateway
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: grpc-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: grpc
protocol: HTTP2
hosts:
- grpc-server.example.com

# VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: grpc-server
spec:
hosts:
- grpc-server.example.com
gateways:
- grpc-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: grpc-server
port:
number: 8000

3.4.3 特点分析

维度说明
核心原理Gateway 在入口做七层负载均衡
适用场景需要统一入口管理,外部流量接入
部署成本中等:需要 Gateway 组件
改造成本:只需配置 Gateway 和 VirtualService
性能损失3-5%(单跳代理)
优点集中式流量管理,支持 TLS 终止
缺点需要 Gateway,只适用于入口流量

四、方案对比与选型

4.1 对比矩阵

方案性能损失改造成本适用场景高级功能
Round-Robin~0%极低Kubernetes
xDS Proxyless~0%Istio 网格✅ 权重/灰度
Envoy Sidecar5-10%服务网格✅ 多种算法
Gateway3-5%入口流量✅ TLS/路由

4.2 选型建议

简单 K8s 用客户端 Round-Robin;已落地 Istio 选 xDS Proxyless,享网格级路由与无 Sidecar 延迟;需统一治理则注入 Envoy Sidecar,策略对应用透明;外部入口统一用 Gateway,集中 TLS 与流量调度,单跳即达后端。

五、方案验证

5.1 演示环境

简单实现了一个完整的验证项目,包含:

  • 服务端:3 个 Pod 副本,每个返回自己的 Pod 名称
  • 客户端:支持 3 种模式(pick_first、round_robin、xds)
  • Kubernetes 配置:完整的 Deployment、Service、Job 配置

5.2 测试结果

测试结果符合预期,相关测试结果如下:

# pick_first(default) 默认会负载不均
./grpc_demo client --server-address=grpc-server:8000 --lb-policy pick_first --requests 10000 --concurrent 1000

=== Results ===
Total: 10000
Requests per Server:
grpc-server-7496b78c84-z65dw: 10000 (100.0%)

✓ All requests went to single server (expected for pick_first)


# round_robin
./grpc_demo client --server-address=dns:///grpc-server-headless:8000 --lb-policy=round_robin --requests 10000 --concurrent 1000

=== Results ===
Total: 10000
Requests per Server:
grpc-server-7496b78c84-nt8g4: 3333 (33.3%)
grpc-server-7496b78c84-zcsq5: 3334 (33.3%)
grpc-server-7496b78c84-z65dw: 3333 (33.3%)

✓ Requests distributed across 3 servers (expected for round_robin)


# xds(proxyless)
./grpc_demo client-xds --target=xds:///grpc-server.blazehu.svc.cluster.local:8000 --requests 10000 --concurrent 1000

=== Results ===
Total: 10000
Requests per Server (pod):
grpc-server-7496b78c84-z65dw: 3327 (33.3%)
grpc-server-7496b78c84-nt8g4: 3345 (33.5%)
grpc-server-7496b78c84-zcsq5: 3328 (33.3%)


# sidecar (测试时修改 client workload 注解,用于注入sidecar)
./grpc_demo client --server-address=grpc-server:8000 --lb-policy pick_first --requests 10000 --concurrent 1000

=== Results ===
Total: 10000
Requests per Server:
grpc-server-7496b78c84-zcsq5: 3344 (33.4%)
grpc-server-7496b78c84-nt8g4: 3314 (33.1%)
grpc-server-7496b78c84-z65dw: 3342 (33.4%)


# istio gateway
./grpc_demo client --server-address grpc-server.example.com:80 --lb-policy pick_first --requests 10000 --concurrent 1000

=== Results ===
Total: 10000
Requests per Server:
grpc-server-7496b78c84-z65dw: 3339 (33.4%)
grpc-server-7496b78c84-zcsq5: 3323 (33.2%)
grpc-server-7496b78c84-nt8g4: 3338 (33.4%)

注意:gateway 测试时修改 client-deployment 容器的 /etc/hosts ,将 grpc-server.example.com 域名解析到 istio-ingressgateway svc 的 clusterIP 地址。

六、总结

gRPC HTTP/2 多路复用导致“长连接 = 单 Pod”的根因是:“连接级负载均衡” vs “请求级负载均衡”。
只要让“选 Pod”的决策点从『TCP 建连时』延后到『每个 Stream/请求时』,问题就解决了——以上方案只是实现“延后决策”的不同姿势。

七、参考资料