由于grpc天生的长连接特性, 如果要在kubernetes中使用grpc, 想要实现负载均衡问题需要考虑长连接问题.
由于本人对grpc了解的不是很深入,没法说的太多,看官们勿喷.
http/2长连接
详情大家可参考这篇文章在K8S上使用gRPC做负载均衡
上述文章中也提出了可有多种方式处理这种场景.
今天要跟大家说一说,使用kubernetes的headless 给grpc做负载均衡引出的问题.因为这种方式最简单且最直接.
需要很简单: 客户端通过grpc调用服务端, 请求可以负载均衡
很简单的demo, 一个服务端, 一个客户端
客户端
客户端逻辑很简单, 主要代码如下:
1 | func lbconn() *grpc.ClientConn { |
开始只有一个实例,启动日志:
从这里也可以看到,grpc启动的时候,会直接通过服务名拿到服务端pod的ip列表
下面是多个服务端实例的情况
服务端
服务端主要代码:
1 | func (s *grpcServer) Hi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) { |
服务端启动日志:
ClusterIP
服务端使用常规的ClusterIP的svc代理出来,这种情况我们发现, 客户端所有的请求都打在一个实例上, 服务端新增或者减少对客户端都没有影响(当然, 如果减少的刚好是连接的那个实例,那所有的实例都会切换到另一实例上), 至始至终都是一个服务端实例在响应.没有达到效果.
因为ClusterIP暴露出来的是一个ip, grpc通过这个ip只能转到某一个实例上进行响应, 所以客户端一起与这个特定的实例建立长连接.
可参考这里
Headless
1 | apiVersion: v1 |
问题
从测试的情况来看:
如果服务实例从2个scale up到5个, 所有的请求还是直接路由到之前的2个, 新增的3个没有请求进来.
如果是从2个scale down到1个,那客户端可以感应到,请求会自动地切换剩余的实例,这个很好理解, grpc维持的长连接具有探活能力.
Scale up这种情况grpc客户端无法更新服务端列表,原因估计是grpc启动的时候通过服务名 –> dns –> headless 直接就拿到了服务端pod的实例ip列表,维持在类似连接池中, 后面所有的请求都直接从连接池中做路由,而不是每次都通过服务名实时地查询dns记录, (实时查询的话那一定是会得到新增实例的.), 这也让grpc更高效
所以,在kubernetes中对grpc的服务,需要做特殊的处理, 如果说服务上线就能确定服务端实例个数, 后续实例数都不会变化,那可以直接使用headless这种方式,但谁也不能保证实例数永远不变或者不会发生重启等情况.
因此, 可以在客户端侧实现watch endpoints机制来保证,服务端实例发生变化时,客户端会即时地拿到最新的实例列表,从而确保了请求的负载均衡, github上有直接的解决方案, 大家可以参考这里kuberesolve
客户端实现创建k8s client即可.
1 | var conns []*grpc.ClientConn |
使用这种watch方式,我们同时测试了,服务端用headless或者clusterip的svc效果都是一样的.
至此,虽然grpc也是基于http协议,但是因为其长连接的特性, 在kubernetes中确实与http会有所不同.
当然, 把watch ep逻辑集成在client侧还是有点侵入性的,应该有更好的办法能实现, 目前没有找到, 如果有了解的同学欢迎告知.