点击上方“芋道源码”,选择“置顶公众号”
技术文章第一时间送达!
源码精品专栏
[**中文详细注释的开源项目**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484404&idx=1&sn=109f263e51b81ca9f270846dd16f6b3a&chksm=fa497c45cd3ef55358b09beb6e18ba04737799d3c0bc32baaa0796dc707b1275c0c555a249ba&scene=21#wechat_redirect)
[**Spring MVC 和 Security 源码合集**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484380&idx=1&sn=b4e0da1a314d77dcd170a25ed1ebb4c5&chksm=fa497c6dcd3ef57bcfb69a52c594bcb72e35d9bbe89fa87601b2a6c9f266d656b1ad2a5d4da4&scene=21#wechat_redirect)
摘要: 原创出处 https://www.cnkirito.moe/rpc-registry/ 「老徐」欢迎转载,保留摘要,谢谢!
在我们之前 RPC 原理的分析中,主要将笔墨集中在 Client 和 Server 端。而成熟的服务治理框架中不止存在这两个角色,一般还会有一个 Registry(注册中心)的角色。一张图就可以解释注册中心的主要职责。
注册中心的地位
目前主要的注册中心可以借由 zookeeper,eureka,consul,etcd 等开源框架实现。互联网公司也会因为自身业务的特性自研,如美团点评自研的 MNS,新浪微博自研的 vintage。
本文定位是对注册中心有一定了解的读者,所以不过多阐述注册中心的基础概念。
注册中心的抽象
借用开源框架中的核心接口,可以帮助我们从一个较为抽象的高度去理解注册中心。例如 motan 中的相关接口:
服务注册接口
public interface RegistryService {
//1. 向注册中心注册服务
void register(URL url);
//2. 从注册中心摘除服务
void unregister(URL url);
//3. 将服务设置为可用,供客户端调用
void available(URL url);
//4. 禁用服务,客户端无法发现该服务
void unavailable(URL url);
//5. 获取已注册服务的集合
CollectionURL getRegisteredServiceUrls();
}
服务发现接口
public interface DiscoveryService {
//1. 订阅服务
void subscribe(URL url, NotifyListener listener);
//2. 取消订阅
void unsubscribe(URL url, NotifyListener listener);
//3. 发现服务列表
ListURL discover(URL url);
}
主要使用的方法是 RegistryService#register(URL) 和 DiscoveryService#discover(URL)。其中这个 URL 参数被传递,显然也是很重要的一个类。
public class URL {
private String protocol;//协议名称
private String host;
private int port;
// interfaceName,也代表着路径
private String path;
private MapString, String parameters;
private volatile transient MapString, Number numbers;
}
注册中心也没那么玄乎,其实可以简单理解为:提供一个存储介质,供服务提供者和服务消费者共同连接,而存储的主要信息就是这里的 URL。但是具体 URL 都包含了什么实际信息,我们还没有一个直观的感受。
注册信息概览
以元老级别的注册中心 zookeeper 为例,看看它实际都存储了什么信息以及它是如何持久化上一节的 URL。
为了测试,我创建了一个 RPC 服务接口
com.sinosoft.student.api.DemoApi
,并且在 6666 端口暴露了这个服务的实现类,将其作为服务提供者。在 6667 端口远程调用这个服务,作为服务消费者。两者都连接本地的 zookeeper,本机 ip 为 192.168.150.1。
使用 zkClient.bash 或者 zkClient.sh 作为客户端连接到本地的 zookeeper,执行如下的命令:
[zk: localhost:2181(CONNECTED) 1] ls /motan/demo_group/com.sinosoft.student.api.DemoApi
[client, server, unavailableServer]
zookeeper 有着和 linux 类似的命令和结构,其中 motan,demo_group,com.sinosoft.student.api.DemoApi,client, server, unavailableServer 都是一个个节点。可以从上述命令看出他们的父子关系。
/motan/demo_group/com.sinosoft.student.api.DemoApi
的结构为 /框架标识/分组名/接口名,其中的分组是 motan 为了隔离不同组的服务而设置的。这样,接口名称相同,分组不同的服务无法互相发现。如果此时有一个分组名为 demo_group2 的服务,接口名称为 DemoApi2,则 motan 会为其创建一个新的节点
/motan/demo_group2/com.sinosoft.student.api.DemoApi2
而 client,server,unavailableServer 则就是服务注册与发现的核心节点了。我们先看看这些节点都存储了什么信息。
server 节点:
[zk: localhost:2181(CONNECTED) 2] ls /motan/demo_group/com.sinosoft.student.api.DemoApi/server
[192.168.150.1:6666]
[zk: localhost:2181(CONNECTED) 3] get /motan/demo_group/com.sinosoft.student.api.DemoApi/server/192.168.150.1:6666
motan://192.168.150.1:6666/com.sinosoft.student.api.DemoApi?serialization=hessian2&protocol=motan&isDefault=true&maxContentLength=1548576&shareChannel=true&refreshTimestamp=1515122649835&id=motanServerBasicConfig&nodeType=service&export=motan:6666&requestTimeout=9000000&accessLog=false&group=demo_group&
client 节点:
[zk: localhost:2181(CONNECTED) 4] ls /motan/demo_group/com.sinosoft.student.api.DemoApi/client
[192.168.150.1]
[zk: localhost:2181(CONNECTED) 5] get /motan/demo_group/com.sinosoft.student.api.DemoApi/client/192.168.150.1
motan://192.168.150.1:0/com.sinosoft.student.api.DemoApi?singleton=true&maxContentLength=1548576&check=false&nodeType=service&version=1.0&throwException=true&accessLog=false&serialization=hessian2&retries=0&protocol=motan&isDefault=true&refreshTimestamp=1515122631758&id=motanClientBasicConfig&requestTimeout=9000&group=demo_group&
unavailableServer 节点是一个过渡节点,所以在一切正常的情况下不会存在信息,它的具体作用在下面会介绍。
从这些输出数据可以发现,注册中心承担的一个职责就是存储服务调用中相关的信息,server 向 zookeeper 注册信息,保存在 server 节点,而 client 实际和 server 共享同一个接口,接口名称就是路径名,所以也到达了同样的 server 节点去获取信息。并且同时注册到了 client 节点下(为什么需要这么做在下面介绍)。
注册信息详解
Server 节点
server 节点承担着最重要的职责,它由服务提供者创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正网络拓扑位置以及得知如何调用。demo 中我只在本机 [192.168.150.1:6666] 启动了一个实例,所以在server 节点之下,只存在这么一个节点,继续 get 这个节点,可以获取更详细的信息
motan://192.168.150.1:6666/com.sinosoft.student.api.DemoApi?serialization=hessian2&protocol=motan&isDefault=true&maxContentLength=1548576&shareChannel=true&refreshTimestamp=1515122649835&id=motanServerBasicConfig&nodeType=service&export=motan:6666&requestTimeout=9000000&accessLog=false&group=demo_group&
作为一个 value 值,它和 http 协议的请求十分相似,不过是以 motan:// 开头,表达的意图也很明确,这是 motan 协议和相关的路径及参数,关于 RPC 中的协议,可以翻看我的上一篇文章《深入理解RPC之协议篇》。
serialization 对应序列化方式,protocol 对应协议名称,maxContentLength 对应 RPC 传输中数据报文的最大长度,shareChannel 是传输层用到的参数,netty channel 中的一个属性,group 对应分组名称。
上述的 value 包含了 RPC 调用中所需要的全部信息。
Client 节点
在 motan 中使用 zookeeper 作为注册中心时,客户端订阅服务时会向 zookeeper 注册自身,主要是方便对调用方进行统计、管理。但订阅时是否注册 client 不是必要行为,和不同的注册中心实现有关,例如使用 consul 时便没有注册。
由于我们使用 zookeeper,也可以分析下 zookeeper 中都注册了什么信息。
motan://192.168.150.1:0/com.sinosoft.student.api.DemoApi?singleton=true&maxContentLength=1548576&check=false&nodeType=service&version=1.0&throwException=true&accessLog=false&serialization=hessian2&retries=0&protocol=motan&isDefault=true&refreshTimestamp=1515122631758&id=motanClientBasicConfig&requestTimeout=9000&group=demo_group
和 Server 节点的值类似,但也有客户独有的一些属性,如 singleton 代表服务是否单例,check 检查服务提供者是否存在,retries 代表重试次数,这也是 RPC 中特别需要注意的一点。
UnavailableServer 节点
unavailableServer 节点也不是必须存在的一个节点,它主要用来做 server 端的延迟上线,优雅关机。
延迟上线:一般推荐的服务端启动流程为:server 向注册中心的 unavailableServer 注册,状态为 unavailable,此时整个服务处于启动状态,但不对外提供服务,在服务验证通过,预热完毕,此时打开心跳开关,此时正式提供服务。
优雅关机:当需要对 server 方进行维护升级时,如果直接关闭,则会影响到客户端的请求。所以理想的情况应当是首先切断流量,再进行 server 的下线。具体的做法便是:先关闭心跳开关,客户端感知停止调用后,再关闭服务进程。
感知服务的下线
服务上线时自然要注册到注册中心,但下线时也得从注册中心中摘除。注册是一个主动的行为,这没有特别要注意的地方,但服务下线却是一个值得思考的问题。服务下线包含了主动下线和系统宕机等异常方式的下线。
临时节点+长连接
在 zookeeper 中存在持久化节点和临时节点的概念。持久化节点一经创建,只要不主动删除,便会一直持久化存在;临时节点的生命周期则是和客户端的连接同生共死的,应用连接到 zookeeper 时创建一个临时节点,使用长连接维持会话,这样无论何种方式服务发生下线,zookeeper 都可以感知到,进而删除临时节点。zookeeper 的这一特性和服务下线的需求契合的比较好,所以临时节点被广泛应用。
主动下线+心跳检测
并不是所有注册中心都有临时节点的概念,另外一种感知服务下线的方式是主动下线。例如在 eureka 中,会有 eureka-server 和 eureka-client 两个角色,其中 eureka-server 保存注册信息,地位等同于 zookeeper。当 eureka-client 需要关闭时,会发送一个通知给 eureka-server,从而让 eureka-server 摘除自己这个节点。但这么做最大的一个问题是,如果仅仅只有主动下线这么一个手段,一旦 eureka-client 非正常下线(如断电,断网),eureka-server 便会一直存在一个已经下线的服务节点,一旦被其他服务发现进而调用,便会带来问题。为了避免出现这样的情况,需要给 eureka-server 增加一个心跳检测功能,它会对服务提供者进行探测,比如每隔30s发送一个心跳,如果三次心跳结果都没有返回值,就认为该服务已下线。
注册中心对比
一般而言注册中心的特性决定了其使用的场景,例如很多框架支持 zookeeper,在我自己看来是因为其老牌,易用,但业界也有很多人认为 zookeeper 不适合做注册中心,它本身是一个分布式协调组件,并不是为注册服务而生,server 端注册一个服务节点,client 端并不需要在同一时刻拿到完全一致的服务列表,只要最终一致性即可。在跨IDC,多数据中心等场景下 consul 发挥了很大的优势,这也是很多互联网公司选择使用 consul 的原因。 eureka 是 ap 注册中心,并且是 spring cloud 默认使用的组件,spring cloud eureka 较为贴近 spring cloud 生态。
总结
注册中心主要用于解耦服务调用中的定位问题,是分布式系统必须面对的一个问题。更多专业性的对比,可以期待 spring4all.com 的注册中心专题讨论,相信会有更为细致地对比。
666. 彩蛋
如果你对 RPC 感兴趣,欢迎加入我的知识一起交流。
目前在知识星球(https://t.zsxq.com/2VbiaEu)更新了如下 Dubbo 源码解析如下:
01. 调试环境搭建
02. 项目结构一览
03. API 配置(一)之应用
04. API 配置(二)之服务提供者
05. API 配置(三)之服务消费者
06. 属性配置
07. XML 配置
08. 核心流程一览
09. 拓展机制 SPI
- 线程池
11. 服务暴露(一)之远程暴露(Injvm)
12. 服务暴露(二)之远程暴露(Dubbo)
…
一共 60 篇++