10-Dubbo 三大中心之配置中心 ConfigCenter

作者: vnjohn / 发表于 2025-05-27 / 专栏: Dubbo 3.3

Dubbo3, 源码

10.1 三大中心介绍

作为一个微服务框架,Dubbo SDK 跟随着微服务组件被部署在分布式集群下的各个位置,为了在分布式环境下实现各个微服务组件间的协作,Dubbo 定义了一些中心化组件,包括如下:

  1. 注册中心:协调 Consumer、Provider 之间的地址注册与发现
  2. 配置中心:负责 Dubbo 启动阶段的全局配置,保证配置的跨环境共享与全局一致性以及负责服务治理规则(路由规则、动态配置等)的存储与推送
  3. 元数据中心:接收 Provider 上报的服务接口元数据,为 Admin 等控制台提供运维能力(如服务测试、接口文档等)同时可以作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力,相当于注册中心的额外扩展

image-20250518032339325

10.2 配置中心简介

对于传统的单体应用而言,常使用配置文件来管理所有配置,比如 SpringBoot application.yml 文件,但是在微服务架构中全部手动修改的话很麻烦而且不易维护。微服务的配置管理一般有以下需求:

  • 集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的
  • 不同环境不同配置,比如数据源配置在不同环境(开发,生产,测试)中是不同的
  • 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等
  • 配置修改后可自动更新。如配置内容发生变化,微服务可以自动更新配置

综上所述对于微服务架构而言,一套统一的、通用的管理配置机制是不可缺少的主要组成部分。常见的做法就是通过配置服务器进行管理

配置中心(config-center)在 Dubbo 中可承担两类职责:

  1. 外部化配置:启动配置的集中式存储(简单理解就是 dubbo.properties 的外部化存储)
  2. 流量治理规则存储

值得注意的是 Dubbo 动态配置中心定义了两个不同层次的隔离选项,分别是 namespace 和 group

  • namespace:配置命名空间,默认值 dubbo。命名空间通常用于多租户隔离,即对不同用户、不同环境或完全不关联的一系列配置进行逻辑隔离,区别于物理隔离的点是不同的命名空间使用的还是同一套物理集群
  • group:配置分组,默认值 dubbo。group 通常用于归类一组相同类型/目的配置项,是对 namespace 下配置项的进一步隔离

使用注册中心作为默认配置中心

在使用 Zookeeper、Nacos 作为注册中心且没有显式配置中心的情况下,Dubbo 框架默认会将 Zookeeper、Nacos 用作配置中心,用作服务治理用途

配置中心与其他两大中心不同,它无关于接口级还是应用级,与接口并没有对应关系,它仅仅与配置数据有关,即使没有部署注册中心和元数据中心,配置中心也能直接被接入到 Dubbo 应用服务中。

在整个部署架构中,整个集群内的实例(无论是 Provider 还是 Consumer)都将会共享配置中心集群中的配置,如下图所示:

image-20250518032339325

该图中不配备注册中心,意味着可能采用了 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 是特殊的个性化配置除外

image-20250531024012754

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参数类型缺省值描述
protocolconfig.protocolstringzookeeper使用哪个配置中心:apollo、zookeeper、nacos等
以zookeeper为例
1. 指定protocol,则address可以简化为127.0.0.1:2181
2. 不指定protocol,则address取值为zookeeper://127.0.0.1:2181
addressconfig.addressstring配置中心地址。 取值参见 protocol 说明
highest-priorityconfig.highestPrioritybooleantrue来自配置中心的配置项具有最高优先级,即会覆盖本地配置项
namespaceconfig.namespacestringdubbo通常用于多租户隔离,实际含义视具体配置中心而不同
如: zookeeper - 环境隔离,默认值dubbo
apollo - 区分不同领域的配置集合,默认使用dubboapplication
clusterconfig.clusterstring含义视所选定的配置中心而不同。 如 Apollo 中用来区分不同的配置集群
groupconfig.groupstringdubbo含义视所选定的配置中心而不同。
nacos - 隔离不同配置集
zookeeper - 隔离不同配置集
checkconfig.checkbooleantrue当配置中心连接失败时,是否终止应用启动。
config-fileconfig.configFilestringdubbo.properties全局级配置文件所映射到的 key
timeoutconfig.timeoutinteger3000ms获取配置的超时时间
usernamestring如果配置中心需要做校验,用户名 Apollo 暂未启用
passwordstring如果配置中心需要做校验,密码 Apollo 暂未启用
parametersMap<string, string>扩展参数,用来支持不同配置中心的定制化配置参数
include-spring-envbooleanfalse使用 Spring 框架时支持,为true时,会自动从Spring Environment中读取配置
默认依次读取 key 为 dubbo.properties 配置 key为 dubbo.properties PropertySource

参考文献

https://cn.dubbo.apache.org/zh-cn/docs/concepts/registry-configcenter-metadata

vnjohn

作者

vnjohn

后端研发工程师。喜欢探索新技术,空闲时也折腾 AIGC 等效率工具。 可以在 GitHub 关注我了解更多,也可以加我微信(vnjohn) 与我交流。