目录

gRpc负载均衡的特别之处

gRpc服务发现的实现

客户端侧服务发现的方案

1.基于外部注册中心组件做服务发现

2.客户端基于Kubernetes的DNS做服务发现

最佳实践

3.客户端基于Kubernetes Api接口做服务发现

如何开发gRpc客户端实现服务发现

NameResolverProvider 和 NameResolver 接口


在 k8s 的网络环境下,一个 grpc 的服务,同一个 namespace 下,可以直接通过 service 访问,不同的 namespace 可以通过 service.namespace 访问。

gRpc负载均衡的特别之处

如果是SpringCloud集群之间的通信,是可以借助k8s service 使用服务端负载均衡的。虽然HTTP/1.1 默认开启 Keepalive时也会保持长连接,但是 HTTP/1.1 多个请求不会共享一个连接,如果连接池里没有空闲连接则会新建一个,经过 Service 的负载均衡,各个 pod 上的连接是相对均衡的。

但是gRpc本身是基于Http 2 长连接多路复用的,也就是说无论server端开启了多少个 pod 实例,客户端一般只会与一个pod建立连接并进行连接复用,当然如果并发超出了单连接并发数可能会触发新连接的建立,但是在客户端和服务端数量不对等时,打到 server 侧的流量大概是不均衡的

gRpc客户端负载均衡的原理

客户端与集群中每个后端服务提供者实例都建立一个长连接,在请求时使用客户端的LB算法选择一个连接通道写入请求。

gRpc服务发现的实现

上面说了gRpc负载均衡的特别之处,很显然gRpc是无法依靠服务端的负载均衡机制来做LB的,那么常见的 grpc 负载均衡方法可以分为两类,一类是客户端侧实现负载逻辑,一类是代理侧实现负载逻辑,例如service mesh对客户端侧是透明的。 

客户端侧服务发现的方案

在Kubernetes网络环境里, gRpc客户端侧的负载均衡有几种常见的实现方案。

1.基于外部注册中心组件进行服务发现

笼统来说这种实现方式相当于把微服务架构中的zk,nacos等注册中心组件又搬到了k8s集群中,有点多此一举。

而且对于SpringCloud迁移k8s集群的场景,社区提供了spring-cloud-kubernetes依赖适配了服务的注册和发现,推荐使用这种方案,这样依赖就不用把原架构的注册中心组件引入到k8s集群中。

2.借助K8S的DNS和Headless Services进行服务发现

Headless Services

k8s中service本身实现了对自身Endpoints的管理和负载均衡,定义Service 的 spec.clusterIP 为 ”None“ 可以把service变成Headless Service。

Headless Services使用场景

1.自主选择权,有时候client想自己决定使用哪个Real Server,可以通过查询DNS来获取Real Server的信息
2.headless service关联的每个endpoint(也就是Pod),都会有对应的DNS域名;这样Pod之间就可以互相访问

创建Headless Service服务,Kubernetes将在该服务的DNS条目中创建多个记录,而每个记录与之对应的是一个Pod IP,这时候在客户端访问Service会返回后端多个 pod IP记录,而不是clusterIP,基于这些ip列表结合LB做负载均衡就可以了。

基于这种方式实现服务发现和负载的流程如下

1.客户端通过 Kubernetes 的 Headless Service 访问服务端。

2. gRPC 获取了 Service 的多个地址后会与这些地址建立子通道。

3. gRpc客户端需要自定义实现负载均衡,实现请求轮流发给已建立的子通道。


最佳实践

gRpc框架中其实已经提供了客户端基于DNS做服务发现的的实现,在DnsNameResolverProvider和DnsNameResolver 两个实现类。

使用这种方案时只需要修改k8s中service的配置和在客户端使用DNS协议定义服务提供者的地址。

1.把service配置为Headless service

修改Service的spec.clusterIP参数值为 ”None“

apiVersion: v1
kind: Service
metadata:
  namespace: tap-prod
  name: my-service
  labels:
    app: my-service
spec:
  clusterIP: None #改为None
  ports:
    - port: 8030
      targetPort: 8030
      name: grpc
  selector:
    app: my-service

2.客户端指定服务提供者地址使用dns:/// 协议

如:grpc.client.store.address = dns:///store-rpc:8020

3.借助K8S的Api进行服务发现

与上面的方式类似,上面的方式基于DNS解析的方式获取pod集群的ip信息,这要求service必须是Headless Service。

基于Kubernetes Api接口做服务发现与之不同的是这里使用K8s的get及watch API,对服务的endpoint进行服务发现获取pod集群的ip信息watch api可以实现pod集群变更的监听信息,方便客户端刷新ip列表和连接管理。

GET /api/v1/namespaces/{namespace}/endpoints/{name}

GET /api/v1/watch/namespaces/{namespace}/endpoints/{name}

上面介绍的这些方案之间其实主要是获取服务端地址列表的方式上有所不同,相同的时都需要在客户端侧借助框架相关接口完成服务发现和LB的逻辑。 


还有一点要注意,由于gRpc长连接的特点,所以当服务提供者扩容时客户端都不能及时感知并与其建立连接,新加入的pod无法进行负载。所以在实现客户端LB时也需要实现动态刷新pod的ip列表维护客户端连接信息的功能。

如何开发gRpc客户端实现服务发现

无论是使用上面哪种方式来实现服务发现和负载均衡,都是基于gRpc客户端来做的,所以都要求客户端实现服务列表的拉取和负载均衡选择的功能。

NameResolverProvider 和 NameResolver 接口

grpc-java 客户端提供了 NameResolver 、NameResolverProvider 、NameResolverRegistry 等实现服务注册发现的扩展类。

上面提到的基于K8S的DNS和Headless Services实现服务发现和LB逻辑的方案,对应已经有一套DNS的实现。使用其他方案自定义实现服务发现和LB时可以参考DNS的实现类。