一、Dubbo中注册中心的作用
在Dubbo的学习中可以看到注册中心是Dubbo的核心组件,注册中心主要有以下作用:
(1)动态加入。
服务提供者可以通过注册中心将自己暴露给其他消费者,无须在消费者服务中逐个更新配置文件。
(2)动态发现
一个消费者可以通过订阅的方式动态的感知新的配置、路由参数和新的服务提供者,无须重启服务使之生效。
(3)动态调整
注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点。
(4)统一配置
由于注册中心的存在,只需要配置注册中心的地址,而无需配置其他服务提供者的配置,避免了本地配置导致每个服务的配置不一致的问题。
二、Zookeeper数据结构
如图所示是zookeeper的节点信息。
如上图所示,可以看到zookeeper是一个树形结构的注册中心,并且在节点中存在持久化节点和临时节点。
树形结构中元素:
(1)根节点
zookeeper树的根结点,默认是/dubbo。也可以通过配置group属性来设置根结点,<dubbo:registry group="name">
(2)持久化节点
分别是providers、consumers、configurations、routes,并且这四个节点都属于持久化节点。
(3)临时节点 - providers目录
providers目录中包含了多个服务提供者的URL信息,属于临时节点。
(4)临时节点 - consumers目录
consumers目录中包含了多个消费者的URL信息,属于临时节点。
(5)临时节点 - configurations目录
configurations目录中包含了多个服务提供者动态配置的URL信息,属于临时节点。
(6)临时节点 - routes目录
routes目录中包含了多个用于消费者路由策略的URL信息,属于临时信息。
· 持久节点和临时节点
持久节点-表示服务节点一但被创建,触发触发主动删除,否则一直存储在zk中,注册中心重启也会存在,节点不会丢失。
临时节点-表示服务注册后连接丢失或session超时,注册的临时节点就会被自动删除。
三、zookeeper的订阅/发布
在传统项目中,我们通常会把配置信息写入配置文件中,如果配置需要变更,需要修改配置文件并刷新内存重新加载或重启服务。而如果我们使用注册中心帮助我们来更新配置信息则会方便很多。
比如上述的节点信息,客户端只需要关心/consumers节点即可,即使当providers目录中的其中一个服务端宕机了,在客户端的配置中也不需要更改配置信息。
上述节点信息中的处理,他会动态的删除宕机的服务临时节点,并且刷新/providers目录。对于客户端对该服务接口的调用由于负载均衡处理,并不会受到影响。
· 服务发布的实现 - zookeeper
服务提供者和消费者在启动时都会将自己注册到注册中心,服务提供者的注册是为了让消费者感知服务的存在,发起远程调用,也让服务治理中心感知有新的服务提供者上线。
消费者的注册是为了让服务治理中心发现自己。
dubbo中服务发布节点创建相关代码:
protected void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
· 订阅的实现 - zookeeper
在服务暴露时,服务端服务会订阅configuration监听动态配置。消费者启动时,会订阅providers、routes、configurations三个节点目录,分别监听服务提供者、路由和动态配置的变更通知。
目前客户端订阅的方式,在客户端刚启动时,会从注册中心主动读取全量的配置信息数据,并且在订阅的节点上注册一个watcher,客户端与注册中心保持长连接。在客户端运行中则是通过注册中心根据watcher来通知客户端,客户端接到通知后会进行事件处理,dubbo中的源码是会把配置信息重新读取下来。
订阅关系如下图所示:
dubbo中订阅代码如下:
List<URL> urls = new ArrayList<URL>();
//需要订阅的路径
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) { //缓存判断
zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
listeners = zkListeners.get(url);
} //订阅缓存判断
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
@Override
public void childChanged(String parentPath, List<String> currentChilds) {
ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
zkListener = listeners.get(listener);
} //创建订阅的持久化节点
zkClient.create(path, false);
//该结点下子路径 List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}notify(url, listener, urls);
上述代码会执行多次,直到服务端服务订阅了configurations节点,客户端引用的服务端服务订阅了providers、routes、configurations节点。
notify方法,主要是触发订阅事件,进行订阅后的处理。
providers节点,则订阅方会更新本地目录的invoker服务列表。
routes节点,则订阅方会更新本地路由规则列表。
configurations节点,则订阅方会更新本地动态参数列表。
参考资料:
深入理解Apache Dubbo与实战 - 第3章 Dubbo注册中心