gRPC 服务注册发现及负载均衡的实现方案与源码解析
今天聊一下gRPC的服务发现和负载均衡原理相关的话题,不同于Nginx
、Lvs
或者F5
这些服务端的负载均衡策略,gRPC采用的是客户端实现的负载均衡。什么意思呢,对于使用服务端负载均衡的系统,客户端会首先访问负载均衡的域名/IP,再由负载均衡按照策略分发请求到后端具体某个服务节点上。而对于客户端的负载均衡则是,客户端从可用的后端服务节点列表中根据自己的负载均衡策略选择一个节点直连后端服务器。
Etcd
软件包的naming
组件里提供了一个命名解析器(naming resolver)结合gRPC
本身自带的RoundRobin
轮询调度负载均衡器,让使用者能方便地搭建起一套服务注册/发现和负载均衡体系。如果轮询调度满足不了调度需求或者不想使用Etcd
作为服务的注册中心和命名解析器的话,可以通过写代码实现gRPC
定义的Resolver
和Balancer
接口来满足系统的自定义需求。
本文引用的源码对应的版本为:gRPC v1.2.x、 Etcd v3.3
gRPC服务注册发现
先来简单的说明一下用Etcd
实现服务注册和发现的原理。服务注册和发现这个流程可以用下面这个示意图简单描述出来:

上图的服务A包含了两个节点,服务在节点上启动后,会以包含服务名加节点IP的唯一标识作为Key(比如/service/a/114.128.45.117),服务节点IP和端口信息作为值存储到Etcd
上。这些Key都是带租约的Key,需要我们的服务自己去定期续租,一旦服务节点本身宕掉,比如node2上的服务宕掉,无法完成续租后,那么它对应的Key:/service/a/114.128.45.117 就会过期,客户端也就无法再从Etcd上获取到这个服务节点的信息了。
与此同时客户端也会利用Etcd
的Watch
功能监听以/servive/a
为前缀的所有Key的变化,如果有新增或者删除节点Key的事件发生Etcd
都会通过WatchChan
发送给客户端,WatchChan
在编程语言上的实现就是Go
的Channel
。
服务注册
关于Etcd
的服务注册,官方提供的软件包里并没有提供统一的注册函数供调用。那么我们在新增服务节点后怎么把节点的信息存储到Etcd
上并通知给命名解析器呢?在Etcd源码包的naming/grpc.go里可以发现提供了一个Update
方法,这个Update
既能执行添加也能执行删除操作:
服务在启动完成后可以通过Update
方法把自己的服务地址和端口Put
到自定义的target为前缀的key里,针对上面图示里的例子,变量target就应该是我们定义的服务名/service/a。一般在具体实践里都是自己根据系统的需求封装Update
方法完成服务注册,以及服务节点Key在Etcd上的定期续租,这块每个公司的实践都不一样,我就不放具体的代码了,一般续租都是通过Etcd
租约里的KeepAlive
方法实现的(Lease.KeepAlive)。
服务发现
在注册完新节点、或者是原来的节点停掉后,客户端是怎么知道的呢?这块就需要命名解析器Resolver来帮助实现了,Resolver的作用可以理解为从一个字符串映射到一组IP端口等信息。
gRPC对Resolver的接口定义如下:
命名解析器的Resolve方法会返回一个Watcher,这个Watcher可以监听命名解析器发来的target(类似上面例子里说的与服务名相对应的Key)对应的后端服务器地址信息变化,通知Balancer对自己维护的地址进行动态地增删。
Watcher接口的定义如下:
Etcd为这两个接口都提供了实现:
这部分GRPCResolver
和gRPCWatcher
类型的每个方法的功能和起到的作用都和RoundRobin
这个gRPC Balancer结合地比较紧密,我准备放到下面和负载均衡的源码实现一起说明。
负载均衡
首先我们来看一下gRPC对负载均衡的接口定义:
在gRPC 客户端与服务端之间建立连接时调用的Dail
方法里可以用WithBalancer
方法在DiaplOption
里指定负载均衡组件:
上面的例子使用了gRPC自带的Balancer实现RoundRobin,RoundRobin除了实现了Balancer接口外自己内置了Resolver用来从名字获取其后绑定的IP信息以及服务的更新事件(增加删除服务节点这些事件) 。上面的例子里给RoundRobin指定了Etcd
提供的name.GRPCResolver
做为它的命名解析器,这个命名解析器就是上一节说的Etcd
软件包里提供的gRPCnaming.Resolver
接口实现。
RoundRobin
下面我们研究一下gRPC包里提供的RoundRobin
代码实现,主要关注负载均衡和利用Resolver进行服务发现及节点更新这两个功能的代码实现原理
RoundRobin
结构体定义如下:
r是命名解析器,可以定义自己的命名解析器,如Etcd命名解析器。如果r为nil,那么Dial中参数target将直接作为可请求地址添加到addrs中。
w是命名解析器Resolve方法返回的watcher,该watcher可以监听命名解析器发来的地址信息变化,通知roundRobin对addrs中的地址进行动态的增删。
addrs是从命名解析器获取地址信息数组,数组中每个地址不仅有地址信息,还有gRPC与该地址是否已经创建了ready状态的连接的标记。
addrCh是地址数组的Channel,该Channel会在每次命名解析器发来地址信息变化后,将所有地址更新通知到gRPC内部的lbWatcher,lbWatcher是统一管理地址连接状态的协程,负责新地址的连接与被删除地址的关闭操作。
next是roundRobin的Index,即轮询调度遍历到addrs数组中的哪个位置了。
waitCh是当addrs中地址为空时,grpc调用
Get()
方法希望获取到一个到target的连接,如果设置了gRPC的failfast为false,那么Get()
方法会阻塞在此Channel上,直到有ready的连接。
启动RoundRobin
启动RoundRobin就是实现Balancer接口的Start方法,该方法是由一开始通过grpc.WithBalancer
把负载均衡器指定给的BalancerWrapperBuilder
在创建BalancerWrapper
时触发的:
Start方法其主要功能就是通过RoundRobin的命名解析器的Resolve
方法拿到监听命名解析器后端变化的Watcher
。与此同时还会新建一个addrChan
用于向gRPC
内部的lbWatcher
推送Watcher
监听到的地址变化。
创建完addrCh后在Start方法最后会开启一个goroutine,这个goroutine会不停地循环调用watchAddrUpdates
查询是否有命名解析器的Watcher
传递过来的更新。
监听服务端地址的更新
在watchAddrUpdates
方法里就是通过上面Start方法里创建的Resolver Watcher的Next方法来监听Etcd
上后端服务节点的更新,这个Watcher的实现就是上面服务发现章节里说的Etcd软件包里提供的gRPCWatcher
类型,它的Next方法里会去通过监听Etcd上由服务名组成的Key的变化,然后在这里把这些信息传递给上面Start
方法里创建好的addrChan
通道。
建立连接
Up方法是gRPC内部负载均衡的watcher
调用的,该watcher
会读全局的连接状态队列,改变RoundRobin维护的连接列表的里连接的状态 (会有单独的goroutine向目标服务发起连接尝试,尝试成功后才会把连接对象的连接状态改为connected),如果是已连接状态的连接 ,会调用Up方法来改变addrs地址数组中该地址的状态为已连接。
关闭连接
关闭连接使用的是Down方法,这个方法就简单, 直接找到addr置为不可用就行了。
客户端获取连接
客户端在调用gRPC
具体Method
的Invoke
方法里,会去RoundRobin
的连接池addrs里获取连接,如果addrs为空,或者addrs里的地址都不可用,Get()
方法会返回错误。但是如果设置了failfast = false
,Get()
方法会阻塞在waitCh
这个通道上,直至Up
方法给到通知,然后轮询调度可用的地址。
总结
整个gRPC
基于Etcd
实现服务注册/发现以及负载均衡的流程和关键的源码实现就梳理完了,其实源码实现的细节远比我这里列举的要复杂,这篇文章的目的也是希望能记录下一学习和实践gRPC的负载均衡和服务解析时的一些关键路径。另外需要注意的是本文里使用的是gRPC v1.2.x的代码,在1.3版本后官方包重新调整了目录和包名,与本文里列举的源码以及Balancer的使用上都会有些出入,不过原理还是大致一样的,只不过每一版都一直在此基础上演进。
看到这里了,如果喜欢我的文章可以帮我点个赞,我会每周通过技术文章分享我的所学所见和第一手实践经验,感谢你的支持。微信搜索关注公众号「网管叨bi叨」第一时间获取我的文章推送。
版权声明: 本文为 InfoQ 作者【Kev】的原创文章。
原文链接:【http://xie.infoq.cn/article/2f1dfde2fca7c9fc90991384e】。未经作者许可,禁止转载。
评论