引言
在 Kubernetes (K8s) 环境中,gRPC 作为一种高性能的 RPC 框架被广泛应用。然而,许多开发者在实践中发现,标准的 K8s Service 似乎无法对 gRPC 服务进行有效的负载均衡,导致所有请求都涌向了单个 Pod。本文将深入探讨这个问题的根源,并详细介绍四种主流的解决方案。
参考资料:
问题的根源:L4 vs L7 负载均衡
传统的 K8s Service(如 ClusterIP)工作在 L4(传输层),它通过 kube-proxy 使用 IPtables 或 IPVS 来分发 TCP/UDP 流量。这种模式对于大多数基于 HTTP/1.1 的服务工作得很好,因为每个请求通常对应一个新的 TCP 连接。
然而,gRPC 构建于 HTTP/2 之上,其核心特性之一是多路复用(Multiplexing)。客户端与服务端之间会建立一个长期存在的 TCP 连接,所有的 gRPC 请求都在这个单一的连接上并发传输。

这就导致了 L4 负载均衡的失效:
kube-proxy在建立连接时,将 TCP 连接路由到了后端的某一个 Pod。- 由于连接是长期的,后续所有通过此连接的 gRPC 请求都会被发送到同一个 Pod。
- 即使后端有多个 Pod 实例,其他 Pod 也无法接收到流量,从而失去了负载均衡的效果。
为了解决这个问题,我们需要一个能够理解 HTTP/2 和 gRPC 协议的 L7(应用层)负载均衡器,它能够在请求级别上进行分发,而不是在连接级别。
方案一:客户端负载均衡 (Client-Side Load Balancing)
这是最直接的解决方案,将负载均衡的逻辑从基础设施层移至客户端。gRPC 客户端本身支持可插拔的名称解析 (Name Resolution) 和负载均衡策略 (Load Balancing Policy)。
核心思路:
- 服务发现: 客户端需要一种机制来发现后端服务的所有 Pod IP 地址。
- 负载均衡: 客户端在获取到所有 Pod IP 后,根据指定的策略(如
round_robin)将请求分发到不同的 Pod。
实现方式:Headless Service + DNS Resolver
这是最常见的客户端负载均衡实现。
创建 Headless Service:
为 gRPC 服务创建一个 Headless Service(即设置clusterIP: None)。K8s 不会为这个 Service 分配 ClusterIP,而是会为它创建一个 DNS A 记录,该记录直接解析到后端所有 Pod 的 IP 地址列表。1
2
3
4
5
6
7
8
9
10
11
12apiVersion: v1
kind: Service
metadata:
name: my-grpc-service
spec:
clusterIP: None # 关键配置
selector:
app: my-grpc-app
ports:
- protocol: TCP
port: 8080
targetPort: 8080配置 gRPC 客户端:
在客户端,我们需要:- 使用
dns作为名称解析器。 - 将负载均衡策略设置为
round_robin。
以
grpc-js为例:1
2
3
4
5
6
7
8
9const client = new MyServiceClient(
'dns:///my-grpc-service.default.svc.cluster.local:8080', // 使用 dns resolver
grpc.credentials.createInsecure(),
{
'grpc.service_config': JSON.stringify({
loadBalancingConfig: [{ round_robin: {} }] // 设置轮询策略
})
}
);- 使用

存在的问题与优化
这种方案的一个主要挑战是如何处理 Pod 的动态变化。
- Pod 缩容: 当一个 Pod 被删除时,客户端的连接会断开,gRPC 的连接探活机制可以处理这种情况,客户端会自动从可用地址列表中移除该 Pod。
- Pod 扩容: 当新的 Pod 加入时,客户端不会自动感知。因为 DNS 解析结果会被缓存,客户端不会主动重新解析 DNS 来获取新的 Pod IP。
解决方案:
设置连接最大存活时间 (
max_connection_age):
通过在服务端和客户端设置max_connection_age_ms,可以强制 gRPC 连接在一段时间后关闭并重新建立。在重建连接的过程中,客户端会重新进行 DNS 解析,从而发现新加入的 Pod。- 服务端 (grpc-js):
1
2
3
4const server = new grpc.Server({
'grpc.max_connection_age_ms': 300000, // e.g., 5 minutes
'grpc.max_connection_age_grace_ms': 5000 // 宽限期
}); - 客户端 (grpc-js):
1
2
3const client = new MyServiceClient(address, credentials, {
'grpc.dns_min_time_between_resolutions_ms': 5000 // 最小DNS解析间隔
});
缺点: 这种方式比较粗暴,可能会中断正在进行的请求,并引入不必要的连接开销。
- 服务端 (grpc-js):
使用
kuberesolver(Go 特定):
对于 Go 语言,社区提供了 kuberesolver 库。它实现了一个自定义的 gRPC 名称解析器,通过直接 Watch K8s API Server 的 Endpoints 资源来动态获取 Pod 列表。1
2
3
4import "github.com/sercand/kuberesolver/v3"
kuberesolver.RegisterInCluster()
cc, err := grpc.Dial("kubernetes:///my-grpc-service.default:8080", opts...)这种方式响应更及时,也更优雅,但需要为客户端 Pod 配置访问 K8s API 的 RBAC 权限。
解决方案2:Istio【service mesh】
参考:https://istio.io/latest/zh/docs/ops/configuration/traffic-management/protocol-selection/
Istio 可以自动检测出 HTTP 和 HTTP/2 流量。如果未自动检测出协议,流量将会视为普通 TCP 流量。
手动调整请求类型
- 可在Service中手动调整appProtocol为
grpc来指定其请求类型。 - 也可调整Service - port name的前缀,如
name: grpc-backend
HTTP routes will be applied to platform service ports named "http-" / "http2-" / "grpc-*"
实战
部署单元
- server
1 | # 这里依旧使用普通的Service类型 |
- client
1 | apiVersion: v1 |
DestinationRule
1 | apiVersion: networking.istio.io/v1beta1 |
VirtualService
1 | apiVersion: networking.istio.io/v1beta1 |
测试
测试请求成功与否:
一个client副本,一个server副本
- 由client发起请求到server,响应200,请求成功
测试负载效果及扩容副本能否自动感知:
一个client副本,扩容一个server副本【此时为2 server副本】
- 不间断由client发起请求到server,请求在这两个server副本间轮询
- 待新副本启动成功后,新请求就会落到新副本上
测试死亡副本是否会接收请求:
一个client副本,缩容一个server副本【此时为1 server副本】
- 不间断由client发起请求到server,client端无任何错误请求日志
测试重启server端后,client能否感知新副本
- 不间断由client发起请求到server,client端无任何错误请求日志
- 待新副本启动成功后,请求就会落到新副本上
测试限流/熔断功能:
xDS【此方案为Envoy的替代方案,很多功能仍属于实验性,不推荐】
参考链接:
- Istio xDS协议简析
- gRPC - xDS在Istio下的应用配置【Istio官方Post】
- BootStrapConfig【github】 - tells the
gRPClibrary how to connect toistiodwith xDS【补充,GRPC_XDS_BOOTSTRAP用来声明file路径,GRPC_XDS_BOOTSTRAP_CONFIG用来声明内容】 - xDS在各语言版本下的功能支持描述【github】
- Istio - gRPC官方部署样例【github】
- grpc-js-xds【github】的bootstrap对象声明位置
- grpc-js-xds npmjs
- 使用 istio 了解 Kubernetes 中的 gRPC 负载均衡【补充,这篇内容有些过时】
- 关于gRPC中的负载均衡
- 准备使用无代理 gRPC 服务设置 Traffic Director【By using Google Cloud】
- BootStrap - 对于node节点下Regions and Zones的可选code列表
- BootStrap - node节点proto message【github】
- xDS v3支持【github】
gRPC的xDS是一种服务发现和负载均衡的协议,它允许gRPC客户端和服务器动态地发现网络中的服务资源。xDS协议基于Envoy xDS API,使得gRPC可以与支持xDS API的开源控制平面(如Istio Pilot)进行交互。xDS提供了更加灵活和先进的负载均衡策略配置功能,以及基于LRS(负载报告服务)的负载报告功能。
总的来说,xDS为gRPC提供了一个动态和可扩展的服务发现和负载均衡解决方案。
解决方案3:注册中心【nacos/consul】
nacos/consul等注册中心组件基本都支持常见的协议,包括grpc,但都需要和项目代码高度耦合
解决方案4:代理端走 ingress【ingress-nginx】
参考:https://kubernetes.github.io/ingress-nginx/examples/grpc/
- 部署nginx ingress controller:略
nginx-ingress-controller 从 0.30.0 版本开始支持 grpc 的流量代理
在 nginx-ingress 代理模式下,grpc 的流量是负责均衡的。这种改动方式也比较简单,服务方只需要新增一个 ingress 代理 grpc 流量即可,客户端链接是无感的,不需要做任何改动
- 缺点:需要ssl
1 | apiVersion: networking.k8s.io/v1 |