dubbo分析-ZookeeperRegistry
本文波妞主要告诉大家,dubbo服务是以啥形式存在zookeeper上的,且是如何工作的
一:demo运行
- 启动本地zookeeper
- 下载安装zookeeper,软件下载地址:http://mirror.bit.edu.cn/apache/zookeeper/
- 解压后,进入到zookeeper目录,进入conf目录,会看到一个 zoo_simple.cfg文件,重命名为zoo.cfg
- 进入bin目录,执行 sh zkServer.sh start
- zookeeper就启动了
- 运行dubbo-demo-api-provider(org.apache.dubbo.demo.provider.Application#main)
- 运行dubbo-demo-api-consumer(org.apache.dubbo.demo.consumer.Application#main)
- 看到
这样一个简单的demo已运行成功,且服务调用成功
二:服务在zookeeper如何存储
接下来,我们顺着这个zookeeper来看下这个服务在zookeeper这个注册中心是以怎样的形式存在的
- 进入刚刚zookeeper的bin
- 然后 ls / 可以看到
根目录下有两个节点:dubbi、zookeeper
- ls /dubbo
我们运行的demo服务已经在上面了
- 继续进一步看里面到底是啥
可以看到有四个节点:consumers、configurators、routers、providers;而且 这四个节点都是持久化的节点
![]()
![]()
//todo 补充 zookeeper节点如何识别是否为持久化节点
- 节点consumers
而当dubbo-demo-api-consumer运行结束后看consumers节点,空空如也
![]()
provider节点为我们的服务提供者的信息,也就是URL的元数据,
- routers为消费者路由策略,URL元数据等
- 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); } }
做了啥呢
- 对于需要通知的url进行分类
- 分类调用notify方法进行通知,这个也就是org.apache.dubbo.registry.NotifyListener#notify这个方法,也就是在服务doSubscribe传入的参数NotifyListener
- 将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还使用了 模版模式&工厂模式,下次再展开分析




浙公网安备 33010602011771号