用etcd做服务发现(service discovery)

什么是服务发现

服务发现其实有两层含义,第一层是实例发现,第二层是端口发现。
如果有两个服务A和B,两个服务都是分布式系统,在某个时刻服务A作为客户端要请求服务B,那么这时从服务A中的某个实例(instance)Ai就要访问服务B中的某个实例Bi,那么Ai找到一个合适的Bi就是服务发现的第一层含义,即实例发现。
Ai找到合适的Bi后其实还没完事,Ai需要知道Bi响应这个请求的应用程序,也就是说需要知道Bi响应这个请求的端口号。由于大部分应用程序的端口号都是随机的,Ai不知道到底请求哪个端口,这就需要Bi找个办法告知Ai。这就是端口发现。
本文所述的服务发现包括实例发现和端口发现,因为虽然说现在是微服务时代,但还是有很多服务承载着若干个功能,这就需要用不同的端口去实现不同的功能,也就需要做端口发现。

为什么要用etcd

如果只考虑效果,不考虑时间、金钱和人力成本,其实用啥样的数据库都能做服务发现。但对于一个用Go语言写的服务来说,由于etcd本身就是Go语言写的,API简单,具有强一致性,并且还有消息广播功能,主要还开源。这也就让etcd有得天独厚的优势,让很多Go语言的服务都用etcd来做服务发现和配置分发。另外,etcd也形成了自己的生态,和K8s集成的很好(可能因为都是Go语言写的),所以现在Go服务用etcd的越来越多。

怎么用etcd做服务发现

对于客户端来说,请求需要分配到哪个实例是完全不知道的,这时候就需要在客户端和服务端之间加入一个服务发现层。一个简单的思想就是,引入一个etcd集群,服务端每次有新实例加入服务时,就向etcd中注册自己的ip和port。客户端只要访问etcd,拿到能够响应请求,并且合适的那个实例的ip和port即可。

比如服务B有个功能Add,服务B中的某个实例Bi在启动时可以注册功能Add的端口和自身的ip到etcd。Bi可以将功能名、ip和端口号组合后存储到etcd,比如以\<FunctionName\>:\<hostIP\>:\<port\>的格式作为key,以\<hostIP\>:\<port\>的格式作为value存储。当服务A请求服务B的Add功能时,请求会先到etcd,根据前缀\<FunctionName\>:拿到能够支持的这个功能的ip和port,然后以某种负载均衡的方式请求一个ip和port,这种负载均衡方式一般都采用round-robin,以保证服务B中的每个实例承载的流量尽量平均。
对于应用程序来说,其实并不想关心这些服务发现的过程,所以在客户端和服务端都有一层服务发现的协议(以下称为服务发现层),来使得这些过程对于应用程序来说是无感知的。服务端的服务发现层就需要在实例添加的时候将实例的ip和port注册到etcd,并且定时向etcd发送心跳,如果etcd超过某个时间没有收到该ip和port的心跳,那么etcd就会删除掉这条记录。客户端的服务发现层就需要每次去请求etcd拿到合适的ip和port返回给应用程序,应用程序再进行远程调用。
如果客户端每次都要请求etcd拿合适的ip和port,那么每次都要增加远程调用,会增大每次调用的延迟,这时就需要想办法减少远程调用。如果我们允许调用失败的存在,那么可以将请求和服务发现解耦,进行异步的服务发现。
借助etcd强大的监听(Watch)功能,可以很方便地实现请求和服务发现的异步调用。客户端可以在本地用缓存记录服务端最新的ip和port,如果etcd有新增的key,即代表服务端有新上的实例,etcd就通知客户端,客户端在本地缓存新增这个ip和port。如果etcd有删除的key,即代表服务端有实例下线(可能是服务端主动缩容或机器爆炸了),也会通知客户端,客户端就可以删除本地缓存中这个ip和port。这样每次客户端调用服务端的时候,只需要检查本地的ip和port列表,用一种负载均衡的方式去请求服务端的某个ip和port即可。或许这种异步调用有时会出现一种edge case:服务端某个实例刚下线,etcd还没来得及通知到客户端,客户端刚好需要访问这个实例,这时客户端对服务端的这个请求就会发生不可达的错误。但是发生这种不一致错误的概率很小,站在成本的角度来说,客户端本地缓存ip和port,进行异步调用是用etcd做服务发现成本最低的方案。

小结

其实端口发现在很多著名的RPC实现里都早已实现了,一般就是在知名端口加一个portmapper,服务端请求这个知名端口拿到需要的端口即可。实例发现有很多种解决方案,Go服务用etcd做实例发现很多公司都有成熟的解决方案,实际工程和架构中需要考虑的问题肯定比本文中描述的要多,但基本思想都和本文中描述的差不多。如果作为一个服务的架构师,用开源的etcd并且用本地缓存异步调用做服务发现是一种兼顾了性能和成本的方案。

分享到