dubbo分析-ZookeeperRegistry

本文波妞主要告诉大家,dubbo服务是以啥形式存在zookeeper上的,且是如何工作的

一:demo运行

  1. 启动本地zookeeper
    1. 下载安装zookeeper,软件下载地址:http://mirror.bit.edu.cn/apache/zookeeper/
    2. 解压后,进入到zookeeper目录,进入conf目录,会看到一个 zoo_simple.cfg文件,重命名为zoo.cfg
    3. 进入bin目录,执行 sh zkServer.sh start
    4. zookeeper就启动了
  2. 运行dubbo-demo-api-provider(org.apache.dubbo.demo.provider.Application#main)
  3. 运行dubbo-demo-api-consumer(org.apache.dubbo.demo.consumer.Application#main)
  4. 看到

     这样一个简单的demo已运行成功,且服务调用成功

二:服务在zookeeper如何存储

接下来,我们顺着这个zookeeper来看下这个服务在zookeeper这个注册中心是以怎样的形式存在的

  1. 进入刚刚zookeeper的bin
  2. 然后 ls /  可以看到

     

     根目录下有两个节点:dubbi、zookeeper

  3. ls /dubbo

     

     我们运行的demo服务已经在上面了

  4. 继续进一步看里面到底是啥

     

     可以看到有四个节点:consumers、configurators、routers、providers;而且 这四个节点都是持久化的节点

     

     

     

     //todo 补充 zookeeper节点如何识别是否为持久化节点

  5. 节点consumers

     

    而当dubbo-demo-api-consumer运行结束后看consumers节点,空空如也 

     

     

  6.  

     provider节点为我们的服务提供者的信息,也就是URL的元数据,

  7.  routers为消费者路由策略,URL元数据等
  8. cinfigurators包含多个用于服务这动态配置URL元数据信息

总结下就是

 

 

三.基于zookeeper的注册中心是如何实现的

核心代码为org.apache.dubbo.registry.zookeeper.ZookeeperRegistry,那以这个为起点进行分析dubbo-registry

类图

 

来,上源码

属性和构造方法

private final static Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);

    /**
     * 默认zookeeper的跟节点 dubbo
     */
    private final static String DEFAULT_ROOT = "dubbo";

    /**
     * 根节点
     */
    private final String root;

    /**
     * Service 接口全名集合。
     * 该属性适可用于监控中心,订阅整个Service层。因为Service层是动态的,可以有不断有新的Service服务发布(注意,不是服务实例)。
     * 在 #doSubscribe(url, notifyListener) 方法中,
     */
    private final Set<String> anyServices = new ConcurrentHashSet<>();

    /**
     * 监听器集合
     */
    private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<>();

    /**
     * zookeeper客户端
     */
    private final ZookeeperClient zkClient;

    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        super(url);
        if (url.isAnyHost()) {
            throw new IllegalStateException("registry address == null");
        }
        //获取根节点,不设置则使用/dubbo
        String group = url.getParameter(GROUP_KEY, DEFAULT_ROOT);
        if (!group.startsWith(PATH_SEPARATOR)) {
            group = PATH_SEPARATOR + group;
        }
        this.root = group;
        //获取zk客户端调用 ZookeeperTransporter#connect(url) 方法,
        // 基于 Dubbo SPIAdaptive 机制,根据url参数,加载对应的 ZookeeperTransporter 实现类,创建对应的 ZookeeperClient 实现类的对应。
        zkClient = zookeeperTransporter.connect(url);
        //设置监听器
        zkClient.addStateListener((state) -> {
            //重连,重新获取最新的服务提供方地址信息
            if (state == StateListener.RECONNECTED) {
                logger.warn("Trying to fetch the latest urls, in case there're provider changes during connection loss.\n" +
                        " Since ephemeral ZNode will not get deleted for a connection lose, " +
                        "there's no need to re-register url of this instance.");
                ZookeeperRegistry.this.fetchLatestAddresses();
            } else if (state == StateListener.NEW_SESSION_CREATED) {
                //重新创建session时,将本服务进行recover恢复,重新发起注册和订阅
                logger.warn("Trying to re-register urls and re-subscribe listeners of this instance to registry...");
                try {
                    ZookeeperRegistry.this.recover();
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            } else if (state == StateListener.SESSION_LOST) {
                logger.warn("Url of this instance will be deleted from registry soon. " +
                        "Dubbo client will try to re-register once a new session is created.");
            } else if (state == StateListener.SUSPENDED) {

            } else if (state == StateListener.CONNECTED) {

            }
        });
    }

注册registry

@Override
    public void doRegister(URL url) {
        try {
            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);
        }
    }

看到 非常的简单,根据url对象构建一个url的Path,然后在zk上创建一个节点,节点是否是持久化的根据dynamic参数决定,true表示创建一个持久化节点,当注册方下线时,持久化节点仍然在zk上

path咋构建的呢:

private String toUrlPath(URL url) {
        return toCategoryPath(url) + PATH_SEPARATOR + URL.encode(url.toFullString());
    }

private String toCategoryPath(URL url) {
        return toServicePath(url) + PATH_SEPARATOR + url.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
    }

private String toServicePath(URL url) {
        String name = url.getServiceInterface();
        if (ANY_VALUE.equals(name)) {
            return toRootPath();
        }
        return toRootDir() + URL.encode(name);
    }
private String toRootDir() {
    if (root.equals(PATH_SEPARATOR)) {
        return root;
    }
    return root + PATH_SEPARATOR;
}

 

也就是Root/ServiceInterface/category/URL

这个回想看最上面那个demo在zookeeper上的存储就能理解啥意思了

category表示类别:也就是

 

 providers、consumers、configurators、routers等这些

下线doUnregister

@Override
    public void doUnregister(URL url) {
        try {
            zkClient.delete(toUrlPath(url));
        } catch (Throwable e) {
            throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

也就是把注册的节点进行删除

订阅doSubscribe

订阅有两种方式pull、push ,pull是客户端定时轮询注册中心拉取配置,push是注册中心主动推送数据给客户端。dubbo是两者结合的方式进行配置的拉取,当第一次启动的时候通过客户端pull拉取所有数据方式,在订阅的节点上进行注册watcher,然后客户端与注册中心保持长连接,而后 每个节点有任何数据变化,注册中心就会更新watcher情况进行回调通知到客户端,客户端再接收到这个通知了

服务在进行注册的时候,服务端会订阅configurators用来监听动态配置的变更

在消费者启动的时候,消费者会监听providers、routers、configurators来监听服务提供者、路由规则、配置变更的通知

@Override
    public void doSubscribe(final URL url, final NotifyListener listener) {
        try {
            //处理所有service的订阅,
            //客户端第一次连接上注册中心的时候,会把这个service设置成*  也就是 ANY_VALUE,进行获取全量数据
            if (ANY_VALUE.equals(url.getServiceInterface())) {
                String root = toRootPath();
                //获取url的监听器
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                if (listeners == null) {
                    //没有listener则创建一
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
                    listeners = zkListeners.get(url);
                }
                //获取ChildListener
                ChildListener zkListener = listeners.get(listener);
                //第一次连接时为空,新建一个childListener
                if (zkListener == null) {
                    listeners.putIfAbsent(listener, (parentPath, currentChilds) -> {
                        //遍历所有子节点
                        for (String child : currentChilds) {
                            child = URL.decode(child);
                            //有新增服务,则发起这个服务的订阅
                            if (!anyServices.contains(child)) {
                                anyServices.add(child);
                                subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
                                        Constants.CHECK_KEY, String.valueOf(false)), listener);
                            }
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                //创建持久化节点,订阅持久化节点的子节点
                zkClient.create(root, false);
                List<String> services = zkClient.addChildListener(root, zkListener);
                //获取到全量service,对每一个service进行订阅
                if (CollectionUtils.isNotEmpty(services)) {
                    for (String service : services) {
                        service = URL.decode(service);
                        anyServices.add(service);
                        subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
                                Constants.CHECK_KEY, String.valueOf(false)), listener);
                    }
                }
            } else {//客户端非第一次连接上注册中心,对于具体的服务进行订阅
                List<URL> urls = new ArrayList<>();
                //根据URL类别,获取一组需要订阅的路径
                //类别:providers、routers、consumers、configurators
                for (String path : toCategoriesPath(url)) {
                    //获取url对应的监听器,没有则创建
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                    if (listeners == null) {
                        zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
                        listeners = zkListeners.get(url);
                    }
                    //获取ChildListener,没有则创建
                    ChildListener zkListener = listeners.get(listener);
                    if (zkListener == null) {
                        //子节点数据发生变更时则调用notify方法进行通知这个listener
                        listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
                        zkListener = listeners.get(listener);
                    }
                    //创建type的持久化节点,且对于这个节点进行订阅
                    zkClient.create(path, false);
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
                //数据获取完成时,调用 `#notify(...)` 方法,回调 NotifyListener,urls为所有自节点的url
notify(url, listener, urls); } } catch (Throwable e) { throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); } }

//todo addListener细节

 

 

取消订阅doUnsubscribe

@Override
    public void doUnsubscribe(URL url, NotifyListener listener) {
        ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
        if (listeners != null) {
            ChildListener zkListener = listeners.get(listener);
            if (zkListener != null) {
                if (ANY_VALUE.equals(url.getServiceInterface())) {
                    String root = toRootPath();
                    zkClient.removeChildListener(root, zkListener);
                } else {
                    for (String path : toCategoriesPath(url)) {
                        zkClient.removeChildListener(path, zkListener);
                    }
                }
            }
        }
    }

拉取全量数据lookup

/**
     * 查询到符合条件的注册数据,全量拉取一次数据
     * @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @return 已注册信息列表,可能为空,含义同{@link NotifyListener#notify(List<URL>)}的参数。
     * @see NotifyListener#notify(List)
     * @return
     */
    @Override
    public List<URL> lookup(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("lookup url == null");
        }
        try {
            List<String> providers = new ArrayList<>();
            for (String path : toCategoriesPath(url)) {
                List<String> children = zkClient.getChildren(path);
                if (children != null) {
                    providers.addAll(children);
                }
            }
            return toUrlsWithoutEmpty(url, providers);
        } catch (Throwable e) {
            throw new RpcException("Failed to lookup " + url + " from zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }
private List<URL> toUrlsWithoutEmpty(URL consumer, List<String> providers) {
        List<URL> urls = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(providers)) {
            for (String provider : providers) {
                provider = URL.decode(provider);
                if (provider.contains(PROTOCOL_SEPARATOR)) {
                    URL url = URL.valueOf(provider);
                    //查找与consumer一致的provider的url,依靠group+version+serviceName进行区分
                    if (UrlUtils.isMatch(consumer, url)) {
                        urls.add(url);
                    }
                }
            }
        }
        return urls;
    }

到这 注册者基于zookeeper进行注册的实现告一段落了

继续深入,notify做了什么呢,如何通知到litener呢?listener是什么,是怎么添加上的,添加到哪了?这个只是zookeeperRegistry做的事情,上面两个抽象类干了啥呢

别着急,我们一个一个看

notify

经过zookeeperRegistry、FailBackRegistry的调用封装,核心逻辑在org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)

 /**
     * Notify changes from the Provider side.
     *
     * @param url      consumer side url
     * @param listener listener
     * @param urls     provider latest urls
     */
    protected void notify(URL url, NotifyListener listener, List<URL> urls) {
        if (url == null) {
            throw new IllegalArgumentException("notify url == null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("notify listener == null");
        }
        if ((CollectionUtils.isEmpty(urls))
                && !ANY_VALUE.equals(url.getServiceInterface())) {
            logger.warn("Ignore empty notify urls for subscribe url " + url);
            return;
        }
        if (logger.isInfoEnabled()) {
            logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
        }
        // keep every provider's category.
        //将url分类放置
        Map<String, List<URL>> result = new HashMap<>();
        for (URL u : urls) {
            if (UrlUtils.isMatch(url, u)) {
                String category = u.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
                List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
                categoryList.add(u);
            }
        }
        if (result.size() == 0) {
            return;
        }
        //获取url在notified的全部url数据,若notified无则创建一个且放入
        Map<String, List<URL>> categoryNotified = notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
        for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
            String category = entry.getKey();
            List<URL> categoryList = entry.getValue();
            categoryNotified.put(category, categoryList);
            //对于所有url进行通知
            listener.notify(categoryList);
            // We will update our cache file after each notification.
            // When our Registry has a subscribe failure due to network jitter, we can return at least the existing cache URL.
            //当我们的注册表由于网络抖动而出现订阅失败时,我们至少可以返回现有的缓存URL。
            saveProperties(url);
        }
    }

做了啥呢

  1. 对于需要通知的url进行分类
  2. 分类调用notify方法进行通知,这个也就是org.apache.dubbo.registry.NotifyListener#notify这个方法,也就是在服务doSubscribe传入的参数NotifyListener
  3. 将url进行保存缓存

缓存saveProperties

private void saveProperties(URL url) {
        if (file == null) {
            return;
        }

        try {
            StringBuilder buf = new StringBuilder();
            /**
             * notifies是 被通知的 URL 集合
             *
             * key1:消费者的 URL ,例如消费者的 URL ,和 {@link #subscribed} 的键一致
             * key2:分类,例如:providers、consumers、routes、configurators。【实际无 consumers ,因为消费者不会去订阅另外的消费者的列表】
             * 在 {@link Constants} 中,以 "_CATEGORY" 结尾
             */
            Map<String, List<URL>> categoryNotified = notified.get(url);
            if (categoryNotified != null) {
                for (List<URL> us : categoryNotified.values()) {
                    for (URL u : us) {
                        if (buf.length() > 0) {
                            buf.append(URL_SEPARATOR);
                        }
                        buf.append(u.toFullString());
                    }
                }
            }
            properties.setProperty(url.getServiceKey(), buf.toString());
            long version = lastCacheChanged.incrementAndGet();
            //同步写入文件还是异步写入文件
            if (syncSaveFile) {
                doSaveProperties(version);
            } else {
                registryCacheExecutor.execute(new SaveProperties(version));
            }
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }

file是啥,什么时候创建的,文件写入本地哪呢

file是在abstractRegistry初始化的时候进行构建的

public AbstractRegistry(URL url) {
        setUrl(url);
        // Start file save timer
        syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
        String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(APPLICATION_KEY) + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
        String filename = url.getParameter(FILE_KEY, defaultFilename);
        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());
    }

doSaveProperties

public void doSaveProperties(long version) {
        if (version < lastCacheChanged.get()) {
            return;
        }
        if (file == null) {
            return;
        }
        // Save
        try {
            File lockfile = new File(file.getAbsolutePath() + ".lock");
            if (!lockfile.exists()) {
                lockfile.createNewFile();
            }
            try (RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
                 FileChannel channel = raf.getChannel()) {
                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");
                }
                // Save
                try {
                    if (!file.exists()) {
                        file.createNewFile();
                    }
                    try (FileOutputStream outputFile = new FileOutputStream(file)) {
                        properties.store(outputFile, "Dubbo Registry Cache");
                    }
                } finally {
                    lock.release();
                }
            }
        } catch (Throwable e) {
            savePropertiesRetryTimes.incrementAndGet();
            if (savePropertiesRetryTimes.get() >= MAX_RETRY_TIMES_SAVE_PROPERTIES) {
                logger.warn("Failed to save registry cache file after retrying " + MAX_RETRY_TIMES_SAVE_PROPERTIES + " times, cause: " + e.getMessage(), e);
                savePropertiesRetryTimes.set(0);
                return;
            }
            if (version < lastCacheChanged.get()) {
                savePropertiesRetryTimes.set(0);
                return;
            } else {
                registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
            }
            logger.warn("Failed to save registry cache file, will retry, cause: " + e.getMessage(), e);
        }
    }

这使用了文件锁的方式保证只有一个在读写文件

消费者伙服务治理中心获取注册信息后会做本地缓存。内存中也有一份,保存在Properties对象里,磁盘上也持久化一份,通过file对象进行引用。

磁盘中是org.apache.dubbo.registry.support.AbstractRegistry#file

内存中的缓存是org.apache.dubbo.registry.support.AbstractRegistry#notified;其类型复杂:ConcurrentMap<URL, Map<String, List<URL>>>

是ConcurrentHashMap里嵌套的Map,外层Map的key是消费者的URL,内层key是分类(providers、consumers、routes、configurators)value是对应的服务列表。对于没有服务提供者的提供服务的URL 会以empty://进行开头

在AbstractRegistry进行构造的时候,就会从本地磁盘文件把持久化的注册信息读取到Properties对象中,并加载到内存缓存里Properties。Properties保存里所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表、路由规则列表、配置规则列表等作为value,以空格隔开。key.registies保存所有的注册中心的地址。如果在应用启动过程中无法连接到注册中心,则Dubbo会自动通过本地缓存进行加载。而当有节点进行更新的时候在触发notify操作的时候也会一起更新内存缓存和更新文件。

org.apache.dubbo.registry.NotifyListener#notify

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

}

这个方法的实现有以下几个

 

这块 dubbo还使用了 模版模式&工厂模式,下次再展开分析

posted @ 2020-01-04 21:09  大龄波妞  阅读(1388)  评论(0)    收藏  举报