12-Dubbo Metadata 元数据中心

作者: vnjohn / 发表于 2025-06-01 / 专栏: Dubbo 3.3

Dubbo3, 源码

12 元数据中心简介

关于元数据中心的介绍,从官网拷贝过来一段文字进行说明:

元数据中心在 2.7.x 版本开始支持,随着应用级别的服务注册和服务发现在 Dubbo 中落地,元数据中心也变的越来越重要。在以下几种情况下会需要部署元数据中心:

  • 对于一个原先采用老版本 Dubbo 搭建的应用服务,在迁移到 Dubbo3 时,Dubbo3 会需要一个元数据中心来维护 RPC 服务与应用的映射关系(即接口与应用的映射关系),因为如果采用了应用级别的服务发现和服务注册,在注册中心中将采用“应用—实例列表”结构的数据组织形式,不再是以往的“接口—实例列表”结构的数据组织形式,而以往用接口级别的服务注册和服务发现的应用服务在迁移到应用级别时,得不到接口与应用之间的对应关系,从而无法从注册中心得到实例列表信息,所以 Dubbo 为了兼容这种场景,在 Provider 端启动时,会往元数据中心存储接口与应用的映射关系
  • 为了让注册中心更加聚焦与地址的发现和推送能力减轻注册中心的负担,元数据中心承载了所有的服务元数据、大量接口/方法级别配置信息等,无论是接口粒度还是应用粒度的服务发现和注册,元数据中心都起到了重要的作用
  • 如果有以上两种需求,都可以选择部署元数据中心,并通过 Dubbo 配置来集成该元数据中心

元数据中心并不依赖于注册中心和配置中心,用户可以自由选择是否集成和部署元数据中心,如下图所示:

image-20250518032339325

图中不配置配置中心,意味着可以不需要全局管理配置的能力。图中不配置注册中心,意味着可能采用了 Dubbo Mesh 方案,也可能不需要进行服务注册,仅仅接收直连模式的服务调用。

综上所述可以用几句话概括:

  1. 元数据中心来维护 RPC 服务与应用的映射关系(即接口与应用的映射关系)来兼容接口与应用之间的对应关系
  2. 让注册中心更加聚焦于地址的发现和推送能力

12.1 回顾元数据中心的启动

元数据中心的启动是通过调用 DefaultApplicationDeployer#initialize 初始化方法实现的,如下所示:

public void initialize() {
  if (initialized) {
    return;
  }
  synchronized (startLock) {
    if (initialized) {
      return;
    }
    onInitialize();
    registerShutdownHook();
    startConfigCenter();
    loadApplicationConfigs();
    initModuleDeployers();
    initMetricsReporter();
    initMetricsService();
    initObservationRegistry();
    // 本章节主要分析这块部分
    startMetadataCenter();
    initialized = true;
    if (logger.isInfoEnabled()) {
      logger.info(getIdentifier() + " has been initialized!");
    }
  }
}

12.2 启动元数据中心的代码全貌

通过了解 DefaultApplicationDeployer#startMetadataCenter 方法来大致观察整个流程

private void startMetadataCenter() {
  // 若未配置元数据中心的地址等信息,则使用注册中心的地址配置等作为元数据中心的配置
  useRegistryAsMetadataCenterIfNecessary();
  //  获取应用配置
  ApplicationConfig applicationConfig = getApplicationOrElseThrow();
  //  获取元数据类型 local 或 remote,如果选择远程,则需要进一步指定元数据中心
  String metadataType = applicationConfig.getMetadataType();
  // FIXME, multiple metadata config support.
  // 查询元数据中心的地址配置
  Collection<MetadataReportConfig> metadataReportConfigs = configManager.getMetadataConfigs();
  if (CollectionUtils.isEmpty(metadataReportConfigs)) {
    // 若使用 remote 元数据类型,则需要进一步指定元数据中心,否则就抛出异常
    if (REMOTE_METADATA_STORAGE_TYPE.equals(metadataType)) {
      throw new IllegalStateException(
        "No MetadataConfig found, Metadata Center address is required when 'metadata=remote' is enabled.");
    }
    return;
  }
  // 获取 MetadataReport 实例的存储库对象
  MetadataReportInstance metadataReportInstance =
    applicationModel.getBeanFactory().getBean(MetadataReportInstance.class);
  List<MetadataReportConfig> validMetadataReportConfigs = new ArrayList<>(metadataReportConfigs.size());
  for (MetadataReportConfig metadataReportConfig : metadataReportConfigs) {
    if (ConfigValidationUtils.isValidMetadataConfig(metadataReportConfig)) {
      ConfigValidationUtils.validateMetadataConfig(metadataReportConfig);
      validMetadataReportConfigs.add(metadataReportConfig);
    }
  }
  // 初始化元数据
  metadataReportInstance.init(validMetadataReportConfigs);
  // MetadataReport 实例的存储库对象初始化失败则抛出异常
  if (!metadataReportInstance.isInitialized()) {
    throw new IllegalStateException(String.format(
      "%s MetadataConfigs found, but none of them is valid.", metadataReportConfigs.size()));
  }
}

12.3 注册中心作为元数据中心

在第 10 篇博客说到 ConfigCenter 配置中心时,说过配置中心如果未配置就会使用注册中心的地址等信息作为默认配置,这里元数据做了类似的操作,如 DefaultApplicationDeployer#useRegistryAsMetadataCenterIfNecessary 方法代码

private void useRegistryAsMetadataCenterIfNecessary() {
  // 配置缓存中查询元数据配置
  Collection<MetadataReportConfig> originMetadataConfigs = configManager.getMetadataConfigs();
  // 配置存在则直接返回
  if (originMetadataConfigs.stream().anyMatch(m -> Objects.nonNull(m.getAddress()))) {
    return;
  }
  // 获取元数据配置中地址信息为空的元数据配置
  Collection<MetadataReportConfig> metadataConfigsToOverride = originMetadataConfigs.stream()
    .filter(m -> Objects.isNull(m.getAddress()))
    .collect(Collectors.toList());
  // 元数据配置数量大于 1 则返回
  if (metadataConfigsToOverride.size() > 1) {
    return;
  }
  // 获取第一条元数据配置信息
  MetadataReportConfig metadataConfigToOverride =
    metadataConfigsToOverride.stream().findFirst().orElse(null);
  // 查询是否有注册中心设置了默认配置 isDefault 设置为 true 的注册中心则为默认注册中心列表
  // 如果没有注册中心设置为默认注册中心,则获取所有未设置默认配置的注册中心列表
  List<RegistryConfig> defaultRegistries = configManager.getDefaultRegistries();
  if (!defaultRegistries.isEmpty()) {
    defaultRegistries.stream()
      // 筛选符合条件的注册中心 (筛选逻辑就是查看是否有对应协议的扩展支持)
      .filter(this::isUsedRegistryAsMetadataCenter)
      // 注册中心配置映射为元数据中心,映射就是获取需要的配置
      .map(registryConfig -> registryAsMetadataCenter(registryConfig, metadataConfigToOverride))
      // 将元数据中心配置存储在配置缓存中方便后续使用
      .forEach(metadataReportConfig ->
               overrideMetadataReportConfig(metadataConfigToOverride, metadataReportConfig));
  }
}

下面来整体概括一下将注册中心映射为元数据中心的实现逻辑

  1. 配置缓存中查询元数据配置,配置存在则直接返回
  2. 获取元数据配置中地址信息为空的元数据配置,存在多个元数据配置则直接返回,不做后续处理
  3. 当元数据配置只有一条时,再获取配置缓存中的默认注册中心列表
    • 选符合条件的注册中心 (筛选逻辑就是查看是否有对应协议的扩展支持)
    • 注册中心配置 RegistryConfig 映射转换为元数据中心配置类型 MetadataReportConfig 映射就是获取需要的配置
    • 将元数据中心配置存储在配置缓存中方便后续使用

讲解元数据配置加载流程之前,先来认识一下相关的配置项有哪些,如下:

元数据的配置可以参考官网:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/metadata-center/overview/

如下是元数据的配置项,来源于类型 MetadataReportConfig

配置变量类型说明
idString配置id
protocolString元数据协议
addressString元数据中心地址
portInteger元数据中心端口
usernameString元数据中心认证用户名
passwordString元数据中心认证密码
timeoutInteger元数据中心的请求超时(毫秒)
groupString该组将元数据保存在哪
parametersMap<String, String>自定义参数
retryTimesInteger重试次数
retryPeriodInteger重试间隔
cycleReportBoolean默认情况下, 是否每天重复存储完整的元数据
syncReportBooleanSync or Async report.
clusterBoolean需要群集支持,默认为false
registryString注册表配置 id
fileString元数据报告文件存储位置
checkBoolean连接到元数据中心时要应用的失败策略

12.4 元数据工厂对象创建与初始化

使用注册中心配置作为元数据配置时,会通过 isUsedRegistryAsMetadataCenter 方法选符合条件的注册中心配置,主要判断 Dubbo 扩展加载器是否支持我们传入的协议 nacos,其接口全类路径是 org.apache.dubbo.metadata.report.MetadataReportFactory ,目前 Dubbo 内部支持 nacos、zookeeper 协议,所属的模块如下:

nacos: dubbo-metadata/dubbo-metadata-report-nacos/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.report.MetadataReportFactory
zookeeper: dubbo-metadata/dubbo-metadata-report-zookeeper/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.report.MetadataReportFactory

关于元数据工厂类型 MetadataReportFactory,元数据工厂用于创建与管理元数据对象,相关的类图如下:

image-20250518032339325

这里主要以 nacos 扩展的元数据工厂 NacosMetadataReportFactory 类型为例,如下:

public class NacosMetadataReportFactory extends AbstractMetadataReportFactory {
    @Override
    protected MetadataReport createMetadataReport(URL url) {
        return new NacosMetadataReport(url);
    }
}

元数据实现代码逻辑非常简单,如下:

  1. 继承抽象工厂 AbstractMetadataReportFactory
  2. 实现工厂方法 createMetadataReport 进行元数据上报类型的创建

元数据上报工厂创建成功以后,接下来会调用 registryAsMetadataCenter 方法将注册中心配置映射为对应的元数据中心,也就是 RegistryConfig 转换为 MetadataReportConfig,最后将加载元数据上报配置存入本地 Map 缓存中,key:metadata-report、value:具体元数据实现类。通过校验工具类对元数据配置进行校验,主要是检查元数据地址、协议是否为空,满足条件就会进入到下面的初始化流程

12.4 元数据中心初始化过程

主要讲解 DefaultApplicationDeployer#startMetadataCenter 方法的这行代码,如下:

// 初始化元数据
metadataReportInstance.init(validMetadataReportConfigs);

MetadataReportInstance#init 初始化方法,源码如下:

public void init(List<MetadataReportConfig> metadataReportConfigs) {
  if (!initialized.compareAndSet(false, true)) {
    return;
  }
  // 元数据类型配置如果未配置则默认为 local
  this.metadataType = applicationModel
    .getApplicationConfigManager()
    .getApplicationOrElseThrow()
    .getMetadataType();
  if (metadataType == null) {
    this.metadataType = DEFAULT_METADATA_STORAGE_TYPE;
  }
  // 获取 MetadataReportFactory 工厂类型
  MetadataReportFactory metadataReportFactory =
    applicationModel.getExtensionLoader(MetadataReportFactory.class).getAdaptiveExtension();
  // 多元数据中心初始化
  for (MetadataReportConfig metadataReportConfig : metadataReportConfigs) {
    init(metadataReportConfig, metadataReportFactory);
  }
  
  // 初始化单个元数据上报配置
  private void init(MetadataReportConfig config, MetadataReportFactory metadataReportFactory) {
    // 配置转 URL
    URL url = config.toUrl();
    if (METADATA_REPORT_KEY.equals(url.getProtocol())) {
      String protocol = url.getParameter(METADATA_REPORT_KEY, DEFAULT_DIRECTORY);
      url = URLBuilder.from(url)
        .setProtocol(protocol)
        .setPort(url.getParameter(PORT_KEY, url.getPort()))
        .setScopeModel(config.getScopeModel())
        .removeParameter(METADATA_REPORT_KEY)
        .build();
    }
    url = url.addParameterIfAbsent(
      APPLICATION_KEY, applicationModel.getCurrentConfig().getName());
    url = url.addParameterIfAbsent(
      REGISTRY_LOCAL_FILE_CACHE_ENABLED,
      String.valueOf(applicationModel.getCurrentConfig().getEnableFileCache()));
    //        RegistryConfig registryConfig = applicationModel.getConfigManager().getRegistry(relatedRegistryId)
    //                .orElseThrow(() -> new IllegalStateException("Registry id " + relatedRegistryId + " does not
    // exist."));
    // 从元数据工厂中获取元数据
    MetadataReport metadataReport = metadataReportFactory.getMetadataReport(url);
    if (metadataReport != null) {
      // 缓存元数据到内存
      metadataReports.put(getRelatedRegistryId(config, url), metadataReport);
    }
  }
}

在前面我们有说到元数据工厂对象 MetadataReportFactory 创建与初始化的过程,下面我们来介绍 MetadataReport 对象创建与初始化过程

12.4.1 元数据对象 MetadataReport 创建

MetadataReportInstance#init 方法中,会从元数据工厂中获取元数据操作对象,其处理逻辑代码如下:

// 从元数据工厂中获取元数据
MetadataReport metadataReport = metadataReportFactory.getMetadataReport(url);

关于元数据对象,用于元数据信息的增删改查等逻辑操作与操作元数据信息的缓存

image-20250518032339325

在这里仍然以 Nacos 扩展的实现 NacosMetadataReportFactory 类型作为参考,先以其父类 AbstractMetadataReportFactory#getMetadataReport 方法作为切入点,方法源码如下:

public MetadataReport getMetadataReport(URL url) {
  // url 值参考例子:zookeeper://127.0.0.1:2181?application=dubbo-demo-api-provider&client=&port=2181&protocol=zookeeper
  // 如果存在 export、refer 参数则移除
  url = url.setPath(MetadataReport.class.getName()).removeParameters(EXPORT_KEY, REFER_KEY);
  // 生成元数据缓存 key 元数据维度 地址+名字
  // 如: zookeeper://127.0.0.1:2181/org.apache.dubbo.metadata.report.MetadataReport
  String key = url.toServiceString(NAMESPACE_KEY);
  // 缓存中查询 查到则直接返回
  MetadataReport metadataReport = serviceStoreMap.get(key);
  if (metadataReport != null) {
    return metadataReport;
  }

  // Lock the metadata access process to ensure a single instance of the metadata instance
  lock.lock();
  try {
    metadataReport = serviceStoreMap.get(key);
    if (metadataReport != null) {
      return metadataReport;
    }
    // check 参数 查元数据报错是否抛出异常
    boolean check = url.getParameter(CHECK_KEY, true) && url.getPort() != 0;
    try {
      // 关键模版方法,调用扩展实现的具体业务(创建元数据操作对象)
      metadataReport = createMetadataReport(url);
    } catch (Exception e) {
      if (!check) {
        logger.warn(PROXY_FAILED_EXPORT_SERVICE, "", "", "The metadata reporter failed to initialize", e);
      } else {
        throw e;
      }
    }
    // check 逻辑检查
    if (check && metadataReport == null) {
      throw new IllegalStateException("Can not create metadata Report " + url);
    }
    // 缓存对象
    if (metadataReport != null) {
      serviceStoreMap.put(key, metadataReport);
    }
    return metadataReport;
  } finally {
    // Release the lock
    lock.unlock();
  }
}

AbstractMetadataReportFactory 作为抽象类,获取元数据操作对象的模版方法 getMetadataReport(URL url),用了双重校验锁的逻辑来创建缓存对象,又用了模版方法设计模式,让抽象类实现通用的逻辑,让子类实现去做对应的扩展,从中可以学习到很多设计思想。其实,在这里主要关注的核心代码 createMetadataReport

// 关键模版方法,调用扩展实现的具体业务(创建元数据操作对象)
metadataReport = createMetadataReport(url);

创建元数据操作对象的代码实际上走的就是子类实现的逻辑,在这里我们指的就是 NacosMetadataReportFactory#createMetadataReport 方法,所以我们直接来看该方法是如何实现的

protected MetadataReport createMetadataReport(URL url) {
  return new NacosMetadataReport(url);
}

主要就是实例化一个元数据操作对象,那么继续来看其构造器里做了哪些逻辑,来自于 NacosMetadataReport 有参构造函数

public NacosMetadataReport(URL url) {
  // URL 即配置传递给抽象类,做一些公共逻辑
  // nacos://127.0.0.1:8848/org.apache.dubbo.metadata.report.MetadataReport?application=dubbo-demo-api-provider&client=&file-cache=true&port=8848&protocol=nacos
  super(url);
  this.configService = buildConfigService(url);
  group = url.getParameter(GROUP_KEY, DEFAULT_ROOT);
}

12.4.2 AbstractMetadataReport 元数据上报初始化

核心的公共操作逻辑都封装在其父类 AbstractMetadataReport 里,首先来看第一行 super 调用的构造器逻辑,如下所示:

public AbstractMetadataReport(URL reportServerURL) {
  // 设置 URL
  // nacos://127.0.0.1:8848/org.apache.dubbo.metadata.report.MetadataReport?application=dubbo-demo-api-provider&client=&file-cache=true&port=8848&protocol=nacos
  setUrl(reportServerURL);
  applicationModel = reportServerURL.getOrDefaultApplicationModel();
  // 是否开启文件缓存,若开启的话会删除未初始化的文件缓存,然后重新读取
  boolean localCacheEnabled = reportServerURL.getParameter(REGISTRY_LOCAL_FILE_CACHE_ENABLED, true);
  // Start file save timer
  // 缓存的文件名字
  // 格式为: 用户目录 + /.dubbo/dubbo-metadata- + 应用程序名字 application + url 地址(IP+端口) + 后缀.cache
  // 如下所示: /Users/vnjohn/.dubbo/dubbo-metadata-dubbo-demo-api-provider-127.0.0.1-8848.cache
  String defaultFilename = SystemPropertyConfigUtils.getSystemProperty(USER_HOME) + DUBBO_METADATA
    + reportServerURL.getApplication()
    + "-" + replace(reportServerURL.getAddress(), ":", "-")
    + CACHE;
  // 如果用户配置了缓存文件名字参数 file 则以用户配置为准
  String filename = reportServerURL.getParameter(FILE_KEY, defaultFilename);
  File file = null;
  // 开启文件缓存并且文件名字不为空
  if (localCacheEnabled && ConfigUtils.isNotEmpty(filename)) {
    file = new File(filename);
    if (!file.exists()
        && file.getParentFile() != null
        && !file.getParentFile().exists()) {
      if (!file.getParentFile().mkdirs()) {
        throw new IllegalArgumentException("Invalid service store file " + file
                                           + ", cause: Failed to create directory " + file.getParentFile() + "!");
      }
    }
    // if this file exists, firstly delete it.
    // 还未初始化则将已存在的历史文件删除掉
    if (!initialized.getAndSet(true) && file.exists()) {
      file.delete();
    }
  }
  // 赋值给成员变量后续继续可以用
  this.file = file;
  // 文件存在则直接加载文件中的内容
  loadProperties();
  // sync-report 配置的值为同步配置还异步配置,true 同步配置,默认为 false 异步配置
  syncReport = reportServerURL.getParameter(SYNC_REPORT_KEY, false);
  // 创建一个重试类型对象
  // retry-times 重试次数配置,默认为 100 次
  // retry-period 重试间隔配置,默认为 3000
  metadataReportRetry = new MetadataReportRetry(
    reportServerURL.getParameter(RETRY_TIMES_KEY, DEFAULT_METADATA_REPORT_RETRY_TIMES),
    reportServerURL.getParameter(RETRY_PERIOD_KEY, DEFAULT_METADATA_REPORT_RETRY_PERIOD));
  // cycle report the data switch
  // 是否定期从元数据中心同步配置
  if (reportServerURL.getParameter(CYCLE_REPORT_KEY, DEFAULT_METADATA_REPORT_CYCLE_REPORT)) {
    // 开启重试定时器,24 个小时间隔从元数据中心同步一次
    reportTimerScheduler = Executors.newSingleThreadScheduledExecutor(
      new NamedThreadFactory("DubboMetadataReportTimer", true));
    reportTimerScheduler.scheduleAtFixedRate(
      this::publishAll, calculateStartTime(), ONE_DAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
  }
  //  是否上报元数据、定义
  this.reportMetadata = reportServerURL.getParameter(REPORT_METADATA_KEY, false);
  this.reportDefinition = reportServerURL.getParameter(REPORT_DEFINITION_KEY, true);
}

来总结一下元数据操作的初始化逻辑:

  1. 首先初始化清理历史的元数据文件,如:/Users/vnjohn/.dubbo/dubbo-metadata-dubbo-demo-api-provider-127.0.0.1-8848.cache
  2. 若非首次进来则直接加载缓存在本地的缓存文件属性 file,然后再加载文件内容赋值给到属性 properties 成员变量
  3. 初始化同步配置是否异步(默认为 false)主要看 sync-report 配置的值为同步还是异步配置,true-同步、false-异步
  4. 初始化重试类型对象,设置重试次数和重试时间间隔
  5. 看参数 cycle-report 是否定期从元数据中心同步配置初始化,默认值是 true,24 小时自动进行一次同步

12.5 元数据同步到 Nacos、本地文件

12.4.2 说到会开启一个重试定时器,通过定时任务调用 AbstractMetadataReport#publishAll 方法进行元数据同步处理

// 开启重试定时器,24 个小时间隔从元数据中心同步一次
reportTimerScheduler = Executors.newSingleThreadScheduledExecutor(
  new NamedThreadFactory("DubboMetadataReportTimer", true));
reportTimerScheduler.scheduleAtFixedRate(
  this::publishAll, calculateStartTime(), ONE_DAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS);

刚开始的执行时间通过 calculateStartTime 方法计算得出,代码里取的是 between 2:00 am to 6:00 am, the time is random. 2点到6点之间启动,在低峰时期启动自动同步

AbstractMetadataReport#publishAll 方法内部主要通过调用 doHandleMetadataCollection 方法处理元数据收集工作

void publishAll() {
  logger.info("start to publish all metadata.");
  this.doHandleMetadataCollection(allMetadataReports);
}

private boolean doHandleMetadataCollection(Map<MetadataIdentifier, Object> metadataMap) {
  if (metadataMap.isEmpty()) {
    return true;
  }
  Iterator<Map.Entry<MetadataIdentifier, Object>> iterable =
    metadataMap.entrySet().iterator();
  while (iterable.hasNext()) {
    Map.Entry<MetadataIdentifier, Object> item = iterable.next();
    if (PROVIDER_SIDE.equals(item.getKey().getSide())) {
      // 提供端的元数据则存储提供端元数据
      this.storeProviderMetadata(item.getKey(), (FullServiceDefinition) item.getValue());
    } else if (CONSUMER_SIDE.equals(item.getKey().getSide())) {
      // 消费端的元数据则存储提供端元数据
      this.storeConsumerMetadata(item.getKey(), (Map) item.getValue());
    }
  }
  return false;
}

提供端的元数据存储方法:AbstractMetadataReport#storeProviderMetadata

public void storeProviderMetadata(
  MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
  if (syncReport) {
    storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
  } else {
    reportCacheExecutor.execute(() -> storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition));
  }
}

通过 AbstractMetadataReport#storeProviderMetadataTask 方法来执行同步

private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
  MetadataEvent metadataEvent = MetadataEvent.toServiceSubscribeEvent(
    applicationModel, providerMetadataIdentifier.getUniqueServiceName());
  MetricsEventBus.post(
    metadataEvent,
    () -> {
      boolean result = true;
      try {
        if (logger.isInfoEnabled()) {
          logger.info("store provider metadata. Identifier : " + providerMetadataIdentifier
                      + "; definition: " + serviceDefinition);
        }
        allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
        failedReports.remove(providerMetadataIdentifier);
        String data = JsonUtils.toJson(serviceDefinition);
        // 内存中的元数据同步到元数据中心
        doStoreProviderMetadata(providerMetadataIdentifier, data);
        // 内存中的元数据同步到本地文件
        saveProperties(providerMetadataIdentifier, data, true, !syncReport);
      } catch (Exception e) {
        // retry again. If failed again, throw exception.
        failedReports.put(providerMetadataIdentifier, serviceDefinition);
        metadataReportRetry.startRetryTask();
        logger.error(
          PROXY_FAILED_EXPORT_SERVICE,
          "",
          "",
          "Failed to put provider metadata " + providerMetadataIdentifier + " in  "
          + serviceDefinition + ", cause: " + e.getMessage(),
          e);
        result = false;
      }
      return result;
    },
    aBoolean -> aBoolean);
}

主要关注的两个点就是元数据同步到元数据中心以及本地文件

// 内存中的元数据同步到元数据中心
doStoreProviderMetadata(providerMetadataIdentifier, data);
// 内存中的元数据同步到本地文件
saveProperties(providerMetadataIdentifier, data, true, !syncReport);

12.5.1 同步元数据中心

doStoreProviderMetadata 方法将内存中的元数据同步到元数据中心,主要就是调用子类 NacosMetadataReport#doStoreProviderMetadata 方法,如下:

protected void doStoreProviderMetadata(MetadataIdentifier providerMetadataIdentifier, String serviceDefinitions) {
  this.storeMetadata(providerMetadataIdentifier, serviceDefinitions);
}

private void storeMetadata(BaseMetadataIdentifier identifier, String value) {
  try {
    boolean publishResult =
      configService.publishConfig(identifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), group, value);
    if (!publishResult) {
      throw new RuntimeException("publish nacos metadata failed");
    }
  } catch (Throwable t) {
    logger.error(
      REGISTRY_NACOS_EXCEPTION,
      "",
      "",
      "Failed to put " + identifier + " to nacos " + value + ", cause: " + t.getMessage(),
      t);
    throw new RuntimeException(
      "Failed to put " + identifier + " to nacos " + value + ", cause: " + t.getMessage(), t);
  }
}

通过 NacosMetadataReport 构造方法中创建 NacosConfigService 发布配置,具体的元数据内容:比较详细记录了应用信息、服务接口信息和服务接口对应的方法信息,如下:

{
    "annotations": [],
    "canonicalName": "org.apache.dubbo.demo.DemoService",
    "codeSource": "file:/Users/vnjohn/Documents/study/%e6%ba%90%e7%a0%81/dubbo-dubbo-3.3.0/dubbo-demo/dubbo-demo-interface/target/classes/",
    "methods": [
        {
            "annotations": [],
            "name": "sayHello",
            "parameterTypes": [
                "java.lang.String"
            ],
            "parameters": [],
            "returnType": "java.lang.String"
        },
        {
            "annotations": [],
            "name": "sayHelloAsync",
            "parameterTypes": [
                "java.lang.String"
            ],
            "parameters": [],
            "returnType": "java.util.concurrent.CompletableFuture"
        }
    ],
    "parameters": {
        "release": "3.3.0",
        "anyhost": "true",
        "application": "dubbo-demo-api-provider",
        "interface": "org.apache.dubbo.demo.DemoService",
        "dubbo": "2.0.2",
        "side": "provider",
        "pid": "3220",
        "executor-management-mode": "isolation",
        "file-cache": "true",
        "bind.ip": "192.168.2.1",
        "prefer.serialization": "hessian2,fastjson2",
        "methods": "sayHello,sayHelloAsync",
        "background": "false",
        "deprecated": "false",
        "dynamic": "true",
        "service-name-mapping": "true",
        "generic": "false",
        "bind.port": "20880",
        "timestamp": "1749282500760"
    },
    "types": [
        {
            "enums": [],
            "items": [],
            "properties": {
                "result": "java.lang.Object",
                "stack": "java.util.concurrent.CompletableFuture.Completion"
            },
            "type": "java.util.concurrent.CompletableFuture"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "java.lang.Object"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "java.lang.String"
        },
        {
            "enums": [],
            "items": [],
            "properties": {
                "next": "java.util.concurrent.CompletableFuture.Completion",
                "status": "int"
            },
            "type": "java.util.concurrent.CompletableFuture.Completion"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "int"
        }
    ],
    "uniqueId": "org.apache.dubbo.demo.DemoService@file:/Users/vnjohn/Documents/study/%e6%ba%90%e7%a0%81/dubbo-dubbo-3.3.0/dubbo-demo/dubbo-demo-interface/target/classes/"
}

12.5.2 同步本地文件

本地缓存文件的写入主要是通过 AbstractMetadataReport.SaveProperties#run 方法调用 AbstractMetadataReport#doSaveProperties 方法,如下:

// AbstractMetadataReport.SaveProperties
public void run() {
  doSaveProperties(version);
}

// AbstractMetadataReport
private void doSaveProperties(long version) {
  if (version < lastCacheChanged.get()) {
    return;
  }
  if (file == null) {
    return;
  }
  // Save
  try {
    // 创建本地文件锁,路径:/Users/vnjohn/.dubbo/dubbo-metadata-dubbo-demo-api-provider-127.0.0.1-8848.cache.lock
    File lockfile = new File(file.getAbsolutePath() + ".lock");
    // 锁文件不存在则创建锁文件
    if (!lockfile.exists()) {
      lockfile.createNewFile();
    }
    // 随机访问文件工具类对象,创建读写权限
    try (RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
         FileChannel channel = raf.getChannel()) {
      // FileChannel中的 lock、tryLock 方法都是尝试去获取在某一文件上的独有锁,可以实现进程间操作的互斥
      // 区别在于 lock 会阻塞(blocking)方法的执行,tryLock()则不会
      FileLock lock = channel.tryLock();
      if (lock == null) {
        throw new IOException(
          "Can not lock the metadataReport cache file " + file.getAbsolutePath()
          + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.metadata.file=xxx.properties");
      }
      // Save
      try {
        // 文件不存在则创建本地元数据缓存文件
        // /Users/vnjohn/.dubbo/dubbo-metadata-dubbo-demo-api-provider-127.0.0.1-8848.cache
        if (!file.exists()) {
          file.createNewFile();
        }
        Properties tmpProperties;
        if (!syncReport) {
          // When syncReport = false, properties.setProperty and properties.store are called from the same
          // thread(reportCacheExecutor), so deep copy is not required
          tmpProperties = properties;
        } else {
          // Using store method and setProperty method of the this.properties will cause lock contention
          // under multi-threading, so deep copy a new container
          // 异步存储会导致锁争用,使用此的 store、setProperty 方法,属性将导致多线程下的锁争用,因此深度复制新容器
          tmpProperties = new Properties();
          Set<Map.Entry<Object, Object>> entries = properties.entrySet();
          for (Map.Entry<Object, Object> entry : entries) {
            tmpProperties.setProperty((String) entry.getKey(), (String) entry.getValue());
          }
        }
        // 将此属性表中的属性列表(键和元素对)以适合使用load(Reader)方法的格式写入输出字符流
        try (FileOutputStream outputFile = new FileOutputStream(file)) {
          tmpProperties.store(outputFile, "Dubbo metadataReport Cache");
        }
      } finally {
        lock.release();
      }
    }
  } catch (Throwable e) {
    if (version < lastCacheChanged.get()) {
      return;
    } else {
      reportCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
    }
    logger.warn(
      COMMON_UNEXPECTED_EXCEPTION,
      "",
      "",
      "Failed to save service store file, cause: " + e.getMessage(),
      e);
  }
}

写入本地文件的内容,如下:

// Dubbo metadataReport Cache
// Sat Jun 07 15:48:31 CST 2025
// org.apache.dubbo.demo.DemoService\:\:\:provider\:dubbo-demo-api-provider 
{
    "annotations": [],
    "canonicalName": "org.apache.dubbo.demo.DemoService",
    "codeSource": "file:/Users/vnjohn/Documents/study/%e6%ba%90%e7%a0%81/dubbo-dubbo-3.3.0/dubbo-demo/dubbo-demo-interface/target/classes/",
    "methods": [
        {
            "annotations": [],
            "name": "sayHello",
            "parameterTypes": [
                "java.lang.String"
            ],
            "parameters": [],
            "returnType": "java.lang.String"
        },
        {
            "annotations": [],
            "name": "sayHelloAsync",
            "parameterTypes": [
                "java.lang.String"
            ],
            "parameters": [],
            "returnType": "java.util.concurrent.CompletableFuture"
        }
    ],
    "parameters": {
        "release": "3.3.0",
        "anyhost": "true",
        "application": "dubbo-demo-api-provider",
        "interface": "org.apache.dubbo.demo.DemoService",
        "dubbo": "2.0.2",
        "side": "provider",
        "pid": "3220",
        "executor-management-mode": "isolation",
        "file-cache": "true",
        "bind.ip": "192.168.2.1",
        "prefer.serialization": "hessian2,fastjson2",
        "methods": "sayHello,sayHelloAsync",
        "background": "false",
        "deprecated": "false",
        "dynamic": "true",
        "service-name-mapping": "true",
        "generic": "false",
        "bind.port": "20880",
        "timestamp": "1749282500760"
    },
    "types": [
        {
            "enums": [],
            "items": [],
            "properties": {
                "result": "java.lang.Object",
                "stack": "java.util.concurrent.CompletableFuture.Completion"
            },
            "type": "java.util.concurrent.CompletableFuture"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "java.lang.Object"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "java.lang.String"
        },
        {
            "enums": [],
            "items": [],
            "properties": {
                "next": "java.util.concurrent.CompletableFuture.Completion",
                "status": "int"
            },
            "type": "java.util.concurrent.CompletableFuture.Completion"
        },
        {
            "enums": [],
            "items": [],
            "properties": {},
            "type": "int"
        }
    ],
    "uniqueId": "org.apache.dubbo.demo.DemoService@file:/Users/vnjohn/Documents/study/%e6%ba%90%e7%a0%81/dubbo-dubbo-3.3.0/dubbo-demo/dubbo-demo-interface/target/classes/"
}

参考文献

https://cn.dubbo.apache.org/zh-cn/blog/2022/08/14/14-dubbo%E9%85%8D%E7%BD%AE%E5%8A%A0%E8%BD%BD%E5%85%A8%E8%A7%A3%E6%9E%90

vnjohn

作者

vnjohn

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