10.1 三大中心介绍
作为一个微服务框架,Dubbo SDK 跟随着微服务组件被部署在分布式集群下的各个位置,为了在分布式环境下实现各个微服务组件间的协作,Dubbo 定义了一些中心化组件,包括如下:
- 注册中心:协调 Consumer、Provider 之间的地址注册与发现
- 配置中心:负责 Dubbo 启动阶段的全局配置,保证配置的跨环境共享与全局一致性以及负责服务治理规则(路由规则、动态配置等)的存储与推送
- 元数据中心:接收 Provider 上报的服务接口元数据,为 Admin 等控制台提供运维能力(如服务测试、接口文档等)同时可以作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力,相当于注册中心的额外扩展
10.2 配置中心简介
对于传统的单体应用而言,常使用配置文件来管理所有配置,比如 SpringBoot application.yml 文件,但是在微服务架构中全部手动修改的话很麻烦而且不易维护。微服务的配置管理一般有以下需求:
- 集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的
- 不同环境不同配置,比如数据源配置在不同环境(开发,生产,测试)中是不同的
- 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等
- 配置修改后可自动更新。如配置内容发生变化,微服务可以自动更新配置
综上所述对于微服务架构而言,一套统一的、通用的管理配置机制是不可缺少的主要组成部分。常见的做法就是通过配置服务器进行管理
配置中心(config-center)在 Dubbo 中可承担两类职责:
- 外部化配置:启动配置的集中式存储(简单理解就是 dubbo.properties 的外部化存储)
- 流量治理规则存储
值得注意的是 Dubbo 动态配置中心定义了两个不同层次的隔离选项,分别是 namespace 和 group
- namespace:配置命名空间,默认值 dubbo。命名空间通常用于多租户隔离,即对不同用户、不同环境或完全不关联的一系列配置进行逻辑隔离,区别于物理隔离的点是不同的命名空间使用的还是同一套物理集群
- group:配置分组,默认值 dubbo。group 通常用于归类一组相同类型/目的配置项,是对 namespace 下配置项的进一步隔离
使用注册中心作为默认配置中心
在使用 Zookeeper、Nacos 作为注册中心且没有显式配置中心的情况下,Dubbo 框架默认会将 Zookeeper、Nacos 用作配置中心,用作服务治理用途
配置中心与其他两大中心不同,它无关于接口级还是应用级,与接口并没有对应关系,它仅仅与配置数据有关,即使没有部署注册中心和元数据中心,配置中心也能直接被接入到 Dubbo 应用服务中。
在整个部署架构中,整个集群内的实例(无论是 Provider 还是 Consumer)都将会共享配置中心集群中的配置,如下图所示:
该图中不配备注册中心,意味着可能采用了 Dubbo Mesh 方案,也可能不需要进行服务注册,仅仅接收直连模式的服务调用
该图中不配备元数据中心,意味着 Consumer 可以从 Provider 暴露的 MetadataService 获取服务元数据,从而实现 RPC 调用
10.3 启动配置中心
在上篇博客【9-DubboBootstrap 服务启动的生命周期】分析 DefaultApplicationDeployer#initialize 方法生命周期时,在初始化方法通过调用 startConfigCenter 方法来启动加载配置中心,下面来详细看下该方法的实现源码:
private void startConfigCenter() {
// load application config
// 配置可能有多个地方可以配置,需要遵循 Dubbo 约定的优先级进行设置,也可能是多应用、多注册中心配置
configManager.loadConfigsOfTypeFromProps(ApplicationConfig.class);
// try set model name
if (StringUtils.isBlank(applicationModel.getModelName())) {
// 设置一下模块名字和模块描述(我们再Debug里面经常会看到这个描述信息 toString 直接返回了 Dubbo 为我们改造的对象信息)
applicationModel.setModelName(applicationModel.tryGetApplicationName());
}
// load config centers
configManager.loadConfigsOfTypeFromProps(ConfigCenterConfig.class);
// 兼容旧版本,如果注册中心没有配置,则使用注册中心作为配置中心
useRegistryAsConfigCenterIfNecessary();
// check Config Center
Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();
// 如果没有配置中心,则创建一个默认的配置中心
if (CollectionUtils.isEmpty(configCenters)) {
ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();
configCenterConfig.setScopeModel(applicationModel);
configCenterConfig.refresh();
// 校验配置中心
ConfigValidationUtils.validateConfigCenterConfig(configCenterConfig);
if (configCenterConfig.isValid()) {
// 添加到配置中心管理器
configManager.addConfigCenter(configCenterConfig);
configCenters = configManager.getConfigCenters();
}
} else {
// 一个或多个配置中心存在情况下,对每个配置中心进行刷新和校验
for (ConfigCenterConfig configCenterConfig : configCenters) {
configCenterConfig.refresh();
ConfigValidationUtils.validateConfigCenterConfig(configCenterConfig);
}
}
// 配置中心存在情况下
if (CollectionUtils.isNotEmpty(configCenters)) {
// 创建一个组合配置中心,用于获取配置中心数据
CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
for (ConfigCenterConfig configCenter : configCenters) {
// Pass config from ConfigCenterBean to environment
// 将配置中心的外部化配置数据添加到环境变量中
environment.updateExternalConfigMap(configCenter.getExternalConfiguration());
// 将配置中心的应用配置数据添加到环境变量中
environment.updateAppExternalConfigMap(configCenter.getAppExternalConfiguration());
// Fetch config from remote config center
// 从配置中心拉取配置添加到组合配置中
compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
}
// 将组合配置中心添加到环境变量中
environment.setDynamicConfiguration(compositeDynamicConfiguration);
}
}
10.3.1 配置管理器加载配置
configManager.loadConfigsOfTypeFromProps(ApplicationConfig.class);
configManager.loadConfigsOfTypeFromProps(ConfigCenterConfig.class);
第一行代码其实就是调用了 AbstractConfigManager#loadConfigsOfTypeFromProps 方法加载配置管理器,会从系统属性中加载配置,这里来详细看下这块的配置,它是我们使用者比较关注的内容
public <T extends AbstractConfig> List<T> loadConfigsOfTypeFromProps(Class<T> cls) {
List<T> tmpConfigs = new ArrayList<>();
// 获取属性配置 dubbo properties in classpath
PropertiesConfiguration properties = environment.getPropertiesConfiguration();
// load multiple configs with id
/*
多注册中心配置id查询,搜索属性并提取指定类型的配置ID
例如如下配置
# 配置信息 properties
dubbo.registries.registry1.address=xxx
dubbo.registries.registry2.port=xxx
# 提取配置的 id extract
Set configIds = getConfigIds(RegistryConfig.class)
# 提取的配置id结果 result
configIds: ["registry1", "registry2"]
*/
Set<String> configIds = this.getConfigIdsFromProps(cls);
configIds.forEach(id -> {
// 遍历这些配置 id 判断配置缓存(configsCache 成员变量)中是否已经存在当前配置
if (!this.getConfig(cls, id).isPresent()) {
T config;
try {
// 创建配置对象 为配置对象初始化配置id
config = createConfig(cls, scopeModel);
config.setId(id);
} catch (Exception e) {
throw new IllegalStateException(
"create config instance failed, id: " + id + ", type:" + cls.getSimpleName());
}
String key = null;
boolean addDefaultNameConfig = false;
try {
// add default name config (same as id), e.g. dubbo.protocols.rest.port=1234
key = DUBBO + "." + AbstractConfig.getPluralTagName(cls) + "." + id + ".name";
if (properties.getProperty(key) == null) {
properties.setProperty(key, id);
addDefaultNameConfig = true;
}
// 刷新配置信息 好理解点就是 Dubbo 配置属性重写
config.refresh();
// 将当前配置信息添加到配置缓存中 configsCache 成员变量
this.addConfig(config);
tmpConfigs.add(config);
} catch (Exception e) {
logger.error(
COMMON_PROPERTY_TYPE_MISMATCH,
"",
"",
"load config failed, id: " + id + ", type:" + cls.getSimpleName(),
e);
throw new IllegalStateException("load config failed, id: " + id + ", type:" + cls.getSimpleName());
} finally {
if (addDefaultNameConfig && key != null) {
properties.remove(key);
}
}
}
});
// If none config of the type, try load single config
// 如果没有该类型的配置,请尝试加载单个配置
if (this.getConfigs(cls).isEmpty()) {
// load single config
List<Map<String, String>> configurationMaps = environment.getConfigurationMaps();
if (ConfigurationUtils.hasSubProperties(configurationMaps, AbstractConfig.getTypePrefix(cls))) {
T config;
try {
config = createConfig(cls, scopeModel);
config.refresh();
} catch (Exception e) {
throw new IllegalStateException(
"create default config instance failed, type:" + cls.getSimpleName(), e);
}
this.addConfig(config);
tmpConfigs.add(config);
}
}
return tmpConfigs;
}
假设我们加载的配置类型是 org.apache.dubbo.config.ConfigCenterConfig,其实通过以下的代码解析出来具体要加载的配置前缀
private Set<String> getConfigIdsFromProps(Class<? extends AbstractConfig> clazz) {
// dubbo.applications.
String prefix = CommonConstants.DUBBO + "." + AbstractConfig.getPluralTagName(clazz) + ".";
return ConfigurationUtils.getSubIds(environment.getConfigurationMaps(), prefix);
}
AbstractConfig#getPluralTagName 方法的用处非常有意思,它会获取我们的类名称,然后匹配它最后的一个字符给它赋予复数的形式进行返回,改 y -> ies、改 s -> es、无 y 无 s 直接加 s,所以最终返回的前缀 prefix 就是 dubbo.config-centers.
public static String getPluralTagName(Class<?> cls) {
// 截取掉尾部的 Config, Bean, ConfigBase 字符
String tagName = getTagName(cls);
if (tagName.endsWith("y")) {
// e.g. registry -> registries
return tagName.substring(0, tagName.length() - 1) + "ies";
} else if (tagName.endsWith("s")) {
// e.g. metrics -> metricses
return tagName + "es";
}
return tagName + "s";
}
在介绍 ConfigurationUtils#getSubIds 方法之前,先需要调用 Environment#getConfigurationMaps 方法作为全局配置集合 globalConfigurationMaps 加载好
public List<Map<String, String>> getConfigurationMaps() {
if (globalConfigurationMaps == null) {
globalConfigurationMaps = getConfigurationMaps(null, null);
}
return globalConfigurationMaps;
}
其实它的加载和获取配置类型其实就是如下图所示一样,除了 AbstractConfig 是特殊的个性化配置除外
10.3.2 默认使用注册中心作为配置中心
出现兼容性目的,若没有明确指定配置中心,并且 RegistryConfig 类的 useAsConfigCenter 属性为 null 或者 true,就会使用注册中心作为默认的配置中心,通过调用 DefaultApplicationDeployer#useRegistryAsConfigCenterIfNecessary 方法来处理逻辑,代码如下:
private void useRegistryAsConfigCenterIfNecessary() {
// we use the loading status of DynamicConfiguration to decide whether ConfigCenter has been initiated.
// 使用 DynamicConfiguration 加载状态来决定是否已启动 ConfigCenter
// 配置中心配置加载完成之后会初始化动态配置 defaultDynamicConfiguration
if (environment.getDynamicConfiguration().isPresent()) {
return;
}
// 从配置缓存中查询是否存在 config-center 相关配置
// 如果已经存在配置了就无需使用注册中心的配置地址直接返回
if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
return;
}
// load registry -> 加载注册中心相关配置
configManager.loadConfigsOfTypeFromProps(RegistryConfig.class);
// 查询是否有注册中心设置了默认配置 isDefault -> true 注册中心则为默认注册中心列表
// 如果没有注册中心设置为默认注册中心,则获取所有未设置默认配置的注册中心列表
List<RegistryConfig> defaultRegistries = configManager.getDefaultRegistries();
if (!defaultRegistries.isEmpty()) {
defaultRegistries.stream()
// 判断当前注册中心是否可以作为配置中心
.filter(this::isUsedRegistryAsConfigCenter)
// 将注册中心配置映射转换为配置中心
.map(this::registryAsConfigCenter)
.forEach(configCenter -> {
if (configManager.getConfigCenter(configCenter.getId()).isPresent()) {
return;
}
// 配置管理器中添加配置中心,方便后续读取配置中心的配置信息
configManager.addConfigCenter(configCenter);
logger.info("use registry as config-center: " + configCenter);
});
}
}
如何判断 registry 是否可以作为配置中心,通过 isUsedRegistryAsConfigCenter 方法来进行判别
private boolean isUsedRegistryAsCenter(
RegistryConfig registryConfig,
Supplier<Boolean> usedRegistryAsCenter,
String centerType,
Class<?> extensionClass) {
final boolean supported;
// 这个 usedRegistryAsCenter 参数是来自注册中心的配置
// 如果配置了这个值则以这个值为准,如果配置了 false 则这个注册中心不能做为配置中心
Boolean configuredValue = usedRegistryAsCenter.get();
if (configuredValue != null) { // If configured, take its value.
supported = configuredValue.booleanValue();
} else { // Or check the extension existence
// 这个逻辑的话是判断下注册中心的协议是否满足要求,我们例子代码中使用的是 nacos
String protocol = registryConfig.getProtocol();
// 这个扩展是否支持的逻辑判断是这样的扫描扩展类,看一下当前扩展类型是否有对应协议的扩展
// 比如在扩展文件里面配置 protocol=xxxImpl 过后就是支持的
// 动态配置的扩展类型为:interface org.apache.dubbo.common.config.configcenter.DynamicConfigurationFactory
// nacos 协议实现了这个动态配置工厂,这个扩展类型为 NacosDynamicConfigurationFactory
// 代码位置在 dubbo-configcenter-nacos: org.apache.dubbo.configcenter.support.nacos.NacosDynamicConfigurationFactory
// 扩展配置中内容为 nacos=org.apache.dubbo.configcenter.support.nacos.NacosDynamicConfigurationFactory
supported = supportsExtension(extensionClass, protocol);
if (logger.isInfoEnabled()) {
logger.info(format(
"No value is configured in the registry, the %s extension[name : %s] %s as the %s center",
extensionClass.getSimpleName(),
protocol,
supported ? "supports" : "does not support",
centerType));
}
}
// 配置中心走注册中心会打印一条日志
if (logger.isInfoEnabled()) {
logger.info(format(
"The registry[%s] will be %s as the %s center",
registryConfig, supported ? "used" : "not used", centerType));
}
return supported;
}
该扩展是否支持对应的扫描扩展类,主要就是看一下当前扩展类型是否有对应协议的扩展,比如在扩展文件里面配置 protocol=xxxImpl 过后就是支持的,动态配置的扩展类型为 org.apache.dubbo.common.config.configcenter.DynamicConfigurationFactory
Nacos 协议是支持的,因为 Nacos 协议实现了这个动态配置工厂,该扩展类型为 NacosDynamicConfigurationFactory,代码位置在 dubbo-configcenter-nacos 模块中的 org.apache.dubbo.common.config.configcenter.DynamicConfigurationFactory 扩展配置中的内容:
nacos=org.apache.dubbo.configcenter.support.nacos.NacosDynamicConfigurationFactory
10.3.3 注册中心配置转配置中心配置
注册中心转配置中心的主要逻辑是调用 DefaultApplicationDeployer#registryAsConfigCenter 方法实现的,源码如下:
private ConfigCenterConfig registryAsConfigCenter(RegistryConfig registryConfig) {
// 注册中心协议获取这里例子中的是 nacos 协议
String protocol = registryConfig.getProtocol();
// 注册中心端口 8848
Integer port = registryConfig.getPort();
// 在 Dubbo 中配置信息,这里转换后的地址为 nacos://127.0.0.1:8848
URL url = URL.valueOf(registryConfig.getAddress(), registryConfig.getScopeModel());
// 生成当前配置中心的 id 封装之后的内容为: config-center-nacos-127.0.0.1-8848
String id = "config-center-" + protocol + "-" + url.getHost() + "-" + port;
// 配置中心配置对象创建
ConfigCenterConfig cc = new ConfigCenterConfig();
// config-center-nacos-127.0.0.1-8848
cc.setId(id);
cc.setScopeModel(applicationModel);
if (cc.getParameters() == null) {
cc.setParameters(new HashMap<>());
}
if (CollectionUtils.isNotEmptyMap(registryConfig.getParameters())) {
cc.getParameters().putAll(registryConfig.getParameters()); // copy the parameters
}
cc.getParameters().put(CLIENT_KEY, registryConfig.getClient());
// nacos
cc.setProtocol(protocol);
// 8848
cc.setPort(port);
if (StringUtils.isNotEmpty(registryConfig.getGroup())) {
cc.setGroup(registryConfig.getGroup());
}
// 这个方法转换地址是修复 bug 用的可以看: https://github.com/apache/dubbo/issues/6476
cc.setAddress(getRegistryCompatibleAddress(registryConfig));
// 注册中心分组作为配置中心命名空间 这里为 null
cc.setNamespace(registryConfig.getGroup());
// nacos 认证信息
cc.setUsername(registryConfig.getUsername());
cc.setPassword(registryConfig.getPassword());
if (registryConfig.getTimeout() != null) {
cc.setTimeout(registryConfig.getTimeout().longValue());
}
// 这个属性注释中已经建议了已经弃用了默认就是 false 了,如果配置中心被赋予最高优先级,它将覆盖所有其他配置
cc.setHighestPriority(false);
return cc;
}
主要的转换逻辑就是将 RegistryConfig 注册配置中的属性转换为 ConfigCenterConfig 配置中心配置的属性,其中会生成一个对应的 ID 来标识,如上生成 ID 内容为 config-center-nacos-127.0.0.1-8848
10.4 配置中心刷新逻辑
当我们创建好 ConfigCenterConfig 以后,下一步其实就是刷新配置的核心逻辑了
public void refresh() {
if (needRefresh) {
try {
// check and init before do refresh
// 刷新之前执行的逻辑 这里并没做什么
preProcessRefresh();
refreshWithPrefixes(getPrefixes(), getConfigMode());
} catch (Exception e) {
logger.error(
COMMON_FAILED_OVERRIDE_FIELD,
"",
"",
"Failed to override field value of config bean: " + this,
e);
throw new IllegalStateException("Failed to override field value of config bean: " + this, e);
}
// 刷新之后执行的逻辑:主要是设置一些默认值及检查更新子配置信息
postProcessRefresh();
}
// 刷新动作完成
refreshed.set(true);
}
其中 preProcessRefresh、postProcessRefresh 方法都是提供给 ServiceConfigBase、ReferenceConfigBase 子类进行扩展实现的,主要关注的处理逻辑是在 refreshWithPrefixes 方法
protected void refreshWithPrefixes(List<String> prefixes, ConfigMode configMode) {
// 获取当前域模型的环境信息对象
Environment environment = getScopeModel().modelEnvironment();
ist<Map<String, String>> configurationMaps = environment.getConfigurationMaps();
// Search props starts with PREFIX in order
String preferredPrefix = null;
for (String prefix : prefixes) {
if (ConfigurationUtils.hasSubProperties(configurationMaps, prefix)) {
preferredPrefix = prefix;
break;
}
}
if (preferredPrefix == null) {
preferredPrefix = prefixes.get(0);
}
// Extract sub props (which key was starts with preferredPrefix)
Collection<Map<String, String>> instanceConfigMaps = environment.getConfigurationMaps(this, preferredPrefix);
Map<String, String> subProperties = ConfigurationUtils.getSubProperties(instanceConfigMaps, preferredPrefix);
InmemoryConfiguration subPropsConfiguration = new InmemoryConfiguration(subProperties);
if (logger.isDebugEnabled()) {
String idOrName = "";
if (StringUtils.hasText(this.getId())) {
idOrName = "[id=" + this.getId() + "]";
} else {
String name = ReflectUtils.getProperty(this, "getName");
if (StringUtils.hasText(name)) {
idOrName = "[name=" + name + "]";
}
}
logger.debug("Refreshing " + this.getClass().getSimpleName() + idOrName + " with prefix ["
+ preferredPrefix + "], extracted props: "
+ subProperties);
}
assignProperties(this, environment, subProperties, subPropsConfiguration, configMode);
// process extra refresh of subclass, e.g. refresh method configs
processExtraRefresh(preferredPrefix, subPropsConfiguration);
}
通过获取到的域模型环境中的配置项,进行匹配将满足特定的前缀配置项解析出来,然后通过 setXxx、setParameter 方法进行属性值填充到当前配置对象中
10.5 配置中心配置大全
ConfigCenterConfig 类型,下面配置信息来源于官网 dubbo:config-center 配置中心,对应的配置类:org.apache.dubbo.config.ConfigCenterConfig
属性 | 对应URL参数 | 类型 | 缺省值 | 描述 |
---|---|---|---|---|
protocol | config.protocol | string | zookeeper | 使用哪个配置中心:apollo、zookeeper、nacos等 以zookeeper为例 1. 指定protocol,则address可以简化为 127.0.0.1:2181 2. 不指定protocol,则address取值为 zookeeper://127.0.0.1:2181 |
address | config.address | string | 配置中心地址。 取值参见 protocol 说明 | |
highest-priority | config.highestPriority | boolean | true | 来自配置中心的配置项具有最高优先级,即会覆盖本地配置项 |
namespace | config.namespace | string | dubbo | 通常用于多租户隔离,实际含义视具体配置中心而不同 如: zookeeper - 环境隔离,默认值 dubbo apollo - 区分不同领域的配置集合,默认使用 dubbo 和application |
cluster | config.cluster | string | 含义视所选定的配置中心而不同。 如 Apollo 中用来区分不同的配置集群 | |
group | config.group | string | dubbo | 含义视所选定的配置中心而不同。 nacos - 隔离不同配置集 zookeeper - 隔离不同配置集 |
check | config.check | boolean | true | 当配置中心连接失败时,是否终止应用启动。 |
config-file | config.configFile | string | dubbo.properties | 全局级配置文件所映射到的 key |
timeout | config.timeout | integer | 3000ms | 获取配置的超时时间 |
username | string | 如果配置中心需要做校验,用户名 Apollo 暂未启用 | ||
password | string | 如果配置中心需要做校验,密码 Apollo 暂未启用 | ||
parameters | Map<string, string> | 扩展参数,用来支持不同配置中心的定制化配置参数 | ||
include-spring-env | boolean | false | 使用 Spring 框架时支持,为true时,会自动从Spring Environment中读取配置 默认依次读取 key 为 dubbo.properties 配置 key为 dubbo.properties PropertySource |
参考文献
https://cn.dubbo.apache.org/zh-cn/docs/concepts/registry-configcenter-metadata