注册中心概述
在Dubbo微服务体系中,注册中心是其核心组件之一.Dubbo通过注册中心实现了分布式环境中各服务之间的注册与发现,是各个分布式节点之间的纽带.
主要作用
- 动态注册
一个服务提供者通过注册中心可以动态地把自己暴露给其他消费者,无需消费者朱歌曲更新配置文件. - 动态发现
一个消费者可以动态地感知新的配置,路由规则和服务提供者,无需重启服务. - 动态调整
注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点. - 统一配置
避免了本地配置导致每个服务配置不一致的问题
模块
Dubbo注册中心源码在Dubbo-registry中,其包含五个子模块:
模块名称 | 模块介绍 |
---|---|
Dubbo-registry-api | 包含了注册中心所有的API和抽象实现类 |
Dubbo-registry-zookeeper | 使用ZooKeeper作为注册中心的实现 |
Dubbo-registry-redis | 使用Redis作为注册中心的实现 |
Dubbo-registry-default | Dubbo基于内存的默认实现 |
Dubbo-registry-multicast | multicast模式的服务注册与发现 |
工作流程
- 服务提供者启动
会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息 - 消费者启动
也会想注册中心写入自己的元数据,并订阅服务提供者,路由和配置元数据信息 - 服务治理中心(Dubbo-admin)启动
会同时订阅所有消费者,服务提供者,路由和配置元数据信息 - 当有服务提供者离开或有新的服务提供者加入
注册中心服务提供者目录会发生变化,变化信息会动态通知给消费者,服务治理中心 - 当消费者发起服务调用
会异步将调用,统计信息等上报给监控中心(Dubbo-monitor-simple)
数据结构
ZooKeeper
ZooKeeper是树形结构注册中心
节点类型
- 持久节点
服务注册后保证借点不会丢失,注册中心重启也会存在 - 持久顺序节点
在持久节点的基础上增加了节点先后顺序的能力 - 临时节点
服务注册后连接丢失或session超时,注册的节点会自动被溢出 - 临时顺序节点
在临时节点特性的基础上增加了节点先后顺序的能力
Dubbo使用ZooKeeper作为注册中心时,只会创建持久节点和临时节点两种,对顺序没有要求
概述
/Dubbo/com.foo.BarService/providers
是服务提供者在ZooKeeper注册中心的一个示例,是一种属性结构,该结构分为4层:
- root
根节点,对应示例中的Dubbo - service
接口名称,对应示例中的com.foo.BarService - 四种服务目录
对应实例中的providers,其他目录还有consumers,routers,configurators
树形结构的关系
- 树的根节点是注册中心分组
下面有多个服务接口,分组值来自用户配置Dubbo:registry中的group属性,默认是/Dubbo - 服务接口下包含4类子目录
分别是providers,consumers,routers,configurators,这个路径是持久节点 - 服务提供者目录
(/Dubbo/service/providers) 下面包含的接口有多个服务者URL元数据信息 - 服务消费者目录
(/Dubbo/service/consumers) 下面包含的接口有多个消费者URL元数据信息 - 路由配置目录
(/Dubbo/service/routers) 下面包含多个用于消费者路由策略URL元数据信息 - 动态配置目录
(/Dubbo/service/configurator) 下面包含多个用于服务者动态配置URL元数据信息
在Dubbo框架启动时,会根据用户配置的服务,在注册中心中创建4个目录,在providers和consumers目录中分别存储服务提供方,消费方元数据信息,主要包括IP,端口,权重和应用名等数据.
在Dubbo框架进行服务调用时,用户可以通过服务治理平台(Dubbo-admin)下发路由配置.如果要在运行时改变服务参数,则用户可以通过服务治理平台下发动态配置.服务端会通过订阅机制收到属性变更,并重新更新已经暴露的服务.
目录名称 | 存储值样例 |
---|---|
/Dubbo/service/providers | Dubbo://192.168.0.1.20880/com.alibaba.demo.Service?key=value&… |
/Dubbo/service/consumers | Dubbo://192.168.0.1.5002/com.alibaba.demo.Service?key=value&… |
/Dubbo/service/routers | condition://0.0.0.0/com.alibaba.demo.Service?category=routers&key=value&… |
/Dubbo/service/configurators | override://0.0.0.0/com.alibaba.demo.Service?category=configurators&key=value&… |
服务元数据中的所有参数都是以键值对形式存储的.
订阅/发布
发布的实现
服务提供者和消费者都需要把自己注册到注册中心.服务提供者的注册是为了让消费者感知服务的存在,从而发起远程调用;也让治理中心感知有新的服务提供者上线.消费者的发布是为了让服务治理中心发现自己.Dubbo中ZooKeeper发布代码非常简单,只是调用了ZooKeeper的客户端库在注册中心上创建一个目录
创建目录:
1 | zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true)); |
删除目录:
1 | zkClient.delete(toUrlPath(url)); |
订阅的实现
订阅通常有pull和push两种方式,一种是客户端定时轮询注册中心拉取配置,另一种是注册中心主动推送数据给客户端.这两种方式各有利弊,目前Dubbo采用的是第一次启动拉取方式,后续接收事件重新拉取数据
在服务暴露时,服务端会订阅configurators用于监听动态配置,在消费端启动时,消费端会订阅providers,routers和configurators这三个目录,分别对应服务提供者,路由和动态配置变更通知.
ZooKeeper注册中心采用的是"事件通知" + "客户端拉取"的方式,客户端再第一次连接上注册中心时,会获取对应目录下全量数据,并在订阅的节点上注册一个watcher,客户端与注册中心之间保持TCP长连接,后续每个节点有任何数据变化的时候,注册中心会根据watcher的回调主动通知客户端(事件通知),客户端接到通知后,会把对应节点下的全量数据都拉取过来(客户端拉取).这一点在NotifyListener#notify(List
ZooKeeper的每个节点都有一个版本号,当某个节点的数据发生变化(即事务操作)时,该节点对应的版本号就会发生变化,并触发watcher时间,推送数据给订阅放,版本号强调的是变更次数,即使该节点的值没有变化,只要有更新操作,依然会使版本号变化.
客户端第一次脸上注册中心,订阅时会获取全量的数据,后续则通过监听器时间进行更新.服务治理中心会处理所有service层的订阅,service被设置成特殊层.此外,服务治理中心除了订阅当前节点,还会订阅这个节点下的所有子节点.
缓存机制
Dubbo的注册中心实现了通用的缓存机制,在抽象类AbstractRegistry中实现.AbstractRegistry类结构关系如图:
消费者或服务治理中心获取注册信息后会做本地缓存.内存中会有一份,保存在Properties对象里,磁盘上也会持久化一份文件,通过file对象引用,在AbstractRegistry抽象类中有如下定义:
1 | private final Properties properties = new Properties(); |
内存中缓存notified是ConcurrentHashMap里面又嵌套了一个Map,外层Map的key是消费者的URL,内存Map的key是分类,包含providers,consumers,routes,configurators四种.value则是对应的服务列表,对于没有服务提供者提供服务的URL,它会以特殊的empty://前缀开头
缓存的加载
在服务初始化的时候,AbstractRegistry构造函数会从本地磁盘文件中把持久化的注册数据读到Properties对象里,并加载到内存缓存中
Properties保存了所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表,路由规则列表,配置规则列表等作为value.由于value是列表,当存在多个的时候使用空格隔开.还有一个特殊的key.registies,保存所有的注册中心地址.如果在应用启动过程中,注册中心无法连接或宕机,则Dubbo框架会自动通过本地缓存加载Invokers.
缓存的保存与更新
缓存的保存有同步和异步两种方式.异步会使用线程池异步保存,如果线程在执行过程中出现异常,则会再次调用线程池不断重试
1 | if (syncSaveFile) { |
AbstractRegistry#notify方法中封装了更新内存缓存和更新文件缓存的逻辑.当客户端第一次订阅获取全量数据,或者后续由于订阅得到新数据时,都会调用该方法进行保存.
重试机制
com.alibaba.Dubbo.registry.support.FailbackRegistry继承了AbstractRegistry,并在此基础上增加了失败重试机制作为抽象能力.zookeeperRegistry和RedisRegistry继承该抽象方法后直接使用即可.
FailbackRegistry抽象类中定义了一个ScheduledExecutorService,每经过固定间隔,调用FailbackRegistry.retry()方法.另外,该抽象类中海油五个较为重要的集合:
集合名称 | 集合介绍 |
---|---|
Set failedRegistered | 发起注册失败的URL集合 |
Set failedUnregistered | 取消注册失败的URL集合 |
ConcurrentMap>failedSubscribed | 发起订阅失败的监听器集合 |
ConcurrentMap>failedUnsubscribed | 取消订阅失败的监听器集合 |
ConcurrentMap>>failedNotified | 通知失败的URL集合 |
在定时器中调用retry方法的时候,会把这五个集合分别遍历和重试,重试成功则从集合中移除.FailbackRegistry实现了subscribe,unsubscribe等通用方法,里面调用了未实现的模板方法,会由子类实现.通过方法会调用这些模板方法,如果捕获到异常,则会把URL添加到对应的重试集合中,以供定时器去重试
设计模式
Dubbo注册中心具有良好的扩展性,用户可以在其基础上快速开发出符合自己业务需求的注册中心.
这种扩展性与Dubbo中使用的设计模式密不可分.
模板模式
整个注册中心的逻辑部分使用了模板模式
AbstractRegistry实现了Registry接口中的注册,订阅,查询,通知等方法,还实现了磁盘文件持久化注册信息这一通用方法.但是注册,订阅,查询,通知等方法只是简单地把URL加入对应的集合,没有具体的注册或订阅逻辑.
FailbackRegistry又继承了AbstractRegistry,重写了父类的注册,订阅,查询和通知等方法,并且添加了重试机制.此外,还添加了四个未实现的抽象模板方法.
以订阅为例,FailbackRegistry重写了subscribe方法,但只实现了订阅的大体逻辑及异常处理等通用性的东西.具体如何订阅,交给继承的子类实现.这就是模板模式的具体实现.
工厂模式
所有注册中心实现,都是通过对应的工厂创建的.工厂类之间的关系:
AbstractRegistryFactory实现了RegistryFactory接口的getRegistry(URL url)方法,是一个通用实现,主要完成了加锁,以及调用抽象模板方法createRegistry(URL url)创建具体实现等操作,并缓存在内存中.抽象模板方法会由具体子类继承并实现.
虽然没种注册中心都有自己具体的工厂类,但是在什么地方判断,应该调用哪个工厂类实现,要在RegistryFactory接口中判断,该接口中有一个Registry getRegistry(URL url)方法,该方法上有@Adaptive({“protocol”})注解:
1 |
|
这个注解会自动生成代码实现一些逻辑,它的value参数会从URL中获取protocol键的值,并根据获取的值来调用不同的工厂类.如,当url.protocol = redis时,获得RedisRegistryFactor实现类.