本系列为个人Dubbo学习笔记衍生篇,是正文篇之外的衍生内容,内容来源于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章。仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。
配置中心的核心功能是作为 Key-Value 存储,Dubbo 框架告知配置中心其关心的 key,配置中心返回该key对应的 value 值。
按照应用场景划分,配置中心在 Dubbo 框架中主要承担以下职责:
为了进一步实现对 key-value 的分组管理,Dubbo 的配置中心还加入了 namespace、group 的概念,这些概念在很多专业的第三方配置中心中都有体现,通常情况下,namespace 用来隔离不同的租户,group 用来对同一租户的key集合做分组。
当前,Dubbo 配置中心实现了对 Zookeeper、Nacos、Etcd、Consul、Apollo 的对接。
目前Dubbo主要版本有两个:Dubbo 2.7和Dubbo 2.6
两个版本最大的差异,在于配置管理中心。Dubbo 2.6版本所有数据都存在注册中心上,Dubbo 2.7版本分成了注册中心,配置中心,和元数据中心。
Dubbo Admin对于配置管理的操作,配置方式和Dubbo 2.7的模式一样,详见Dubbo Admin配置说明,并且兼容Dubbo 2.6的版本,这里的“兼容”是指:
我们以 dubbo-admin-0.2.0 为例来创建动态配置
在以ZK 为注册中心的情况下,不同的配置在zk上的节点结构如下:
正常的服务节点:
/dubbo/服务名/providers
/dubbo/服务名/routers
/dubbo/服务名/consumers
/dubbo/服务名/congurators
应用级别的配置:
/dubbo/config/应用名/configurators
/dubbo/config/应用名/routers
/dubbo/config/应用名/dubbo.properties
服务级别的配置:
/dubbo/config/分组名/服务名:版本号/configurators
/dubbo/config/分组名/服务名:版本号/routers
/dubbo/config/分组名/服务名:版本号/dubbo.properties
配置管理(外部化配置中心):
/dubbo/config/dubbo/dubbo.properties : global 级别
/dubbo/config/应用名/dubbo.properties : 应用级别。
在 zk 上的体现如下图:
本文来分析配置中心外部化配置中心 和 服务治理的功实现
Dubbo 外部化配置即可以通过外部化的方式引入 dubbo 的相关配置,包括 注册中心地址、端口、协议等。官方相关如下:配置中心扩展
我们可以通过dubbo-admin-0.2.0 中的配置管理来进行外部化配置,如下图:
我们可以基于应用级别和 全局级别来建立配置文件,下图中的 global 配置即为 全局配置,而 spring-dubbo-provider 则是以应用级别的配置文件。
其中 spring-dubbo-provider 的配置内容如下,指定了注册中心的地址和超时时间。(global 配置这里只是为了演示,没有配置数据)
当 应用名为 spring-dubbo-provider 的服务启动后,则会读取上面指定的配置,使用 zookeeper://192.168.99.52:2182 作为注册中心,超时时间设定为 3s。
下面我们来分析一下外部配置的功能是如何实现的:
经过之前的系列文章分析,我们知道消费者和提供者在启动时会分别调用 ServiceConfig#checkAndUpdateSubConfigs
和 ReferenceConfig#checkAndUpdateSubConfigs 来检查一些配置信息。而在这其中会调用AbstractInterfaceConfig#startConfigCenter 来启动配置中心,其代码如下:
// org.apache.dubbo.config.AbstractInterfaceConfig#startConfigCenter
void startConfigCenter() {
if (configCenter == null) {
ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
}
if (this.configCenter != null) {
// TODO there may have duplicate refresh
// 1. 刷新配置中心,按照优先级合并配置信息,因为配置文件具有优先级,系统配置优先级最高,如下配置顺序
// isConfigCenterFirst = true : SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
// isConfigCenterFirst = false : SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
this.configCenter.refresh();
// 2. 环境准备,读取配置中心的配置加载到环境中
prepareEnvironment();
}
// 3. 刷新全部配置,将外部配置中心的配置应用到本地
ConfigManager.getInstance().refreshAll();
}
这里可以分为三步:
下面我们来详细分析上面几步的实现:
this.configCenter.refresh();
调用的是 AbstractConfig#refresh
。 这里主要是将当前配置中心的属性与已存在的配置中心属性按照配置优先级的规则进行属性合并。
public void refresh() {
try {
// getPrefix() 默认值为 dubbo.config-center。
// 1. 这里从 当前的配置级中获取 dubbo.config-center 前缀的复合配置,即获取已存在的配置中心的属性
CompositeConfiguration compositeConfiguration = Environment.getInstance().getConfiguration(getPrefix(), getId());
// 2. 构建当前配置中心的配置类 InmemoryConfiguration
InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
config.addProperties(getMetaData());
// 3. 按照规则配置将现在的配置中心属性添加到复合配置中。
if (Environment.getInstance().isConfigCenterFirst()) {
// The sequence would be: SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
compositeConfiguration.addConfiguration(3, config);
} else {
// The sequence would be: SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
compositeConfiguration.addConfiguration(1, config);
}
// loop methods, get override value and set the new value back to method
// 4. 和复合配置中的属性合并
Method[] methods = getClass().getMethods();
for (Method method : methods) {
if (ClassHelper.isSetter(method)) {
try {
String value = compositeConfiguration.getString(extractPropertyName(getClass(), method));
// isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
if (StringUtils.isNotEmpty(value) && ClassHelper.isTypeMatch(method.getParameterTypes()[0], value)) {
method.invoke(this, ClassHelper.convertPrimitive(method.getParameterTypes()[0], value));
}
} catch (NoSuchMethodException e) {
logger.info("Failed to override the property " + method.getName() + " in " +
this.getClass().getSimpleName() +
", please make sure every property has getter/setter method provided.");
}
}
}
} catch (Exception e) {
logger.error("Failed to override ", e);
}
}
我们按照上面代码的注释分析:
关于上面的复合配置 :
由于 Dubbo 中存在很多作用域的配置,如注册中心的配置、配置中心的配置、服务接口的配置等, Dubbo将这些配置保存到 Environment 中,不同的配置存在不同前缀,如配置中心的前缀 dubbo.config-center、监控中心的前缀dubbo.monitor 等。当需要加载不同的配置时只需要指定前缀,如果配置精确到服务级别则使用 id来区分不同的服务。又由于 Dubbo 相同配置间存在优先级。所以在 Environment 中每个优先级存在一个 Map,而在上面的代码中,我们看到,如果设置了 configCenterFirst = true。则优先级为 SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
否则为 SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
在 Environment 中每个优先级声明为 一个 Map,其key 是作用域,value 为对应配置。下图为Environment 中定义的配置Map,其中 systemConfigs 是系统级别配置、externalConfigs 和 appExternalConfigs 是配置中心外部化配置,propertiesConfigs 是 属性配置等。
那么这里 我们再来看看这里 Environment#getConfiguration的实现如下:
public CompositeConfiguration getConfiguration(String prefix, String id) {
CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
// Config center has the highest priority
compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
return compositeConfiguration;
}
这里可以看到,Environment#getConfiguration 返回的符合保存了多个级别针对于 prefix + id 的配置。CompositeConfiguration 使用 一个 List 保存添加的配置。
在上一步完成了对配置中心属性的合并。这一步则开始加载配置中心的内容,其逻辑还是比较清晰的,根据配置中心属性获取到配置中心实例后,将配置中心中的 dubbo.properties 配置读取出来并保存到 Environment 中。其实现如下:
// org.apache.dubbo.config.AbstractInterfaceConfig#prepareEnvironment
private void prepareEnvironment() {
if (configCenter.isValid()) {
if (!configCenter.checkOrUpdateInited()) {
return;
}
// 1. 获取动态配置中心
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
// 2. 获取 global 的配置,在 dubbo/config/dubbo/dubbo.properties 中
String configContent = dynamicConfiguration.getConfig(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = application != null ? application.getName() : null;
String appConfigContent = null;
if (StringUtils.isNotEmpty(appGroup)) {
// 3. 获取 application 级别的 配置. 在 dubbo/config/当前应用名/dubbo.properties 中
appConfigContent = dynamicConfiguration.getConfig
(StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
// 同步配置信息
// 设置优先级
Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
// 更新 Environment global 的外部配置。 将配置信息保存到了Environment#appExternalConfigurationMap中
Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
// 更新 Environment application 作用域的 外部配置。将配置信息保存到了Environment#externalConfigurationMap 中
Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
}
}
// 根据协议类型获取动态配置的 实现类
private DynamicConfiguration getDynamicConfiguration(URL url) {
// 获取 DynamicConfigurationFactory spi 实现类
DynamicConfigurationFactory factories = ExtensionLoader
.getExtensionLoader(DynamicConfigurationFactory.class)
.getExtension(url.getProtocol());
// 获取动态配置
DynamicConfiguration configuration = factories.getDynamicConfiguration(url);
// 动态配置信息设值到 Environment
Environment.getInstance().setDynamicConfiguration(configuration);
return configuration;
}
这里可以看到,全局配置作为 externalConfiguration 配置保存,应用配置作为 appExternalConfiguration 保存。在后面会根据优先级判定使用那个配置。
加载完成配置中心的配置后则开始刷新所有的配置,以应用配置中心中的配置。这里则是调用了各个部分的refresh 来应用配置,具体不再详述。
public void refreshAll() {
// refresh all configs here,
getApplication().ifPresent(ApplicationConfig::refresh);
getMonitor().ifPresent(MonitorConfig::refresh);
getModule().ifPresent(ModuleConfig::refresh);
getProtocols().values().forEach(ProtocolConfig::refresh);
getRegistries().values().forEach(RegistryConfig::refresh);
getProviders().values().forEach(ProviderConfig::refresh);
getConsumers().values().forEach(ConsumerConfig::refresh);
}
总结一下:在服务启动时会检查缺省参数,在这个过程中会检查是否配置了配置中心,如果设置了,则会去读取配置中心中的配置文件,并将读取到的内容加载到本地环境作为之后操作的参数。
从2.7.0 版本开始,Dubbo同时支持应用、服务两种粒度的服务治理规则,对于这两种粒度,其key取值规则如下:
{应用名 + 规则后缀}
。如: demo-application.configurators、demo-application.tag-router等{服务接口名:[服务版本]:[服务分组] + 规则后缀}
,其中服务版本、服务分组是可选的,如果它们有配置则在key中体现,没被配置则用":“占位。如org.apache.dubbo.demo.DemoService::.configurators、org.apache.dubbo.demo.DemoService:1.0.0:group1.configurators。和之前外部化配置中心不同的是,这里的服务治理是动态更新的,也就是说我们这边修改后,服务会立刻感知到,这是因为无论是消费者还是提供者,在服务启动的时候就对配置节点进行了监听。当节点发生变化时,服务监听器可以感知,并重置更新配置。
我们可以通过dubbo-admin 对服务运行时的一些属性进行配置,如路由规则、权重调整等,如下图Dubbo-Admin 服务治理提供了如下选择:
我们可以通过 dubbo-admin-0.2.0 建立动态配置,其在zk节点的映射如下:
关于动态配置的具体的配置规则详参 : https://dubbo.apache.org/zh/docs/v2.7/user/examples/config-rule/
下面我们分为提供者和消费者两个方面来看具体实现:
虽然dubbo-admin 提供了服务治理的诸多功能,但是对于提供者来说只需要关注动态配置的内容即可。因为条件路由、便签路由、黑白名单、权重调整、负载均衡都是在集群层面的说法,是消费者需要才需要关注的。
在 Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export 中,我们提到过,提供者会通过 RegistryProtocol#export 来暴露服务,并且在暴露服务时会订阅自身节点以便接收到动态配置的更新,其订阅的节点为 /dubbo/服务名/configurators。
以上图为例其订阅节点为 /dubbo/com.kingfish.service.impl.DemoService/configurators
。也就是说,当 /dubbo/com.kingfish.service.impl.DemoService/configurators
节点更新后,提供者就可以感知到并对本地的配置进行更新。
但当我们使用,dubbo-admin-0.2.0 来创建动态配置时,dubbo-admin-0.2.0 会调用 OverrideServiceImpl#saveOverride 来创建节点。如下(该代码是 dubbo-admin-0.2.0 代码):
// 该代码是 dubbo-admin-0.2.0 代码
@java.lang.Override
public void saveOverride(DynamicConfigDTO override) {
String id = ConvertUtil.getIdFromDTO(override);
String path = getPath(id);
String exitConfig = dynamicConfiguration.getConfig(path);
List<OverrideConfig> configs = new ArrayList<>();
OverrideDTO existOverride = new DynamicConfigDTO2OverrideDTOAdapter(override);
if (exitConfig != null) {
existOverride = YamlParser.loadObject(exitConfig, OverrideDTO.class);
if (existOverride.getConfigs() != null) {
for (OverrideConfig overrideConfig : existOverride.getConfigs()) {
if (Constants.CONFIGS.contains(overrideConfig.getType())) {
configs.add(overrideConfig);
}
}
}
}
configs.addAll(override.getConfigs());
existOverride.setEnabled(override.isEnabled());
existOverride.setConfigs(configs);
// 创建或更新 /dubbo/config/{服务分组}/{服务接口}:{服务版本}/configurators 节点内容
dynamicConfiguration.setConfig(path, YamlParser.dumpObject(existOverride));
//for2.6
if (StringUtils.isNotEmpty(override.getService())) {
List<Override> result = convertDTOtoOldOverride(override);
for (Override o : result) {
// 创建或更新 /dubbo/{接口名称}/configurators 节点内容
registry.register(o.toUrl().addParameter(Constants.COMPATIBLE_CONFIG, true));
}
}
}
上面代码会创建(更新)两个节点,映射到zk 上的节点如下图:
即 dubbo-admin-0.2.0 的服务治理会创建(或更新)两个节点:
/dubbo/{接口名称}/规则后缀
下面的节点的内容,兼容dubbo2.6版本,如上图2标注。/dubbo/config/{服务分组}/{服务接口}:{服务版本}/规则后缀
节点。该种方式是dubbo 2.7版本更新方式,如上图1标注。(配置中心是 zk的情况下该节点并不会起到作用,而是使用dubbo2.6版本的方式来更新配置,具体原因下面分析)第一种方式 我们在之前的文章中已经分析,这里仅看 dubbo 2.7.0 版本的方式:
对于上面第二种方式,服务提供者通过 ProviderConfigurationListener 和 ServiceConfigurationListener 来监听 dubbo-admin-0.2.0 的服务治理内容。这两个监听器类都是 RegistryProtocol 内部类。其区别如下:
ServiceConfigurationListener 和 ProviderConfigurationListener 都继承于AbstractConfiguratorListener,所以我们这里来看一下 AbstractConfiguratorListener 的实现:
AbstractConfiguratorListener 类非常关键,其实现如下:
public abstract class AbstractConfiguratorListener implements ConfigurationListener {
private static final Logger logger = LoggerFactory.getLogger(AbstractConfiguratorListener.class);
protected List<Configurator> configurators = Collections.emptyList();
protected final void initWith(String key) {
// 获取动态配置中心
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
// 监听 key 的节点变化,监听器为自身,当key节点发生变化时会触发 AbstractConfiguratorListener#process
dynamicConfiguration.addListener(key, this);
// 获取 key 指定的配置内容,如果不为空,则手动触发process 来刷新配置
String rawConfig = dynamicConfiguration.getConfig(key);
if (!StringUtils.isEmpty(rawConfig)) {
process(new ConfigChangeEvent(key, rawConfig));
}
}
@Override
public void process(ConfigChangeEvent event) {
if (logger.isInfoEnabled()) {
logger.info("Notification of overriding rule, change type is: " + event.getChangeType() +
", raw config content is:\n " + event.getValue());
}
// 删除事件则清除缓存的配置
if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
configurators.clear();
} else {
try {
// parseConfigurators will recognize app/service config automatically.
// 否则对配置进行解析,保存到 configurators 中
configurators = Configurator.toConfigurators(ConfigParser.parseConfigurators(event.getValue()))
.orElse(configurators);
} catch (Exception e) {
logger.error("Failed to parse raw dynamic config and it will not take effect, the raw config is: " +
event.getValue(), e);
return;
}
}
// 通知事件,供子类实现
notifyOverrides();
}
protected abstract void notifyOverrides();
// 获取当前配置信息
public List<Configurator> getConfigurators() {
return configurators;
}
public void setConfigurators(List<Configurator> configurators) {
this.configurators = configurators;
}
}
我们这里要注意:
在本篇中,配置中心使用的是 zk,所以这里 DynamicConfiguration 的实现类是 ZookeeperDynamicConfiguration,其部分代码如下:
public class ZookeeperDynamicConfiguration implements DynamicConfiguration {
// The final root path would be: /configRootPath/"config"
// rootPath 获取规则是 url.getParameter("config.namespace", "dubbo") + "/config" 。
// "config.namespace" 默认为 dubbo
private String rootPath;
// 节点监听器。ZookeeperDynamicConfiguration 对 zk 的节点监听依赖于CacheListener
// 当 rootPath 节点有变化时会触发 CacheListener#childEvent 方法。
private CacheListener cacheListener;
....
ZookeeperDynamicConfiguration(URL url) {
this.url = url;
rootPath = "/" + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
...
this.cacheListener = new CacheListener(rootPath, initializedLatch);
...
}
在 ZookeeperDynamicConfiguration 中我们关注下面几个内容:
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
// 被 / 够至少大于 5 才会被处理。因为一个合法的节点 规则应该为 /dubbo/config/应用名/规则后缀 或 /dubbo/config/分组/接口:版本/规则后缀。
if (data.getPath().split("/").length >= 5) {
byte[] value = data.getData();
// 按照规则转换路径,
// 规则为 path.replace(rootPath + "/", "").replaceAll("/", ".") :剔除 rootPath 并把 / 替换为 .
// 如 /dubbo/config/spring-dubbo-provider/configurators 会转化为 spring-dubbo-provider.configurators
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);
// 从 keyListeners 中获取监听此节点(key) 的监听器,触发其监听事件
Set<ConfigurationListener> listeners = keyListeners.get(key);
if (CollectionUtils.isNotEmpty(listeners)) {
listeners.forEach(listener -> listener.process(configChangeEvent));
}
}
}
这里注意:
下面我们来看看 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());
}
}
ProviderConfigurationListener 针对应用级别的服务,由于一个服务只能有一个应用名,所以在 RegistryProtocol 中 ProviderConfigurationListener 作为属性直接初始化。
private final ProviderConfigurationListener providerConfigurationListener = new ProviderConfigurationListener();
ServiceConfigurationListener 的实现 和 ProviderConfigurationListener 类似
private class ServiceConfigurationListener extends AbstractConfiguratorListener {
private URL providerUrl;
private OverrideListener notifyListener;
public ServiceConfigurationListener(URL providerUrl, OverrideListener notifyListener) {
this.providerUrl = providerUrl;
this.notifyListener = notifyListener;
// 传入 监听的 key。
this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX);
}
// 获取现有配置规则,并在导出之前覆盖提供程序URL。当服务发布时会调用此方法来对配置进行刷新。
private <T> URL overrideUrl(URL providerUrl) {
return RegistryProtocol.getConfigedInvokerUrl(configurators, providerUrl);
}
// 当监听的节点更新后会触发 notifyListener.doOverrideIfNecessary 来刷新本地 url 并重新发布
@Override
protected void notifyOverrides() {
notifyListener.doOverrideIfNecessary();
}
}
ServiceConfigurationListener 针对服务级别的服务,由于可以存在多个服务,所以在 RegistryProtocol中 ServiceConfigurationListener 是以map 的形式存在。其中key对应的是某个服务的唯一值,value对应的是针对该服务的监听器。
private final Map<String, ServiceConfigurationListener> serviceConfigurationListeners = new ConcurrentHashMap<>();
以下是本人推测,正确性无法保证:
在 ServiceConfigurationListener 的构造函数,其中 this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX);
中 URL#getEncodedServiceKey 的实现如下:
public String getEncodedServiceKey() {
String serviceKey = this.getServiceKey();
serviceKey = serviceKey.replaceFirst("/", "*");
return serviceKey;
}
这里会将 第一个 / 替换成 * 。这里导致服务的规则是 {分组}*{服务接口}:{版本号}.{规则后缀}
,而 CacheListener#pathToKey 的规则是 {分组}.{服务接口}:{版本号}.{规则后缀}
。举下面一个例子:
com.kingfish.service.impl.DemoService:1.0.0
,分组为 main
。this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX);
即 main*com.kingfish.service.impl.DemoService:1.0.0.configurators
。/dubbo/config/main/com.kingfish.service.impl.DemoService:1.0.0/configurators
/dubbo/config/main/com.kingfish.service.impl.DemoService:1.0.0/configurators
节点发生改变后(dubbo-admin 修改了服务级别的动态配置),会触发 CacheListener#childEvent
方法, CacheListener#childEvent
方法会调用 CacheListener#pathToKey
方法按照规则将 上面的 zk 路径转化为 main.com.kingfish.service.impl.DemoService:1.0.0.configurators
, 并使用此作为key去 CacheListener#keyListeners
中寻找监听此节点的监听器。main*com.kingfish.service.impl.DemoService:1.0.0.configurators
。这就导致,由于无法匹配上监听的key,ServiceConfigurationListener#notifyOverrides 就无法被触发。即 ServiceConfigurationListener 没有完成这种情况下服务治理功能。即总结:
dubbo/{接口名}/{规则节点}
来实现的,当 dubbo-admin 进行服务治理后,dubbo-admin 会修改zk 上对应的 /dubbo/{接口名}/{规则节点}
节点信息。而对应的接口服务会监听 /dubbo/{接口名}/{规则节点}
节点,当节点被修改时服务会感知到从而刷新自身配置。而应用级别的服务治理,dubbo 2.6 版本不支持。在RegistryProtocol 中 serviceConfigurationListeners
和 providerConfigurationListener
的构造如下,由于一个服务只存在一个应用名,所以应用级别的监听ProviderConfigurationListener 在服务初始化时就创建,而一个服务可以发布多个服务名,所以这里使用Map 结构来保存 服务级别的监听 :
private final Map<String, ServiceConfigurationListener> serviceConfigurationListeners = new ConcurrentHashMap<>();
private final ProviderConfigurationListener providerConfigurationListener = new ProviderConfigurationListener();
我们知道,提供者会通过 RegistryProtocol#export 来暴露服务,并且在暴露服务时会订阅自身节点以便接收到动态配置的更新,其订阅的节点为 /dubbo/服务名/configurators。
下面我们来看一下这部分代码:
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 生成订阅的 url
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 订阅 overrideSubscribeUrl 的监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 添加监听器,从配置中心获取配置并应用。
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
....
// 进行订阅,当 overrideSubscribeUrl 更新时会触发 overrideSubscribeListener 监听器
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
....
return new DestroyableExporter<>(exporter);
}
其中会调用 RegistryProtocol#overrideUrlWithConfig 方法,该方法完成了配置同步 并且添加了服务监听。
private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) {
// 1. 获取应用级别现有配置规则,并在导出之前覆盖提供程序URL。
providerUrl = providerConfigurationListener.overrideUrl(providerUrl);
// 2. 初始化服务级别监听器,并保存到 serviceConfigurationListeners 中,完成了对当前服务的监听。
ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener);
serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener);
// 3. 获取服务级别现有配置规则,并在导出之前覆盖提供程序URL。
return serviceConfigurationListener.overrideUrl(providerUrl);
}
RegistryProtocol#overrideUrlWithConfig 添加了服务的监听,并进行了配置同步。这里我们可以总结为:
当动态配置发生改变后,监听器接收到感知后会触发RegistryProtocol.OverrideListener#doOverrideIfNecessary
方法,该方法会合并配置,如果配置发生改变,则会重新发布服务。其实现如下,其实现如下:
public synchronized void doOverrideIfNecessary() {
final Invoker<?> invoker;
....
//The current, may have been merged many times
// 获取当前 url
URL currentUrl = exporter.getInvoker().getUrl();
//Merged with this configuration
// 合并 /dubbo/接口名/configurators 节点下的配置
URL newUrl = getConfigedInvokerUrl(configurators, originUrl);
// 合并 /dubbo/config/分组/接口:版本/configurators 节点的配置
newUrl = getConfigedInvokerUrl(serviceConfigurationListeners.get(originUrl.getServiceKey())
.getConfigurators(), newUrl);
// 合并 /dubbo/config/应用名/configurators 节点的配置
newUrl = getConfigedInvokerUrl(providerConfigurationListener.getConfigurators(), newUrl);
// 经过上面三次合并后,如果url变化了,则说明有配置更新,则重新发布
if (!currentUrl.equals(newUrl)) {
RegistryProtocol.this.reExport(originInvoker, newUrl);
logger.info("exported provider url changed, origin url: " + originUrl +
", old export url: " + currentUrl + ", new export url: " + newUrl);
}
}
我们这里来总结一下提供者端的流程:
消费者端的实现和 提供者类似,也是依赖于两个监听器类,如下:
2.1.4 一点推测
中所说的问题,也就是说 在以zk 为配置中心的情况下,动态配置并非通过RegistryDirectory#ReferenceConfigurationListener 完成,而是通过兼容的 dubbo 2.6 版本 来完成。ConsumerConfigurationListener 和 ReferenceConfigurationListener 也都继承了 AbstractConfiguratorListener ,逻辑也基本类似。具体实现如下。
ConsumerConfigurationListener 实现如下:
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);
}
// 通知节点刷新,触发了 RegistryDirectory#refreshInvoker,刷新引用的服务的本地配置
@Override
protected void notifyOverrides() {
listeners.forEach(listener -> listener.refreshInvoker(Collections.emptyList()));
}
}
当消费者启动后会订阅自身 应用级别的动态配置信息。当应用配置发生改变后会逐一刷新自身引用的服务的本地配置。
ReferenceConfigurationListener 实现如下:
private static class ReferenceConfigurationListener extends AbstractConfiguratorListener {
private RegistryDirectory directory;
private URL url;
ReferenceConfigurationListener(RegistryDirectory directory, URL url) {
this.directory = directory;
this.url = url;
// 订阅引用服务的 服务级别配置,这里存在和提供者一样的问题,该问题在 2.1.4 一点推测里进行了介绍
this.initWith(url.getEncodedServiceKey() + Constants.CONFIGURATORS_SUFFIX);
}
// 通知节点刷新,触发了 RegistryDirectory#refreshInvoker
@Override
protected void notifyOverrides() {
// to notify configurator/router changes
// 通知有配置或路由改变,这里应该只能监听到有动态配置改变
directory.refreshInvoker(Collections.emptyList());
}
}
这里可以看到,配置节点更新会,触发了 RegistryDirectory#refreshInvoker,而完成了本地服务缓存的更新。
在 RegistryDirectory#subscribe 方法中 二者完成了订阅功能,如下:
public void subscribe(URL url) {
setConsumerUrl(url);
// 1. 订阅本应用的配置,当本应用的配置更新时会触发 RegistryDirectory的监听回调方法 refreshInvoker 来刷新本地缓存的配置
consumerConfigurationListener.addNotifyListener(this);
// 2. 订阅引入的服务的配置,当引入动态配置更新时会触发 RegistryDirectory的监听回调方法 refreshInvoker 来刷新本地缓存的配置
serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
// 订阅当前服务
registry.subscribe(url, this);
}
当消费者引用某一个服务时会创建其代理对象,在创建过程中消费者会从订阅如下节点,当节点变化时则会触发其监听回调。而 RegistryDirectory#subscribe 则是订阅的过程。
/dubbo/服务名/providers
/dubbo/服务名/routers
/dubbo/服务名/congurators
消费者的启动过程详参:
当监听节点发生变化时会触发 RegistryDirectory#refreshInvoker 方法。该方法详参Dubbo笔记 ⑨ : 消费者启动流程 - RegistryProtocol#refer
这里简单看一下:RegistryDirectory#refreshInvoker 中会调用 RegistryDirectory#toInvokers 将 url 转换为 Invoker。其中会调用 RegistryDirectory#mergeUrl 方法来将 参数合并,其实现如下:
private URL mergeUrl(URL providerUrl) {
// 合并消费者端的参数
providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap); // Merge the consumer side parameters
// 合并动态配置的参数
providerUrl = overrideWithConfigurator(providerUrl);
// 不检查连接是否成功,一直创建Invoker
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;
}
其中我们看到 providerUrl = overrideWithConfigurator(providerUrl);
合并了动态配置的参数,其实现如下:
private URL overrideWithConfigurator(URL providerUrl) {
// override url with configurator from "override://" URL for dubbo 2.6 and before
//兼容 2.6 版本的配置合并
providerUrl = overrideWithConfigurators(this.configurators, providerUrl);
// override url with configurator from configurator from "app-name.configurators"
// 这里通过 consumerConfigurationListener 获取消费者应用级别的动态配置并覆盖
providerUrl = overrideWithConfigurators(consumerConfigurationListener.getConfigurators(), providerUrl);
// override url with configurator from configurators from "service-name.configurators"
if (serviceConfigurationListener != null) {
// 如果有针对于当前引用服务的服务级别动态配置,则使用该配置进行合并。
providerUrl = overrideWithConfigurators(serviceConfigurationListener.getConfigurators(), providerUrl);
}
return providerUrl;
}
dubbo:
application:
name: spring-dubbo-provider
monitor: http://localhost:8080
registry:
address: zookeeper://localhost:2181
# 配置中心
config-center:
# 指定nacos
address: nacos://localhost:8848
# 指定命名空间
namespace: 4d2e74c1-b983-4054-b03b-c65eb7cedb78
当 nacos 配置改变后会触发
NacosDynamicConfiguration.NacosConfigListener#receiveConfigInfo
-> NacosDynamicConfiguration.NacosConfigListener#innerReceive
-> AbstractConfiguratorListener#process
-> AbstractConfiguratorListener#notifyOverrides
-> 触发 dubbo的刷新操作
以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://dubbo.apache.org/zh/docs/v2.7/dev/impls/config-center/
https://blog.csdn.net/huanglei_hacker/article/details/107127558
https://blog.csdn.net/u012881904/article/details/95891448
https://blog.csdn.net/huanglei_hacker/article/details/107127558
https://segmentfault.com/a/1190000039947964?utm_source=sf-similar-article
https://github.com/apache/dubbo-admin/wiki/服务治理兼容性说明
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正