dubbo源码学习(2)--服务注册

  Dubbo的Provider,Consumer在启动时都会创建一个注册中心,注册中心可以选择Zookeeper,Redis。常用的是Zookeeper,我们这篇博客主要讲的就是Dubbo与Zookeeper的注册交互过程。

  Dubbo里默认使用zkclient来操作zookeeper服务器,其对zookeeper原始客户单做了一定的封装,操作zookeeper时能便捷一些,比如不需要手动处理session超时,不需要重复注册watcher等等。

  分布式服务框架Dubbo中使用zookeeper来作为其命名服务,维护全局的服务地址列表。在Dubbo是实现中:服务提供者provider在启动的时候,向Zookeeper的指定节点:~/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就会完成服务的发布。一般数据存放路径为 /zookeeper-xxx/data/version-xx/log.x

  服务消费者启动的时候,订阅~/dubbo/${serviceName}/providers目录下的提供者URL地址。注意:所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。

Dubbo在Zookeeper上注册的节点目录:假设接口名称是:com.bob.dubbo.service.CityDubboService

如果注册中心集群都挂掉,发布者和订阅者之间还能通信么?
可以通信,启动dubbo时,消费者会从zk拉去注册的生产者的地址接口作为数据,缓存在本地,每次调用安装本地的缓存地址进行调用。
在具体讲解ZookeeperRegistry的相关源码之前,先来分析下dubbo在zookeeper的目录结构以及dubbo如何利用这个特性:
针对每个接口节点会存在以下4个子节点:

节点名 作用
consumers 存储消费者节点url
configuators 存储override或者absent url,用于服务治理
routers 用于设置路由url,用于服务治理
providers 存储在线提供者url

如图:

image

Dubbo启动时,Consumer和Provider都会把自身的URL格式化为字符串,然后注册到zookeeper相应节点下,作为一个临时节点,当连断开时,节点被删除。

Consumer在启动时,不仅仅会注册自身到 …/consumers/目录下,同时还会订阅…/providers目录,实时获取其上Provider的URL字符串信息。
zookeeper注册中心的源码为com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry
ZookeeperRegistry 类继承自 FailbackRegistryFailbackRegistry 又继承自 AbstractRegistryAbstractRegistry实现了 RegistryService 接口。

因此我们阅读源码顺序为:RegistryService -> AbstractRegistry -> FailbackRegistry -> ZookeeperRegistry
RegistryService 接口

image

image

AbstractRegistry 抽象类
从构造函数可以看出 AbstractRegistry 抽象类主要是提供了对注册中心数据的文件缓存。

image

Dubbo会在用户目录创建./dubbo文件夹及缓存文件,以windows为例,生成的缓存文件为:C:\Users\你的登录用户名/.dubbo/dubbo-registry-127.0.0.1.cache

FailbackRegistry 抽象类
FailbackRegistry 顾名思义是主要提供的是失败自动恢复,同样看一下构造函数,在构造函数中会通过 ScheduledExecutorService 一直执行Retry方法进行重试。

image

retry()方法主要的从各个操作中的失败列表取出失败的操作进行重试。


image

ZookeeperRegistry 类

image

同时提供了几个抽象方法


image

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 主要是实现了FailbackRegistry的那几个抽象方法。本次也主要分析 doRegister(),doSubscribe()这两个方法。

注册:

image

image

doRegister() 主要是调用zkClient创建一个节点。 create()以递归的方式创建节点,通过判断Url中dynamic=false 判断创建的是持久化节点还是临时节点。
主要做了以下几步:
1)记录注册注册地址 2) 注册节点到zookeeper上 3) 捕捉错误信息,出错则记录下来,等待定期器去重新执行

订阅

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

消费者在引用服务时,会订阅接口下的providers的节点。一旦providers下的子节点发生改变(提供者的服务器增加或者删除),会通知到消费者。消费者会把提供者的集群地址缓存到本地。

主要做了以下几步操作(以具体接口为例)

1) 将订阅信息记录到集合中 `
2) 将路径转变成/dubbo/xxService/providers,
 /dubbo/xxService/configurators 
,/dubbo/xxService/routers 循环这三个路径

如果消费者的接口没有创建过子节点监听器,那么就创建子节点监听器

创建路径节点,并将子节点监听器放入到节点上。(一旦子节点发生改变,就通知)

获取到当前路径节点下的所有子节点(提供者),将这些子节点组装成集合;
如果没有节点,那么就将消费者的地址的协议变成empty、
empty://10.118.14.204/com.test.ITestService?
application\=testservice&category\=configurators
通知
3) 出现异常,根据url从本地缓存文件中获取到提供者的地址,通知

[站外图片上传中...(image-3598a1-1550486301310)]

ZookeeperRegistry
接口是* (对所有的接口进行订阅,有点类似于递归订阅)

`1) 如果在集合中没有创建过*的子节点监听器,那么就创建子节点监听器,一旦root下的子节点(service)发生改变,那么就对这个节点就行订阅NotifyListener 。(这时就有具体的接口了)

  1. 创建root节点,将子节点监听器放入到root上。并返回root下的所有的接口,对这些接口订阅NotifyListener`

接口是具体 (以providers为例)
`1)将接口名称转变成/dubbo/com.test.ITestService/providers,集合中没有没有providers的子节点监听器,就创建子节点监听器。一旦子节点发生改变,那么就通知

  1. 创建 /dubbo/com.test.ITestService/providers,并且将子节点监听器放入到这个节点上,并返回所有的子节点(提供者),通知。`

总结
当消费者要订阅接口中的提供者时
会监听/dubbo/xxService/providers下的所有提供者。一旦提供者的节点删除或增加时,都会通知到消费者的url(consumer://10.118.14.204/com.test.ITestService…….)

它会监听以下三个节点的子节点
1) /dubbo/xxService/providers 2)/dubbo/xxService/configurators 3)/dubbo/xxService/routers
组装的url集合(即提供者的子节点providers,configurators,routers下的子节点)。如果没有子节点(没有提供者),那么就将消费者的协议变成empty作为url。

protected void doSubscribe(final URL url, final NotifyListener listener) {
    //接口名称(*代表需要监听root下面的所有节点)
    if ("*".equals(url.getServiceInterface())) {
        ConcurrentMap listeners = zkListeners.
        get(url);
        //如果listeners为空创建并放入到map中...
        ChildListener zkListener = listeners.get(listener);
        /**
           root下的子节点是service接口
           创建子节点监听器,对root下的子节点做监听,一旦有子节点发生改变,
           那么就对这个节点进行订阅.
        **/
        if (zkListener == null) {
            listeners.putIfAbsent(listener, new ChildListener() {
                public void childChanged(String parentPath, List 
                    currentChilds) {
                    for (String child : currentChilds) {
                        //如果不存在,才订阅
                        if (! anyServices.contains(child)) {
                            anyServices.add(child);
                            //订阅
                            subscribe(url.setPath(child).addParameters(
                            "interface", child,"check", "false"), listener);
                        }
                    }
                }
            });
            zkListener = listeners.get(listener);
        }
        //创建root节点
        zkClient.create(root, false);
        //添加root节点的子节点监听器,并返回当前的services
        List services = zkClient.addChildListener(root, zkListener);
        if (services != null && services.size() > 0) {
            //对root下的所有service节点进行订阅
            for (String service : services) {
                service = URL.decode(service);
                anyServices.add(service);
                subscribe(url.setPath(service).addParameters("interface", 
                service, "check", "false"), listener);
            }
        }
    } else {
        List urls = new ArrayList();
        /**将url转变成
            /dubbo/com.test.ITestService/providers
            /dubbo/com.test.ITestService/configurators
            /dubbo/com.test.ITestService/routers
        **/
        for (String path : toCategoriesPath(url)) {
            ConcurrentMap listeners = 
                zkListeners.get(url);
            //如果listeners为空就创建并放入盗map中
            ChildListener zkListener = listeners.get(listener);
            /**
              对接口下的providers的子节点进行监听,一旦发生改变,就通知
            **/
            if (zkListener == null) {
                listeners.putIfAbsent(listener, new ChildListener() {
                    public void childChanged(String parentPath, List 
                        currentChilds) {
                        //通知
                        ZookeeperRegistry.this.notify(url, listener, 
                            toUrlsWithEmpty(url, parentPath, currentChilds));
                    }
                });
                zkListener = listeners.get(listener);
            }
            //创建/dubbo/com.test.ITestService/providers
            zkClient.create(path, false);
            //获取到providers的所有子节点(提供者)
            List children = zkClient.addChildListener(path, 
                zkListener);
            //获取到所有的提供者,组装起来
            if (children != null) {
                //有子节点组装,没有那么就将消费者的协议变成empty作为url。
                urls.addAll(toUrlsWithEmpty(url, path, children));
            }
        }
        //通知/dubbo/com.test.ITestService/providers的所有子节点
        notify(url, listener, urls);
    }

 }

/**
根据url获取到哪些类型
consumer://10.118.14.204/com.test.ITestService?application=testservice
&category=providers,configurators,routers&...
这里的category是重点
**/
private String[] toCategoriesPath(URL url) {
    String[] categroies;
    //如果是*
    if ("*".equals(url.getParameter(Constants.CATEGORY_KEY))) 
        categroies = new String[] {"providers", "consumers", 
            "routers", "configurators"}; 
    else
    //从url获取到category的值,没有的话就默认providers
        categroies = url.getParameter("category", 
            new String[] {"providers"});
    String[] paths = new String[categroies.length];
    //将格式转变成/dubbo/xxService/类型
    for (int i = 0; i < categroies.length; i ++) {
        paths[i] = toServicePath(url) + "/" + categroies[i];
    }
    return paths;
}

/**
组装providers、routers、configurators下的url。
如果有提供者那么就组装;没有的话,就将消费者的协议变成empty
**/
private List toUrlsWithEmpty(URL consumer, String path, List providers) {
        List urls = toUrlsWithoutEmpty(consumer, providers);
        if (urls.isEmpty()) {
            int i = path.lastIndexOf('/');
            String category = i < 0 ? path : path.substring(i + 1);
            URL empty = consumer.setProtocol("empty").addParameter(
                "category", category);
            urls.add(empty);
        }
        return urls;
    }

通知
有三个参数
url: 消费者的地址 consumer://10.118.14.204/com….. listener: 监听器 urls: providers,configurators和routers

FailbackRegistry
主要是铺捉到异常时放入到集合中,定时重试

image

AbstractRegistry

urls三种
1) providers(providers下的子节点) dubbo://10.118.22.29:20710/com.test.ITestService?anyhost=true&application=testservice&default.cluster=failfast…

2) configurators(configurators下的子节点为空,将消费者的url变成empty ) empty://10.118.14.204/com.test.ITestService?application=testservice&category=configurators&default.check=false..

3) routers(routers下的子节点为空,将消费者的url变成empty) empty://10.118.14.204/com.test.ITestService?application=testservice&category=routers&default.check=false..

image

保存到本地缓存文件
组装url保存到properties中,如果是同步,直接保存到本地缓存文件中,否则文件缓存定时写入

image

首先会有个dubbo-registry-10.118.22.25.cache.lock,会获取这个文件的锁,然后保存dubbo-registry-10.118.22.25.cache文件,再释放锁。


    public void doSaveProperties(long version) {
        if (version < lastCacheChanged.get()) {
            return;
        }
        if (file == null) {
            return;
        }
        Properties newProperties = new Properties();
        // 保存之前先读取一遍,防止多个注册中心之间冲突
        InputStream in = null;
        try {
            if (file.exists()) {
                in = new FileInputStream(file);
                newProperties.load(in);
            }
        } catch (Throwable e) {
            logger.warn("Failed to load registry store file, cause: " + e.getMessage(), e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        // 保存
        try {
            newProperties.putAll(properties);
            File lockfile = new File(file.getAbsolutePath() + ".lock");
            if (!lockfile.exists()) {
                lockfile.createNewFile();
            }
            RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
            try {
                FileChannel channel = raf.getChannel();
                try {
                    FileLock lock = channel.tryLock();
                    if (lock == null) {
                        throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
                    }
                    // 保存
                    try {
                        if (!file.exists()) {
                            file.createNewFile();
                        }
                        FileOutputStream outputFile = new FileOutputStream(file);
                        try {
                            newProperties.store(outputFile, "Dubbo Registry Cache");
                        } finally {
                            outputFile.close();
                        }
                    } finally {
                        lock.release();
                    }
                } finally {
                    channel.close();
                }
            } finally {
                raf.close();
            }
        } catch (Throwable e) {
            if (version < lastCacheChanged.get()) {
                return;
            } else {
                registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
            }
            logger.warn("Failed to save registry store file, cause: " + e.getMessage(), e);
        }
    }

监听器通知 (当收到服务变更通知时触发。)
当收到提供者的地址发生改变时,这时刷新缓存中的invoker,如果url不存在,那么重新refer(根据dubbo协议)

通知需处理契约: 
1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。 
2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。 
3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routers, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。 
4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。 
5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。

你可能感兴趣的:(dubbo源码学习(2)--服务注册)