个人随记 —— gRPC 在 k8s 中的负载均衡
问题描述
微服务架构越来越流行,很多系统采用了 gRPC 进行微服务间的通信,在 k8s 下,自然而然就采用 Service 来实现负载均衡。
但是在观测 gRPC 流量时,发现服务的 gRPC 流量并不均衡,极端场景下出现流量95%都集中在其中一个 server 上,登录到 server 上去观察建立的 tcp 链接,发现并不均衡。。
netstat -anlp
server1 的 tcp 链接情况如下:
server2 的 tcp 链接情况如下:
可以很明显看出来,8888 端口暴露的 gRPC 服务不同 server 的链接并不均衡,说明通过 k8s 的 Service 实现负载均衡出现了问题。
根因分析
gRPC 基于 HTTP/2 实现,支持多路负载均衡,因此官方并没有提供设置连接数等之类的参数,而是默认仅与 server 构建一条 TCP 长连接进行服用,且默认无 idle 设置。
那么问题就很明显,仅仅一条 TCP 链接,如果只采用 k8s 的 Service 的 kube proxy 方案在四层网络上是无法实现负载均衡的。
所以解决方案有这么几种:
- 客户端负载均衡
- 服务端负载均衡
客户端负载均衡
官方提供一种方式来实现负载均衡,文档 link:https://kubernetes.io/blog/2018/11/07/grpc-load-balancing-on-kubernetes-without-tears/
简单来说这个方案有几个关键点:
- Service 改用 headless,这样子在解析 Service 的 dns 地址时候可以解析出来所有的 endpoint 地址
- gRPC 客户端设置负载均衡策略 round_robin 可以与解析出来的各个 ip 建立长连接,并轮训进行请求
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`)
存在的问题:
当 Pod 扩缩容时 客户端可以感知到并更新连接吗?
- Pod 缩容后,由于 gRPC 具有连接探活机制,会自动丢弃无效连接。
- Pod 扩容后,没有感知机制,导致后续扩容的 Pod 无法被请求到。
可以简单在 server 端去设置 gRPC 链接的保持时间来控制。
s := grpc.NewServer(grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionAge: time.Minute,
}))
kuberesolver
当然上述方案并不是一个好的解决方案,明明是在客户端做负载均衡,还需要服务端进行特定的配置配合。
在 k8s 环境下,可以在 client 端调用 Kubernetes API 监测 Service 下挂在的 endpoints 变化,动态更新连接信息。目前 github 已经有相关实现:https://github.com/sercand/kuberesolver
具体就是将 DNSresolver 替换成了自定义的 kuberesolver。
关键点如果 Kubernetes 集群中使用了 RBAC 的话需要给 client 所在 Pod 赋予 endpoints 资源的 get 和 watch 权限。
服务端负载均衡
服务端负载均衡主要是在 Pod 之前增加一个 7 层负载均衡。微服务如果使用的网关设计模式可以使用 nginx,使用的是网格模式,可以考虑 service mesh 中 istio。
小结
建议使用客户端负载均衡来实现,通过 kuberesolver + gRPC 策略 round_robin 进行实现。
这边也扩展一个思考。gRPC 默认只会 client 与 server 只会建立一个 TCP 长连接。但其实如果与同一个 server 建立多个链接的话,在某些场景下是可以提升单节点的 QPS 的。gRPC 连接池的实现推荐 github 上的一个实现:https:/github.com/processout/grpc-go-pool
这边其实也是一个假设,在微服务的架构下,其实不会把 pod 的规格拉的很大,一个 TCP 长连接的流量完全可以吃掉这个 pod 分配的资源,提升性能就是进行水平扩展。在规格较大的 pod 场景下,才需要考虑多连接的引入。