本系列为个人Dubbo学习笔记衍生篇,是正文篇之外的衍生内容,内容来源于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章。仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。
Dubbo 在 2.7 版本之后提供了新的监听方式。本文来分析 Dubbo 2.6 和 Dubbo 2.7 版本之间对于配置更新的差异。
首先我们需要知道 Dubbo 2.7提供了 应用级别配置 和 服务级别配置
综述,如下图(下图中的zk充当注册中心和配置中心,因此会创建下面三个节点):
/dubbo/{服务接口全路径}/configurations
/dubbo/config/{应用名}/configurations
/dubbo/config/{分组}/{服务接口全路径}:{版本号}/configurations
。该节点的内容等价于节点 ①的内容(实际节点内容由于生成规则不同有所不同,但其所包含的配置内容是等价的)。本文使用的 dubbo-admin-0.2.0 来进行动态配置,该版本会兼容 2.6 和 2.7 版本,因此如果进行服务级别动态配置,则上图中的 ① 和 ③ 两个配置节点都会创建。如果进行应用级别动态配置,则会创建 ② 的配置节点,因为 Dubbo2.6版本不支持 应用级别配置。
关于上述内容,在 Dubbo笔记 ㉒ :配置中心 中有较为完整的描述,如有需要可阅读。
基于上述前提,我们来看下面的内容。
我们知道提供者在服务暴露时的核心方法是 RegistryProtocol#export,这里我们忽略其他代码,仅看如下代码:
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
.... // 获取服务接口 URL, 获取注册中心 URL
// 获取订阅覆盖 URL
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 生成 对 overrideSubscribeUrl 的监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 1. 从配置中心加载最新的配置并监听配置
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
.... // 将服务注册到注册中心
// Deprecated! Subscribe to override rules in 2.6.x or before.
// 2. 订阅并监听配置
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
.... // 收尾工作
return new DestroyableExporter<>(exporter);
}
这里我们主要关注两个方法 :
下面我们详细来看这两个方法的具体实现:
该函数是 Dubbo2.7 版本提供的,Dubbo 2.6 版本所有数据都存在注册中心上,Dubbo 2.7版本分成了注册中心,配置中心,和元数据中心。
private final ProviderConfigurationListener providerConfigurationListener = new ProviderConfigurationListener();
private final Map<String, ServiceConfigurationListener> serviceConfigurationListeners = new ConcurrentHashMap<>();
private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) {
// 获取配置中心应用级别配置并覆盖本地配置
providerUrl = providerConfigurationListener.overrideUrl(providerUrl);
// 创建服务级别配置监听器
ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener);
serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener);
// 获取配置中心服务级别配置并覆盖本地配置
return serviceConfigurationListener.overrideUrl(providerUrl);
}
这里我们的关键点在于两个监听器 :
serviceConfigurationListeners
来保存不同接口的监听器。我们这里先来看 ServiceConfigurationListener 之后再来看 ProviderConfigurationListener 。
ServiceConfigurationListener 用于 Dubbo 2.7 及以后 来处理 服务级别的配置,在 ServiceConfigurationListener 的构造函数中会通过 this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX)
来监听当前服务的配置。其实现如下:
private class ServiceConfigurationListener extends AbstractConfiguratorListener {
private URL providerUrl;
private OverrideListener notifyListener;
public ServiceConfigurationListener(URL providerUrl, OverrideListener notifyListener) {
this.providerUrl = providerUrl;
this.notifyListener = notifyListener;
// 这里需要注意,在ServiceConfigurationListener 创建时会通过该方法订阅配置中心的配置。
this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX);
}
private <T> URL overrideUrl(URL providerUrl) {
return RegistryProtocol.getConfigedInvokerUrl(configurators, providerUrl);
}
@Override
protected void notifyOverrides() {
notifyListener.doOverrideIfNecessary();
}
}
这里的关键在于 ServiceConfigurationListener#initWith ,其实现在其父类中完成,如下:
// AbstractConfiguratorListener#initWith
protected final void initWith(String key) {
// 1. 获取配置中心
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
// 2. 添加监听器
dynamicConfiguration.addListener(key, this);
// 根据 key 获取配置中心上的配置
String rawConfig = dynamicConfiguration.getConfig(key);
if (!StringUtils.isEmpty(rawConfig)) {
// 3. 如果配置不为空,则会解析配置并更新本地配置
process(new ConfigChangeEvent(key, rawConfig));
}
}
下面我们按照注释的顺序一一来看:
DynamicConfiguration#getDynamicConfiguration 实现如下:
static DynamicConfiguration getDynamicConfiguration() {
// 获取动态配置中心,这里即获取的配置中心的对象
Optional<Configuration> optional = Environment.getInstance().getDynamicConfiguration();
// 如果没有配置中心,则获取默认的 DynamicConfiguration
return (DynamicConfiguration) optional.orElseGet(() -> getExtensionLoader(DynamicConfigurationFactory.class)
.getDefaultExtension()
.getDynamicConfiguration(null));
}
我们这里以 zk 为配置中心,所以获取的是 ZookeeperDynamicConfiguration 实例。ZookeeperDynamicConfiguration 会在构造函数中建立与 ZK 的连接并获取节点内容缓存到本地。
DynamicConfiguration#addListener 是实现监听服务级别配置的的核心方法,这里调用的是 DynamicConfiguration 接口的默认方法如下
// org.apache.dubbo.configcenter.DynamicConfiguration#addListener(java.lang.String, org.apache.dubbo.configcenter.ConfigurationListener)
default void addListener(String key, ConfigurationListener listener) {
// 分组指定为 dubbo
addListener(key, DEFAULT_GROUP, listener);
}
随后通过如下调用顺序:
ZookeeperDynamicConfiguration#addListener -> CacheListener#addListener
CacheListener#addListener 实现如下:
private Map<String, Set<ConfigurationListener>> keyListeners = new ConcurrentHashMap<>();
public void addListener(String key, ConfigurationListener configurationListener) {
// 将 key 和 configurationListener 保存到了 keyListeners 中。
Set<ConfigurationListener> listeners = this.keyListeners.computeIfAbsent(key, k -> new CopyOnWriteArraySet<>());
listeners.add(configurationListener);
}
这里可以看到,这里会将当前接口服务的key 和监听器保存到 CacheListener#keyListeners 中。而当我们进行动态配置时会修改配置中心的节点,而当 zk 节点变化时会触发 CacheListener#childEvent 方法,其实现如下:
@Override
public void childEvent(CuratorFramework aClient, TreeCacheEvent event) throws Exception {
TreeCacheEvent.Type type = event.getType();
ChildData data = event.getData();
if (type == TreeCacheEvent.Type.INITIALIZED) {
initializedLatch.countDown();
return;
}
// TODO, ignore other event types
if (data == null) {
return;
}
// TODO We limit the notification of config changes to a specific path level, for example
// /dubbo/config/service/configurators, other config changes not in this level will not get notified,
// say /dubbo/config/dubbo.properties
if (data.getPath().split("/").length >= 5) {
byte[] value = data.getData();
// 根据节点path 转换为 key
String key = pathToKey(data.getPath());
ConfigChangeType changeType;
switch (type) {
case NODE_ADDED:
changeType = ConfigChangeType.ADDED;
break;
case NODE_REMOVED:
changeType = ConfigChangeType.DELETED;
break;
case NODE_UPDATED:
changeType = ConfigChangeType.MODIFIED;
break;
default:
return;
}
ConfigChangeEvent configChangeEvent = new ConfigChangeEvent(key, new String(value, StandardCharsets.UTF_8), changeType);
// 根据转换的key 来获取对应的监听器,该监听器在
Set<ConfigurationListener> listeners = keyListeners.get(key);
if (CollectionUtils.isNotEmpty(listeners)) {
listeners.forEach(listener -> listener.process(configChangeEvent));
}
}
}
那么这里我们就可以得知:当配置中心上有节点变动时会触发 CacheListener#childEvent 方法,而在 CacheListener#childEvent 方法中会通过 pathToKey(data.getPath());
将path 转为 key,随后依据 key 从 keyListeners 找到对应节点的监听器,并执行 listener.process(configChangeEvent)
来触发监听器操作。
上述理论上如此,但需要注意的是:
当我们使用zk 作为配置中心时 Dubbo的服务级别配置是通过 RegistryService#subscribe 完成更新,而不是 通过 ServiceConfigurationListener 完成,因为当配置节点更新时并不会触发监听器操作, 原因在于keyListeners 中的key 和 pathToKey(data.getPath());
转化的key 并不一致。
并且
上述问题的原因如下 :
在 Dubbo 2.7.0 的版本下,ServiceConfigurationListener 在初始化时会传入监听器的 key,而这个key是通过 providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX
生成。
其中 URL#getEncodedServiceKey 实现如下,可以看到仅仅是将 serviceKey 的第一个 /
替换为了 *
public String getEncodedServiceKey() {
String serviceKey = this.getServiceKey();
serviceKey = serviceKey.replaceFirst("/", "*");
return serviceKey;
}
所以实际上 CacheListener#keyListeners 中监听的 key 为dubbo*com.kingfish.service.impl.SimpleDemoService:1.0.0.configurators
。也即是说对于 com.kingfish.service.impl.SimpleDemoService 这个服务接口,我们对应的监听器的key 为 dubbo*com.kingfish.service.impl.SimpleDemoService:1.0.0.configurators
。
如下图:
而当我们修改动态配置时,即zk节点发生变化,会触发 CacheListener#childEvent 方法。其中会根据节点路径转换为对应的key,如下:
1. 修改的节点路径如下:
data.getPath() = /dubbo/config/dubbo/com.kingfish.service.impl.SimpleDemoService:1.0.0/configurators
2. key 的生成规则如下:
key = pathToKey(data.getPath())
= dubbo.com.kingfish.service.impl.SimpleDemoService:1.0.0.configurators
也即是说,当我们修改 com.kingfish.service.impl.SimpleDemoService 这个服务接口的动态配置时,Dubbo 根据 zk 节点路径转化的Key 为 dubbo.com.kingfish.service.impl.SimpleDemoService:1.0.0.configurators
。而我们实际的key为 dubbo*com.kingfish.service.impl.SimpleDemoService:1.0.0.configurators
。从而导致我们无法通过path转化的key找到对应服务的监听器。
如下图,由于变更节点转化的key 与 keyListeners中的key 不匹配导致 无法根据变更节点找到对应的监听器来进行处理:
AbstractConfiguratorListener#process
-> RegistryProtocol.ServiceConfigurationListener#notifyOverrides
-> RegistryProtocol.OverrideListener#doOverrideIfNecessary
在 RegistryProtocol.OverrideListener#doOverrideIfNecessary 中 完成了本地的配置更新,如下:
public synchronized void doOverrideIfNecessary() {
final Invoker<?> invoker;
if (originInvoker instanceof InvokerDelegate) {
invoker = ((InvokerDelegate<?>) originInvoker).getInvoker();
} else {
invoker = originInvoker;
}
//The origin invoker
// 获取 原始 URL
URL originUrl = RegistryProtocol.this.getProviderUrl(invoker);
String key = getCacheKey(originInvoker);
ExporterChangeableWrapper<?> exporter = bounds.get(key);
if (exporter == null) {
logger.warn(new IllegalStateException("error state, exporter should not be null"));
return;
}
//The current, may have been merged many times
// 获取当前 URL,当前URL,可能已经合并过很多次,所以并不一定等同于 originUrl
URL currentUrl = exporter.getInvoker().getUrl();
//Merged with this configuration
// 使用 originUrl 与当前配置合并后,产生新 url,如果动态配置更新了,则新的URl和旧URl肯定不同
URL newUrl = getConfigedInvokerUrl(configurators, originUrl);
// 合并服务级别配置
newUrl = getConfigedInvokerUrl(serviceConfigurationListeners.get(originUrl.getServiceKey())
.getConfigurators(), newUrl);
// 合并应用级别配置
newUrl = getConfigedInvokerUrl(providerConfigurationListener.getConfigurators(), newUrl);
// currentUrl != newUrl, 则说明配置有变动,则重新发布服务
if (!currentUrl.equals(newUrl)) {
// 重新发布服务
RegistryProtocol.this.reExport(originInvoker, newUrl);
...
}
}
ProviderConfigurationListener 的实现与 ServiceConfigurationListener 基本相同,不同的是,ServiceConfigurationListener 监听的服务级别, ProviderConfigurationListener监听的是应用级别的配置,所以这里监听的节点会有所不同。
private class ProviderConfigurationListener extends AbstractConfiguratorListener {
public ProviderConfigurationListener() {
// 传入 监听的 key。这里为 {应用名}.configurators
this.initWith(ApplicationModel.getApplication() + CONFIGURATORS_SUFFIX);
}
// 获取现有配置规则,并在导出之前覆盖提供程序URL。当服务发布时会调用此方法来对配置进行刷新。
private <T> URL overrideUrl(URL providerUrl) {
return RegistryProtocol.getConfigedInvokerUrl(configurators, providerUrl);
}
// 当监听节点有变化时会触发该方法,此方法会调用监听器的doOverrideIfNecessary 方法来刷新本地缓存的配置。
@Override
protected void notifyOverrides() {
// 遍历所有的 overrideListeners 并调用 doOverrideIfNecessary 来刷新 配置。
overrideListeners.values().forEach(listener -> ((OverrideListener) listener).doOverrideIfNecessary());
}
}
对于应用级别的动态配置,即是是 ZK 作为配置中心也不会出现上面 服务级别动态配置无法触发监听器的情况。
关于这部分的具体代码,详阅:Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export 中 服务订阅
部分的内容。
我们这里直接说结果 :由于 Dubbo 2.7之前的版本只有注册中心,并没有配置中心和元数据中心,所以动态配置的内容会保存在注册中心上。以 zk 为例,如下图:
RegistryService#subscribe 会监听 图 ① 标注的节点,而上面我们提到,当我们创建服务级别的配置时,dubbo-admin-0.2.0 为了 兼容,会同时创建 图① 和 图 ② 节点。
而我们上面提到 当配置中心是zk 时ServiceConfigurationListener 并不起作用,所以此时的服务级别的动态配置更新就是通过 RegistryService#subscribe 监听 图① 节点的变化完成。
如下表
序号 | Dubbo版本 | 配置中心 | 服务级别配置 | 应用级别配置 |
---|---|---|---|---|
1 | 2.6 及以下 | 不支持 | 通过 RegistryService#subscribe 完成 | 不支持 |
2 | 2.7 及以上 | ZK | 通过 RegistryService#subscribe 完成 | 通过 ProviderConfigurationListener 完成 |
3 | 2.7 及以上 | 非ZK | 通过 ServiceConfigurationListener 完成 | 通过 ProviderConfigurationListener 完成 |
对于 Dubbo 2.6 及以下,通过 ServiceConfigurationListener 和 ProviderConfigurationListener 监听器完成更新。但是对于 zk作为配置中心的场景,ServiceConfigurationListener 失效,通过 Dubbo 2.6 版本的旧逻辑来完成更新。
对于 Dubbo 2.7及以上,通过 RegistryService#subscribe 方法中的逻辑完成更新。
应用配置和服务级别配置的优先级为: 应用级别 > 服务级别
因为配置的刷新是通过 RegistryProtocol.OverrideListener#notify 方法完成,在这个方法中会先 加载 ServiceConfigurationListeners 的配置,后加载 ProviderConfigurationListener 的配置。即后加载的配置会覆盖之前加载的配置,故应用级别 > 服务级别。
消费者的流程和提供者相似,下面来简单介绍。
当消费者启动时,Dubbo 会按照如下流程执行:
RegistryProtocol#refer -> RegistryProtocol#doRefer -> RegistryDirectory#subscribe
其中 RegistryDirectory#subscribe 的实现如下:
private static final ConsumerConfigurationListener consumerConfigurationListener = new ConsumerConfigurationListener();
private ReferenceConfigurationListener serviceConfigurationListener;
public void subscribe(URL url) {
setConsumerUrl(url);
// 1. 订阅应用级别配置
consumerConfigurationListener.addNotifyListener(this);
// 2. 订阅当前引用的接口服务的服务级别配置
serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
// 3. Dubbo 2.6 版本的订阅方式
registry.subscribe(url, this);
}
这里我们又看到了两个监听器,我们直接说明:
注: 对于 路由节点,消费者会在 ListenableRouter#init
和 TagRouter#notify
方法中也会调用 DynamicConfiguration#addListener 来增加监听器进行监听。
RegistryService#subscribe 我们在上面已经提到过,需要注意的是 对于消费者来说RegistryDirectory#subscribe 不仅仅只订阅了configurators,而是订阅了 providers、configurators、routers 节点,当providers、configurators、routers 子节点变化时会通过回调通知RegistryDirectory。
下面我们来看 ConsumerConfigurationListener 和 ReferenceConfigurationListener 的实现。
ConsumerConfigurationListener和 ReferenceConfigurationListener 都是 RegistryDirectory 的内部静态类,其实现基本相同,如下:
private static class ConsumerConfigurationListener extends AbstractConfiguratorListener {
List<RegistryDirectory> listeners = new ArrayList<>();
ConsumerConfigurationListener() {
// 应用级别监听
this.initWith(ApplicationModel.getApplication() + Constants.CONFIGURATORS_SUFFIX);
}
// 添加监听器
void addNotifyListener(RegistryDirectory listener) {
this.listeners.add(listener);
}
@Override
protected void notifyOverrides() {
// 当消费者的应用级别配置更新时会遍历所有监听器进行配置刷新
// 即触发 RegistryDirectory#refreshInvoker 来刷新本地
listeners.forEach(listener -> listener.refreshInvoker(Collections.emptyList()));
}
}
private static class ReferenceConfigurationListener extends AbstractConfiguratorListener {
private RegistryDirectory directory;
private URL url;
ReferenceConfigurationListener(RegistryDirectory directory, URL url) {
this.directory = directory;
this.url = url;
// 服务级别监听
this.initWith(url.getEncodedServiceKey() + Constants.CONFIGURATORS_SUFFIX);
}
@Override
protected void notifyOverrides() {
// to notify configurator/router changes
// 刷新引用的服务配置
// 触发 RegistryDirectory#refreshInvoker 来刷新本地
directory.refreshInvoker(Collections.emptyList());
}
}
当动态配置更新后,会触发 RegistryDirectory#refreshInvoker 来刷新本地配置,按照如下流程调用后 来到了RegistryDirectory#mergeUr 方法中。
RegistryDirectory#refreshInvoker -> RegistryDirectory#toInvokers -> RegistryDirectory#mergeUrl
在 RegistryDirectory#mergeUrl 方法中 Dubbo 完成了配置合并,合并的优先级是
动态配置 > Jvm 本地配置 > 消费者端配置(xml > Properties) > 提供者端配合
下面我们来看具体实现:
/**
* Merge url parameters. the order is: override > -D > Consumer > Provider
*
* @param providerUrl
* @return
*/
private URL mergeUrl(URL providerUrl) {
// providerUrl 是 提供者端, queryMap 是消费者端的参数
// 1. 合并消费者端的参数。这一步会使用消费者参数覆盖提供者参数
providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap); // Merge the consumer side parameters
// 2. 动态配置的参数合并
providerUrl = overrideWithConfigurator(providerUrl);
providerUrl = providerUrl.addParameter(Constants.CHECK_KEY, String.valueOf(false)); // Do not check whether the connection is successful or not, always create Invoker!
// The combination of directoryUrl and override is at the end of notify, which can't be handled here
this.overrideDirectoryUrl = this.overrideDirectoryUrl.addParametersIfAbsent(providerUrl.getParameters()); // Merge the provider side parameters
if ((providerUrl.getPath() == null || providerUrl.getPath()
.length() == 0) && "dubbo".equals(providerUrl.getProtocol())) { // Compatible version 1.0
//fix by tony.chenl DUBBO-44
String path = directoryUrl.getParameter(Constants.INTERFACE_KEY);
if (path != null) {
int i = path.indexOf('/');
if (i >= 0) {
path = path.substring(i + 1);
}
i = path.lastIndexOf(':');
if (i >= 0) {
path = path.substring(0, i);
}
providerUrl = providerUrl.setPath(path);
}
}
return providerUrl;
}
// 2. 覆盖参数配置
private URL overrideWithConfigurator(URL providerUrl) {
// override url with configurator from "override://" URL for dubbo 2.6 and before
// 2.1 Dubbo 2.6 及以前的版本的覆盖策略
providerUrl = overrideWithConfigurators(this.configurators, providerUrl);
// override url with configurator from configurator from "app-name.configurators"
// 2.2 Dubbo 2.7 版本,通过 consumerConfigurationListener 监听动态消费者应用级别配置,在此进行配置合并
providerUrl = overrideWithConfigurators(consumerConfigurationListener.getConfigurators(), providerUrl);
// override url with configurator from configurators from "service-name.configurators"
// 2.3 提供者服务级别配置合并
if (serviceConfigurationListener != null) {
providerUrl = overrideWithConfigurators(serviceConfigurationListener.getConfigurators(), providerUrl);
}
return providerUrl;
}
我们这里假设 消费者端 本地的原始配置为 URL0
,则 :
URL0
。RegistryDirectory 在初始化时会将合并后的消费者配置保存到 RegistryDirectory#queryMap 中。这一步完成了 Jvm 配置 覆盖 消费者端本地配置 ,诞生了合并后的新配置 URL1
。ClusterUtils.mergeUrl(providerUrl, queryMap);
会使用 queryMap 中的配置覆盖providerUrl 中的配置,而 queryMap 中保存的是URL1
,providerUrl 为从注册中心拉取的提供者的配置。这一步完成了 URL1
覆盖提供者的配置,诞生了合并后的新配置 URL2
.URL2
的配置,这一步完成了动态配置 覆盖 URL2
,诞生了合并后的最终配合 URL3
。综合上面的过程得到优先级的合并规则 :
动态配置 > Jvm 本地配置 > 消费者端配置(xml > Properties) > 提供者端配合
以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正