Dubbo笔记-2-注册中心

注册中心概述

在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)

QQ图片20190814021452.jpg

数据结构

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元数据信息

QQ图片20190814024515.jpg

在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 urls)接口上就有约束的注释说明.全量拉取有一个局限,当微服务节点较多时会对网络造成很大的压力.

ZooKeeper的每个节点都有一个版本号,当某个节点的数据发生变化(即事务操作)时,该节点对应的版本号就会发生变化,并触发watcher时间,推送数据给订阅放,版本号强调的是变更次数,即使该节点的值没有变化,只要有更新操作,依然会使版本号变化.

客户端第一次脸上注册中心,订阅时会获取全量的数据,后续则通过监听器时间进行更新.服务治理中心会处理所有service层的订阅,service被设置成特殊层.此外,服务治理中心除了订阅当前节点,还会订阅这个节点下的所有子节点.

缓存机制

Dubbo的注册中心实现了通用的缓存机制,在抽象类AbstractRegistry中实现.AbstractRegistry类结构关系如图:

QQ图片20190814114202.jpg

消费者或服务治理中心获取注册信息后会做本地缓存.内存中会有一份,保存在Properties对象里,磁盘上也会持久化一份文件,通过file对象引用,在AbstractRegistry抽象类中有如下定义:

1
2
3
private final Properties properties = new Properties();
private File file;
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();

内存中缓存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
2
3
4
5
if (syncSaveFile) {
doSaveProperties(version);
} else {
registryCacheExecutor.execute(new SaveProperties(version));
}

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中使用的设计模式密不可分.

模板模式

整个注册中心的逻辑部分使用了模板模式

QQ图片20190814153948.jpg

AbstractRegistry实现了Registry接口中的注册,订阅,查询,通知等方法,还实现了磁盘文件持久化注册信息这一通用方法.但是注册,订阅,查询,通知等方法只是简单地把URL加入对应的集合,没有具体的注册或订阅逻辑.

FailbackRegistry又继承了AbstractRegistry,重写了父类的注册,订阅,查询和通知等方法,并且添加了重试机制.此外,还添加了四个未实现的抽象模板方法.

以订阅为例,FailbackRegistry重写了subscribe方法,但只实现了订阅的大体逻辑及异常处理等通用性的东西.具体如何订阅,交给继承的子类实现.这就是模板模式的具体实现.

工厂模式

所有注册中心实现,都是通过对应的工厂创建的.工厂类之间的关系:

QQ图片20190814154823.jpg

AbstractRegistryFactory实现了RegistryFactory接口的getRegistry(URL url)方法,是一个通用实现,主要完成了加锁,以及调用抽象模板方法createRegistry(URL url)创建具体实现等操作,并缓存在内存中.抽象模板方法会由具体子类继承并实现.

虽然没种注册中心都有自己具体的工厂类,但是在什么地方判断,应该调用哪个工厂类实现,要在RegistryFactory接口中判断,该接口中有一个Registry getRegistry(URL url)方法,该方法上有@Adaptive({“protocol”})注解:

1
2
3
4
5
@SPI("Dubbo")
publicinterface RegistryFactory{
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}

这个注解会自动生成代码实现一些逻辑,它的value参数会从URL中获取protocol键的值,并根据获取的值来调用不同的工厂类.如,当url.protocol = redis时,获得RedisRegistryFactor实现类.