Dubbo源码学习五--Zookeeper注册中心

1.Dubbo注册中心下主要包括的模块

Dubbo注册中心下的模块.png

可以看到在Dubbo的注册中心下面包含了众多实现方式:

1.dubbo-registry-api:包含了注册中心所有的api和抽象实现类
2.dubbo-registry-consul:基于consul实现的注册中心
3.dubbo-registry-default:Dubbo:基于内存默认实现的注册中心
4.dubbo-registry-etcd3 : 结余etcd3实现的注册中心
5.dubbo-registry-multicast: multicast模式的注册中心
6.dubbo-registry-multiple:
7.dubbo-registry-nacos:基于SpringCloud-Alibaba nacos组件的注册中心。
8.dubbo-registry-sofa: 基于阿里巴巴sofa的注册中心。
9.dubbo-registry-zookeeper:基于zookeeper的注册中心。

由于目前自己用的是Zookeeper作为Dubbo的注册中心,着重分析下Zookeeper注册中心的实现原理。

2.dubbo-registry-zookeeper源码分析

1.源码结构:

registry-zookeeper的结构.png

通过查看源码的目录结构可以看到,有两个实现类,一个是ZookeeperRegistry另一个是ZookeeperRegistryFactory,通过Idea的代码分析工具查看这两个类的继承和实现结构:


ZookeeperRegistry类继承结构.png

ZookeeperRegistry继承自FallbackRegistry,FallbacRegistry继承自AbstractRegistry,而AabstractRegistry又实现了Registry接口,Registry接口又继承自RegistryService接口和Node接口。

ZookeeperRegistryFactory继承结构图.png

ZookeeperRegistryFactory继承自AbstractRegistry,AbstractRegistryFactory继承自RegistryFactory。

通过以上的梳理,我们可以看出,zookeeper下的两个类分别实现了dubbo-registry-api下面的接口,那么接下来从dubbo-registry-api下面一点点进行分析。

2.dubbo-registry-api结构及代码分析

registry-api的整体结构图.png

由于Dubbo支持多种注册中心,具体的实现方式在第一节Dubbo注册中心包含的模块下有详细的分析,这些实现方式都依赖于support包下面的类,而support包下面的有些类又实现了Registry接口和RegistryFactory接口。在dubbo-registry-api包下有几个重要的接口,分别是:

Registry:

package org.apache.dubbo.registry;

import org.apache.dubbo.common.Node;
import org.apache.dubbo.common.URL;

/**
* Registry. (SPI, Prototype, ThreadSafe)
*
* @see org.apache.dubbo.registry.RegistryFactory#getRegistry(URL)
* @see org.apache.dubbo.registry.support.AbstractRegistry
*/
public interface Registry extends Node, RegistryService {
}

该接口没有自己的实现,继承了Node和RegistryService,主要把节点和注册中心服务的方法进行了整合。

Node的源码:

package org.apache.dubbo.common;

/**
 * Node. (API/SPI, Prototype, ThreadSafe)
 */
public interface Node {

    /**
     * get url. 获取节点地址
     *
     * @return url.
     */
    URL getUrl();

    /**
     * is available.  判断节点是否可用
     *
     * @return available.
     */
    boolean isAvailable();

    /**
     * destroy.  销毁节点
     */
    void destroy();

}

RegistryService:

主要封装了注册、取消注册、订阅、取消订阅以及查询URL地址等方法。

package org.apache.dubbo.registry;

import org.apache.dubbo.common.URL;

import java.util.List;


public interface RegistryService {
        /**
     * 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则,等数据。
     * 
     * 注册需处理契约:
     * 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。
     * 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。
     * 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。
     * 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。
     * 5. 允许URI相同但参数不同的URL并存,不能覆盖。
     * 
     * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     */
    void register(URL url);
    /**
     * 取消注册.
     * 
     * 取消注册需处理契约:
     * 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。
     * 2. 按全URL匹配取消注册。
     * 
     * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     */
    void unregister(URL url);
    /**
     * 订阅符合条件的已注册数据,当有注册数据变更时自动推送.
     * 
     * 订阅需处理契约:
     * 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。<br>
     * 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。
     * 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0
     * 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*
     * 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。
     * 6. 允许URI相同但参数不同的URL并存,不能覆盖。
     * 7. 必须阻塞订阅过程,等第一次通知完后再返回。
     * 
     * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @param listener 变更事件监听器,不允许为空
     */
    void subscribe(URL url, NotifyListener listener);
     /**
     * 取消订阅.
     * 
     * 取消订阅需处理契约:
     * 1. 如果没有订阅,直接忽略。
     * 2. 按全URL匹配取消订阅。
     * 
     * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @param listener 变更事件监听器,不允许为空
     */
    void unsubscribe(URL url, NotifyListener listener);
   /**
     * 查询符合条件的已注册数据,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
     * 
     * @see com.alibaba.dubbo.registry.NotifyListener#notify(List)
     * @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @return 已注册信息列表,可能为空,含义同{@link com.alibaba.dubbo.registry.NotifyListener#notify(List<URL>)}的参数。
     */
    List<URL> lookup(URL url);

}

RegistryFactory:

该接口是注册中心的工厂接口,用来返回注册中心的对象。

package org.apache.dubbo.registry;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

/**
 * RegistryFactory. (SPI, Singleton, ThreadSafe)
 *
 * @see org.apache.dubbo.registry.support.AbstractRegistryFactory
 */
@SPI("dubbo")
public interface RegistryFactory {

    /**
     * Connect to the registry
     * <p>
     * Connecting the registry needs to support the contract: <br>
     * 1. When the check=false is set, the connection is not checked, otherwise the exception is thrown when disconnection <br>
     * 2. Support username:password authority authentication on URL.<br>
     * 3. Support the backup=10.20.153.10 candidate registry cluster address.<br>
     * 4. Support file=registry.cache local disk file cache.<br>
     * 5. Support the timeout=1000 request timeout setting.<br>
     * 6. Support session=60000 session timeout or expiration settings.<br>
     *
     * @param url Registry address, is not allowed to be empty
     * @return Registry reference, never return empty value
     */
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);

}

NotifyListener:

该接口只有一个notiofy方法,通知监听器,当服务变更时触发通知。

import org.apache.dubbo.common.URL;

import java.util.List;

/**
 * NotifyListener. (API, Prototype, ThreadSafe)
 *
 * @see org.apache.dubbo.registry.RegistryService#subscribe(URL, NotifyListener)
 */
public interface NotifyListener {

    /**
      当收到服务变更通知时触发。
     * <p>
     * 通知需处理契约:<br>
     * 1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。<br>
     * 2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。<br>
     * 3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routers, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。<br>
     * 4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。<br>
     * 5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。<br>
     *
     * @param urls 已注册信息列表,总不为空,含义同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
     */
    void notify(List<URL> urls);
}

以上几个接口为registry-api的主要接口,提供了返回注册中心的工厂,同时也提供了注册,订阅,取消订阅,取消注册以及服务变更时进行通知等方法。

3.回头继续分析ZookeeperRegistry

通过分析registry-api源码中的接口,我们可以得知服务注册的一些基本的原理,以及一些基本的方法。每一种注册方式都有自己的实现具体实现,而在dubbo-registry-zookeeper中只有两个类,一个是ZookeeperRegistry另一个是ZookeeperRegistryFactory,其中ZookeeperReistry才是实现dubbo服务向注册中心进行注册的一个桥梁。

根据上图ZookeeperRegistry的继承顺序,我们可以按照从上往下的顺序进行分析,依次是RegistryService--->AbstractRegistry-->FailBackRegistry-->ZookeeperRegistry。

1. RegistryService:上面已经进行分析,主要封装了一些订阅、取消订阅等方法。

2. AbstractRegistry:

AbstractRegistry构造方法的源码:

public AbstractRegistry(URL url) {
        //1. 设置配置中心的地址
        setUrl(url);
        // Start file save timer
        //2. 配置中心的URL中是否配置了同步保存文件属性,否则默认为false
        syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
        //3. 配置信息本地缓存的文件名
        String filename = url.getParameter(FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
        File file = null;
        //逐层创建文件目录
        if (ConfigUtils.isNotEmpty(filename)) {
            file = new File(filename);
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {
                    throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
                }
            }
        }
        this.file = file;
        // When starting the subscription center,
        // we need to read the local cache file for future Registry fault tolerance processing.
        //如果现有配置缓存,则从缓存文件中加载属性
        loadProperties();
        notify(url.getBackupUrls());
    }

在Dubbo服务提供者和调用者启动的过程到那个中都有一个查看本地是否缓存注册中心地址的过程,如果没有缓存直接从注册中心获取。那么AbstractRegistry最主要的一个作用就是将没有缓存的URL缓存到本地,如果有配置缓存,则直接从缓存文件中加载属性,从缓存文件中加载属性使用的是
loadProperties()方法,该方法的具体实现如下:

private void loadProperties() {
        //当本地存在配置缓存文件时
        if (file != null && file.exists()) {
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                //读取配置文件的内容,并加载为properties的键值对存储
                properties.load(in);
                if (logger.isInfoEnabled()) {
                    logger.info("Load registry cache file " + file + ", data: " + properties);
                }
            } catch (Throwable e) {
                logger.warn("Failed to load registry cache file " + file, e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        logger.warn(e.getMessage(), e);
                    }
                }
            }
        }
    }

下面运行一下Dubbo提供的示例代码体验一下在没有缓存文件时写缓存文件的过程和在缓存文件存在时读取缓存文件的一个过程。

首先进入本地缓存url文件的目录:


zookeeper缓存文件目录.png
已经存在的缓存文件.png

由于之前运行过项目,所以该目录下会存在缓存文件,现将缓存文件全部删除运行dubbo服务提供者,然后查看日志:


没有缓存文件时启动.png

自动添加缓存文件.png

通过上图可以看到,没有输出 "Load registry cache file"相关的日志,并且已经全部删除的存放缓存文件的目录也已经有了缓存文件。查看下缓存文件中的内容:


Dubbo缓存文件内容.png

再次重启dubbo服务观察启动日志:


加载已经存在的缓存文件.png

3. FailBackRegistry

构造函数如下:
主要提供失败自动恢复,同时提供了一系列的失败重试调用的方法

public FailbackRegistry(URL url) {
        super(url);
        //获取重试的周期
        this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);

        // since the retry task will not be very much. 128 ticks is enough.
      //定期检查是否有失败请求,如果有失败请求则一直进行重试
        retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
    }
失败重试调用的方法.png

除此之外提供了几个模板方法:

    public abstract void doRegister(URL url);

    public abstract void doUnregister(URL url);

    public abstract void doSubscribe(URL url, NotifyListener listener);

    public abstract void doUnsubscribe(URL url, NotifyListener listener);

4. ZookeeperRegistry

Zookeeper主要采用树状结构来存储节点,Dubbo的各层存放在Zookeeper的不同节点:


Zookeeper树状结构图.jpeg

Zookeeper的各节点存放信息:

1.dubbo的Root层是根目录,通过<dubbo:registry group="dubbo" />的“group”来设置zookeeper的根节点,缺省值是“dubbo”。
2.Service层是服务接口的全名。
3.Type层是分类,一共有四种分类,分别是providers(服务提供者列表)、consumers(服务消费者列表)、routes(路由规则列表)、configurations(配置规则列表)。
4.URL层:根据不同的Type目录:可以有服务提供者 URL 、服务消费者 URL 、路由规则 URL 、配置规则 URL 。不同的Type关注的URL不同。

** ZookeeperRegistry的流程**

服务提供者启动时
向/dubbo/com.foo.BarService/providers目录下写入自己的URL地址。
服务消费者启动时
订阅/dubbo/com.foo.BarService/providers目录下的提供者URL地址。
并向/dubbo/com.foo.BarService/consumers目录下写入自己的URL地址。

监控中心启动时
订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL地址。

ZookeeperRegistry主要实现了AbstractRegistry的几个抽象方法,其中主要的是注册和发布方法:

doRegistry()方法:

 public void doRegister(URL url) {
        try {
          /**调用zookeeper客户端创建节点*/
            zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }


     /**create()方法*/
    public void create(String path, boolean ephemeral) {
        if (!ephemeral) {
            if (checkExists(path)) {
                return;
            }
        }
    /**根据是否含有'/'判断创建临时节点还是永久节点,其中服务提供者和调用者暴露出来的URL地址为临时节点。*/
        int i = path.lastIndexOf('/');
        if (i > 0) {
            create(path.substring(0, i), false);
        }
        if (ephemeral) {
            createEphemeral(path);
        } else {
            createPersistent(path);
        }
    }

doSubscribe方法
订阅Zookeeper节点是通过创建ChildListener来实现的具体调用的方法是 addChildListener()
addChildListener()又调用 AbstractZookeeperClient.addTargetChildListener()然后调用subscribeChildChanges()
最后调用ZkclientZookeeperClient ZkClientd.watchForChilds()

protected void doSubscribe(final URL url, final NotifyListener listener) {
        //....
        List<String> children = zkClient.addChildListener(path, zkListener);
        //.....
    }
    public List<String> addChildListener(String path, final ChildListener listener) {
        //......
        return addTargetChildListener(path, targetListener);
    }
    ```java
    public List<String> addTargetChildListener(String path, final IZkChildListener listener) {
        return client.subscribeChildChanges(path, listener);
    }
    public List<String> subscribeChildChanges(String path, IZkChildListener listener) {
        //.....
        return watchForChilds(path);
    }

Dubbo系列文章一--Dubbo重点掌握模块
Dubbo系列文章二--配置文件加载过程
Dubbo系列文章三--Dubbo源码结构及实现方
Dubbo系列文章四--Dubbo服务暴露机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容