3.1 注册中心概述
在Dubbo微服务体系中,注册中心是其核心组件之一。Dubbo通过注册中心实现了分布式环境中各服务之间的注册与发现,是各个分布式节点之间的纽带。
Dubbo的注册中心源码在模块dubbo-registry中,里面包含五个子模块:
- dubbo-registry-api 注册中心的api和抽象实现类
- dubbo-registry-zookeeper 使用zookeeper作为注册中心
- dubbo-registry-redis 使用redis作为注册中心
- dubbo-registry-default 基于内存的默认实现
- dubbo-registry-multicast multicast模式的服务注册与发现
生成环境推荐使用zookeeper。
3.1.1 工作流程
- Provider启动时,会向注册中心写入自己的元数据信息,同时会订阅Config元数据信息。
- Consumer启动时,也会向注册中心写入自己的元数据信息,并订阅Provider、Router和Config元数据信息。
- 服务治理中心(dubbo-admin)启动时,会同时订阅所有Consumer、Provider、Router和Config元数据信息。
- 当有Provider离开或有新的Provider加入时,注册中心Provider目录会发生变化,变化信息会动态通知给Consumer和服务治理中心。
- 当Consumer发起服务调用,会异步将调用和统计信息上报给监控中心(dubbo-monitor-simple)。
3.1.2 Zookeeper原理概述
Zookeeper是树形结构的注册中心,节点分为持久节点、持久顺序节点、临时节点和临时顺序节点4种。
- 持久节点:服务注册后保证不会丢失,注册中心重启后也会存在。
- 持久顺序节点:在持久节点特性的基础上增加了节点先后顺序的能力。
- 临时节点:服务注册后连接丢失或session超时,注册的节点会自动被移除。
-
临时顺序节点:在临时节点特性的基础上增加了节点先后顺序的能力。
Dubbo只使用了持久节点和临时节点两种,对顺序没有需求。
/dubbo/com.foo.BarService/providers是Provider在zookeeper注册中心的路径示例,是一种树形结构,该结构分4层:root(根节点,对应示例中的dubbo)、service(接口名称,对应com.foo.BarService)、四种服务目录(对应示例中的providers,其他还包括consumers、routers、configurators)。再往后就是具体的Dubob服务URL。
树形结构的关系:
(1) 树的根节点是注册中心的分组,下面有多个服务接口,这个分组值来自用户配置<dubbo:registry>中的group树形,默认值是/dubbo。
(2) 根目录下面是service目录,service目录下包含4类子目录,分别是providers、consumers、routers、configurators,都是持久节点。
(3) providers目录下面包含有该service的多个服务者URL元数据信息。
(4) consumers目录下面包含有该service的多个消费者URL元数据信息。
(5) routers目录下面包含多个用于消费者路由策略的URL元数据信息。
(6) configurators下面包含多个用于服务者动态配置的URL元数据信息。(只用于服务者吗?貌似只是大部分吧)
在Dubbo框架启动时,会根据用户配置的服务,在注册中心创建4各目录,在providers和consumers目录分别存储服务提供方、消费方元数据信息,主要包括IP、端口、权重和应用名等数据。
在Dubbo框架进行服务调用时,用户可以通过服务治理平台(dubbo-admin)下发路由配置。如果要在运行时改变服务参数,则用户也可以通过服务治理平台下发动态配置。服务器端会通过订阅机制收到属性变更,并重新更新已经暴露的服务。
3.1.3 ZooKeeper的实现
- 发布的实现:
Provider和Consumer都需要把自己注册到注册中心。Provider的注册是为了让Consumer感知到服务的存在,从而发起远程调用;也让服务治理中心感知到有新的provider上线。Consumer的注册是为了让服务治理中心发现自己。ZooKeeper发布代码非常简单,只是调用了ZooKeeper的客户端库在注册中心上创建一个目录:
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
取消发布也很简单,只是把ZooKeeper注册中心上对应的路径删除:
zkClient.delete(toUrlPath(url));
- 订阅的实现
订阅通常有pull和push两种方式,一种是客户端定时轮询注册中心拉取配置,另一种是注册中心主动推送数据给客户端。两种方式各有利弊,目前Dubbo采用的是第一次启动拉取,后续接收事件重新拉取数据。
在服务暴露时,provider会订阅configurators用于监听动态配置,在消费端启动时,consumer会订阅providers、routers和configurators这三个目录,分别对应服务提供者、路由和动态配置变更通知。
Dubbo在dubbo-remoting-zookeeper模块中实现了ZooKeeper客户端的统一封装,定义了统一的Client API,并用两种不同的ZooKeeper开源客户端库实现了这个接口: Apache Curator和zkClient。用户可以使用<dubbo:registry>的client属性切换实现,默认使用的是curator。
ZooKeeper注册中心采用的是“事件通知”+“客户端拉取”的方式,客户端第一次连接上注册中心时,会获取对应目录的全量数据,并在订阅的节点上注册一个watcher,客户端与注册中心之间保持TCP长连接,后续每个节点有任何数据变化,注册中心都会根据watcher的回调主动通知客户端(事件通知),接到通知后,会把对应节点下的全量数据都拉取过来,这一点在NotifyListener#notify(List<URL> urls)接口上就有约束的注释说明。但全量拉取有一个局限,当微服务节点很多时会对网络造成很大的压力。
什么操作会被认为是ZooKeeper的事务操作
客户端任何新增、删除、修改、会话创建和失效操作,都会被认为是事务操作,都会由ZooKeeper中的leader执行。及时客户端连接的是非leader节点,请求也会被转发给leader执行,以此来保证所有事务的全剧时序性,由于每个节点都有一个版本号,因此可以通过CAS操作比较版本号来保证该节点数据操作的原子性
客户端第一次连上注册中心,订阅时会获取全量数据,后续通过监听器事件进行更新。服务治理中心会处理所有service层的订阅,service被设置成特殊值*。此外,服务治理中心除了订阅当前节点,还会订阅这个节点下的所有子节点,核心代码来自ZookeeperRegistry:
接下来是普通消费者的订阅逻辑,首先根据URL的类别得到一组需要订阅的路径,如果类别是*,则会订阅四种类型的路径(providers、routers、consuemrs、configurators),否则只订阅providers。
- 注意点1
zkClient.addChildListener()
会返回子节点数据列表,即首次订阅的全量拉取的表现。 - 注意点2 从代码可以看出只对第一层根节点和第3层节点进行了订阅,第二层service节点没有进行订阅,估计是因为第2层节点的子节点最多只有4种不会动态变更的原因。
- 注意点3 最后一步notify的操作,其实是获得了第4层的全量url,所以需要通知更新。如果是providers类别的数据,则订阅方会更新本地Directory管理的Invoker服务列表;如果是routers分类,则更新本地路由规则列表;如果是configurators,则更新动态参数列表。
3.2 注册中心缓存机制
如果每次远程调用都要先从注册中心获取一次可调用的服务列表,则会让注册中心承受巨大的流量压力。另外,每次额外的网络请求也会让整个系统性能下降,因此Dubbo的注册中心实现了通用的缓存机制,在抽象类AbstractRegistry中实现。
消费者或服务治理中心获取注册信息后会做本地缓存。内存中会有一份,保存在Properties对象里,磁盘上也会持久化一份文件,通过file对象引用。
内存中的缓存notified是ConcurrentHashMap里面又嵌套了一个Map,外层Map的key是消费者的URL,内层的Map的Key是分类,包含providers、consumers、routers、configurators四种。value则是对应的服务列表,对于没有服务提供者提供的URL,它会以特殊的empty://前缀开头。
翻看了下源码,个人理解是,properties和map都是内存中的缓存,properties主要是为了方便与file的交互进行直接的读入和写出,map才是运行时主要使用的。启动时,从文件加载到的properties并没有直接写入map中,而是在notify方法中才对properties和map进行同时写入的,也就是说map的数据永远来源于注册中心,刚启动的时候是空的,只有当注册中心不可用的情况下才会去使用properties。(3.2.1证明这点理解是正确的)
3.2.1 缓存的加载
在服务初始化时,AbstractRegistry构造函数里会从本地磁盘文件中把持久化的注册数据读到Properties对象里,并加载到内存缓存中。
Properties保存了所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表、路由规则列表、配置规则列表等作为value。由于value是列表,所以用空格隔开多个。还有一个特殊的key.registies,保存了所有的注册中心地址。如果应用在启动过程中,注册中心无法连接或者宕机,Dubbo会自动通过本地缓存加载Invokers。
3.2.2 缓存的保存与更新
缓存的保存有同步和异步两种方式。异步会使用线程池异步保存,如果线程在执行过程中出现异常,会再次调用线程池不断重试。
AbstractRegistry#notify方法中封装了更新内存缓存和文件缓存的逻辑。当客户端第一次订阅全量数据,或者后续由于订阅得到新数据,都会调用notify方法来保存。
3.3 重试机制
我们知道FailbackRegistry继承了AbstractRegistry,并在此基础上增加了失败重试机制。ZooKeeperRegistry直接继承FailbackRegistry接口直接使用重试机制。
FailbackRegistry抽象类定义了一个ScheduledExecutorService,每隔固定时间(默认5秒)就调用FailbackRegistry#retry()方法,另外还通过5个集合来记录:
retry方法会把这5个集合分别进行遍历和重试,重试成功就移除。FailbackRegistry实现了subscribe、unsubscribe等通用方法,里面调用了抽象的模板方法,将由子类来实现,通过这种模板方法的调用,如果捕获到异常,就会把URL添加到对应的重试集合中,以供定时器去重试。