Skip to content

Discussion 3342

alanxtl edited this page Sep 20, 2026 · 1 revision

Dubbo-go Metadata Design Spec — 综合审查报告

讨论目录 · GitHub 原讨论

  • 编号:#3342
  • 分类:General
  • 状态:已关闭 / 无已采纳答案
  • 创建:2026-05-24T05:48:22Z
  • 更新:2026-06-25T09:08:40Z
  • 作者:AlexStocks

社区原始记录;其中的建议、未决提案和版本描述需结合 版本与升级 核对。快照日期:2026-09-20。

阅读说明:原文引用作者本地磁盘文件的链接已转为文字路径,避免产生无效 Wiki 导航;原始内容完整保存在 sources/discussions.json。

讨论正文

审查日期: 2026-05-24
审查基准: dubbo-go-metadata-design-spec-zh.md (61,504 字符, 20 章节)
对照参考: Java Dubbo 3.3 源码 (MetadataServiceV2 proto, MetadataInfo, ServiceInstanceHostPortCustomizer)


总体评价

这是一份高质量的设计文档。结构清晰、问题识别准确、实施路径合理。作者对 Java Dubbo 和 dubbo-go 的差异有深入理解,并且明确区分了"协议兼容性"和"实现风格"的边界。

但仍存在多个 CRITICAL 级别和 WARNING 级别的问题需要在实施前解决。

综合评分: ★★★☆ (3.5/5) — 方向正确,细节需补强


一、架构师审查 — 关键发现

CRITICAL (必须修复)

C-1: ServiceInfo key 格式必须与 Java 字节级对齐

Java Dubbo 的 ServiceInfo key 格式为 {group}/{interface}:{version}:{protocol}。
方案中提到了 key 格式但未明确规定分隔符和字段顺序。

dubbo-go 当前已有 common.ServiceKey(intf, group, version) 和 common.MatchKey(serviceKey, protocol),生成逻辑接近 Java,但方案没有把这个作为 wire contract 写死。

风险: 如果 Go 使用不同分隔符或字段顺序,Java/Go 之间的 revision 对比永远不匹配,导致消费者持续重新获取元数据。

建议: 在方案中明确定义 key 格式,包括:

  • 分隔符 (/, :)
  • 空值处理:group 为空时省略前缀;version 为空或等于 0.0.0 时省略版本段
  • 字段顺序:group → interface → version → protocol

关键实现细节: 当前 common.MatchKey(serviceKey, protocol) 无条件追加 ":" + protocol,当 protocol 为空时会生成尾随冒号(如 "group/interface:version:")。Java 的行为是 protocol 非空才追加。设计文档必须明确 Go 实现需与 Java 对齐:protocol 为空时不追加冒号,否则空 protocol 边界场景下的 revision 会与 Java 不一致。


C-2: Go map 迭代随机性 — revision 计算实现验收约束

Java 使用 TreeMap 保证迭代顺序确定性。Go 的 map[string]*ServiceInfo 迭代顺序是完全随机的。

方案第 8.2 节已正确处理了两层排序:外层 sort.Strings(keys) 排序 service keys,ToDescString() 内部也使用了 sortedParamsString(s.Params) 处理 params map 随机迭代问题,方向是正确的。

实现验收约束: 排序要求不止一层。Java 使用 TreeMap / TreeSet 保证所有层级确定,Go 实现必须覆盖所有 map 层级,包括 Services、Params、method-level params、exported/subscribed URL sets。

建议: 以下测试用例必须全部覆盖:

  • TestRevision_Deterministic_ServiceInsertionOrder:相同服务不同插入顺序 → 相同 revision
  • TestRevision_Deterministic_ParamInsertionOrder:params key/value 顺序不同 → 相同 revision
  • TestRevision_Deterministic_MethodParamInsertionOrder:method-level params 顺序不同 → 相同 revision
  • TestRevision_JavaGoldenVectors:同一 metadata,Go/Java revision 完全一致

C-3: Hash 算法必须与 Java 一致

Java RevisionResolver.calRevision() 使用 MD5。方案中说用 revision.Resolve() 但未指明具体算法。

风险: 如果 Go 用 SHA-256 而 Java 用 MD5,相同元数据会产生不同 revision,导致消费者误判元数据变更。

建议: 这是互操作契约,不是实现偏好,方案中应明确写"必须 MD5"而不是"建议 MD5"。完整规格:

revision.Resolve 必须等价于 Java Dubbo RevisionResolver.calRevision:
- 输入:app + sorted ServiceInfo.toDescString()
- 算法:MD5
- 输出格式:与 Java MD5Utils.getMd5(metadata) 完全一致
- 空 services:EMPTY_REVISION = "0"(见 C-6)

添加跨语言 revision 一致性测试(golden vector 测试)。


C-4: map 并发安全 — 不能返回可变引用

Java synchronized 天然保护了所有访问。Go 的 RWMutex 如果只保护 GetMetadataInfo() 调用,但返回 *MetadataInfo 指针,调用方可以在锁外读写 Services map。

风险: Go map 并发读写会 panic。即使有 RWMutex,返回的指针被并发访问仍然崩溃。

设计文档虽然要求 MetadataManager 加 RWMutex,也要求 Clone() 深拷贝,但同时保留了 GetMetadataInfo(registryId string) *info.MetadataInfo 这个返回 live pointer 的 API。Manager 的锁只能保护 map lookup,不能保护返回对象后的访问。

建议:

  1. 明确 API 语义分层:
    • GetMetadataInfoSnapshot(registryID string) *info.MetadataInfo:返回 deep clone,用于发布/cache/consumer 读取
    • MutateMetadataInfo(registryID string, fn func(*info.MetadataInfo) error) error:受锁保护的写路径
    • 原 GetMetadataInfo 如需保留兼容,注明"仅用于兼容旧调用,不应用于新写路径"
  2. Clone() 方法必须深度复制所有 map(Services、exported URLs、subscribed URLs)
  3. MetadataInfo.Services 不应直接暴露可变 map;getters 返回 copy 或只读视图

C-5: 持久化 cache 的 key/format 迁移策略不明确

方案将 cache key 从 {revision} 改为 {app}:{revision},这是正确的设计,但方案未明确持久化 cache 文件的版本化或清理策略。

当前代码确认:revisionToMetadata 是 map[string]*info.MetadataInfo(key 只有 revision);metaCache.Get(revision) 也只用 revision;cacheOnce 用第一个 app 初始化 cache manager。

风险: 如果 dubbo-go 存在 file-based 本地持久化 cache,key 格式变更后旧格式条目可能被新代码误读,导致错误 metadata 复用。进程内内存 cache miss 是正常的升级行为,不是核心风险。

建议:

  1. 明确持久化 cache 文件是否存在;如果存在,key 格式变更时需清理旧格式文件或给 storage format 加版本号(方案 §8.2 已提到"旧 revision cache 和新 revision cache 不能混用",但未说明具体处理方式)
  2. cacheOnce 用第一个 app 初始化的问题需要修复:改为 app-neutral 共享 cache 或 per-provider-app cache manager

WARNING (强烈建议修复)

W-1: RWMutex 粒度 — 锁竞争问题

方案提出单个 MetadataManager 用一把 RWMutex 保护所有 registry 的 MetadataInfo。

问题: 如果 calRevision() 持写锁计算哈希(可能遍历大量服务),会阻塞所有并发读取。多 registry 场景下,一个 registry 的 revision 变更会阻塞其他 registry 的读取。

建议: 考虑按 registryId 分片加锁,或使用 MetadataInfo 级别的 mutex。


W-2: updated 标志的竞态条件

Java 在 synchronized 块内读写 updated。Go 如果将 updated 作为普通 bool 字段,在锁外读取可能读到脏值。

建议: 使用 atomic.Bool 或确保所有 updated 访问都在锁内。


W-3: extendParams vs instanceParams 的语义未完全明确

Java MetadataInfo 有 extendParams(扩展参数)和 instanceParams(实例参数)两个独立 map。

设计文档 §8.2 已写"instance-level params 和 service-level revision params 保持分离",方向正确,但未明确这两个 map 在 Go 侧是否需要字段级等价模型。

风险: 如果 Go 将两者合并,可能丢失 Java wire format 里的 params 分层语义,影响互操作。

建议: 方案应补充说明:

  • Go 侧是否需要与 Java extendParams / instanceParams 字段语义对等
  • 这两个 map 是否参与 JSON wire format
  • 是否仅作为运行时派生字段,不序列化

W-4: 多协议端点选择优先级规则需量化

设计文档 §11.4 已写了主地址选择原则(preferred business protocol → 第一个 exported service URL → metadata-service port 兜底),方向正确,但 preferred/default business protocol 的配置来源、协议优先级顺序和 same-protocol multi-port 的 tie-breaker 仍未量化。

风险: "第一个 exported service URL"在 Go map 或并发 export 场景里可能不稳定,导致 primary address 非确定性。

建议: 将选择规则写成可测试的确定性规格:

  1. 显式配置的 preferred protocol(来自 dubbo.application.preferred-protocol 或同等配置)
  2. application/default protocol
  3. stable sorted business endpoints 中的第一个(排序键:(protocol priority, host, port, service key))
  4. 仅当没有 business endpoint 时,使用 metadata-service endpoint

明确协议优先级顺序(如 tri > dubbo > rest),否则 step 3 的 sort key 无法确定。


W-5: 启动时的 revision 计算防抖(优化项,非第一阶段硬要求)

Java 的 synchronized 天然串行化 revision 计算。Go 的 RWMutex 如果处理不当,50 个服务同时 export 可能触发 50 次 revision 计算。

设计文档的 PR 5 已将 revision 计算放到 CalAndGetRevision(),并用 updated 标志避免未变化时重复计算,这已经是基础保护。

建议: 第一阶段不必引入 debounce,以免改变注册时序。应先保证 dirty 标志正确和注册时单点计算;如果 benchmark 显示大量服务 export 时 revision 计算成为瓶颈,再引入批量/防抖机制作为后续优化。


W-6: Provider 侧 MetadataService export failure 处理不明确

方案第 14 节讨论了 consumer 侧的错误处理,但 provider 侧 MetadataService export failure 的行为策略不够明确。Go 服务端不应在设计文档里承诺 goroutine 自动重启,除非已有 supervisor 模型。

建议: 方案应补充 provider 侧的 fail-safe 规则:

  • local 模式下,若计划导出 V1 失败,应返回启动错误,或至少不注册 storage-type=local 的 instance
  • metadata-service-protocol=tri 且 V2 导出失败但 V1 成功,capability 只能声明 V1,写 meta-v=1.0.0
  • V1/V2 全部失败,不能注册 dubbo.metadata.storage-type=local 的 provider instance
  • remote 模式下,本地 MetadataService export failure 不影响启动

W-7: 大规模场景下的内存管理(后续扩展风险)

MetadataInfo 包含所有服务的完整描述。100+ 服务 x 1000+ 方法 = MB 级元数据。此项不应阻塞 PR 1-3,但需要在方案中有明确的阈值策略。

建议: 第一阶段增加 metadata size warning 阈值,而非实现压缩:

  • metadata JSON size > 1 MiB 时 warning
  • 4 MiB 时可配置 fail-fast 或跳过 remote publish

具体阈值需结合 Nacos/ZK/etcd backend 的 value size 限制评估,适合在 PR 5/6 阶段补充。


W-8: Go nil 安全性

Java 对 null 处理较宽容(空字符串、空 map)。Go 读 nil map 不 panic,但写 nil map 会 panic,风险主要在 AddService / mutation 路径和外部反序列化后的对象初始化。

建议:

  1. 所有 MetadataInfo/ServiceInfo 构造函数必须初始化 map 字段
  2. 以下测试用例必须覆盖:
    • TestMetadataInfo_ZeroValueSafeRead
    • TestMetadataInfo_DeserializedNilServices_AddService
    • TestServiceInfo_NilParams_ToDescString
    • TestServiceInfo_NilParams_GetMethods

W-9: EMPTY_REVISION 常量命名未明确

Java Dubbo 3.3 RevisionResolver.EMPTY_REVISION 的值是 "0",与设计文档 §8.2 的 revision="0" 一致,值已对齐。

文档缺口: 方案未显式引用该常量名,读者可能误以为 "0" 是 dubbo-go 自定义值,或者在实现时随意改为其他哨兵值。

建议: 在设计文档中补充:

空 services 时 revision 返回 RevisionResolver.EMPTY_REVISION,即 "0",与 Java Dubbo 3.3 一致。

并在代码中定义对应常量,不要直接使用字面量 "0"。


W-10: common.MatchKey 对空 protocol 的行为必须与 Java 对齐

当前 dubbo-go common.MatchKey(serviceKey, protocol) 无条件追加 ":" + protocol,当 protocol 为空时会生成尾随冒号(如 "group/interface:version:")。Java 的行为是 protocol 非空才追加冒号。

风险: 边界场景下(protocol 字段为空字符串),Go 生成的 match key 与 Java 不一致,导致 revision 计算偏差。这是 C-1 里"字节级对齐"的具体实现要求,需要单独列出以确保实现时不被忽略。

建议: 修改 MatchKey 实现,protocol 为空时不追加 ":",并增加对应边界 case 测试。


二、测试人员审查 — 测试计划缺口

现有测试规范质量评分

维度 评分 说明
单元测试覆盖 ★★★ 基本覆盖核心组件,但缺少并发/边界场景
集成测试覆盖 ★★ 涵盖本地/远程元数据、Java 兼容性,但场景不足
兼容性测试 ★★ 有多协议/多注册中心覆盖,但缺少升级/降级路径
验收标准可测性 ★★ 24 条标准中有模糊项,部分缺少量化指标
回归保护 ★ 仅提及多提供者 URL 生成,覆盖面严重不足

综合评分: ★★ (2/5) — 框架存在但存在显著缺口

P0 测试缺口 (必须修复)

# 测试项 风险
1 MetadataManager 并发安全 (go test -race,100 goroutines 并发 Add/Remove/Clone/CalAndGetRevision) 数据竞争 → panic
2 部分 export 失败场景 (V1 成功 V2 失败、V2 成功但 capability 未写入、remote publish 失败) meta-v 与实际能力不符
3 nil/空 metadata 不 panic、不写 cache consumer metadata fetch crash
4 Java golden vector:同一 metadata,Go/Java revision 完全一致 Java/Go 互操作 revision 永远不匹配
5 Multi-app same revision cache 隔离:appA:0 与 appB:0 不串 跨应用 metadata 错乱

P1 测试缺口 (重要)

# 测试项 风险
6 资源泄漏检测 (goroutine/memory/connection,使用 goleak) 长期运行 OOM
7 缓存 TTL/淘汰策略 内存泄漏、过期数据
8 多注册中心完整场景 (故障转移、数据一致性) 生产部署故障
9 性能基准测试 + CI benchstat 回归 无性能 SLA
10 升级/降级兼容性测试 (滚动升级、版本共存) 升级中断服务

P2 测试缺口 (后续扩展,不应阻塞 phase 1)

  • 灰度发布场景
  • 服务治理联动 (路由/限流/熔断)
  • 配置热更新
  • 1000+ 服务实例压力测试
  • 接口级↔应用级动态切换
  • 网络分区与 toxiproxy 超时恢复

测试基础设施需求

基础设施 用途
嵌入式 Nacos / testcontainers-go 集成测试注册中心
Mock Metadata Report 单元测试 + 故障注入
Java Dubbo Docker 容器 互操作性双向测试
toxiproxy 网络故障注入
独立 benchmark CI runner 性能回归检测

三、与 Java Dubbo 3.3 源码对比验证

已验证对齐的项 ✓

项 Java Dubbo 3.3 方案 状态
MetadataServiceV2 proto GetMetadataInfo + GetOpenAPIInfo 对齐 (PR2) ✓
ServiceInfoV2.Port int32 port 字段存在 保留 Port 字段 ✓
Revision 计算 app + sorted services + toDescString 方案第 8.2 节 ✓ (方向正确)
calAndGetRevision synchronized + updated 标志 方案第 8.2 节 ✓ (概念对齐)
EMPTY_REVISION Java RevisionResolver.EMPTY_REVISION = "0" 方案用 "0",值已对齐;需补充常量名引用 ✓(需澄清命名)
ServiceInstance 主地址 preferred protocol endpoint 方案第 11.4 节 ✓ (方向正确)
日志格式 [METADATA_REGISTER] 前缀 方案第 15 节 ✓

需要对齐但方案未明确的项 ⚠️

项 Java Dubbo 3.3 方案 状态
ServiceInfo key 格式 {group}/{interface}:{version}:{protocol},空值省略 未明确分隔符和空值规则 ⚠️ C-1
MatchKey 空 protocol protocol 为空时不追加冒号 当前 Go 实现无条件追加 ⚠️ W-10
extendParams/instanceParams 两个独立 map 未说明 Go 侧是否需要字段级等价 ⚠️ W-3
MetadataParamsFilter SPI 过滤运行时参数 提到轻量过滤但未定义 SPI ⚠️
EMPTY 常量命名 RevisionResolver.EMPTY_REVISION 值 "0" 已对齐,但未引用常量名 ⚠️ W-9
rawMetadataInfo JSON 缓存与 revision 绑定 未提及 ⚠️

四、方案优点 (值得肯定的设计决策)

  1. Go-first 约束清晰 — 明确拒绝照搬 Java 架构 (ApplicationModel/ConfigManager),保持 Go 的包结构
  2. Strategy + Capability 分离 — "计划导出什么" vs "实际导出了什么" 分离,这是修复 meta-v 问题的关键
  3. meta-v 硬规则 — "meta-v=2.0.0 only if V2 actually exported" 是正确的核心原则
  4. 6 个 PR 的分步策略 — 每个 PR 范围可控,风险隔离好
  5. 双模型审查意识 — 方案本身就体现了区分 wire contract 和实现的理念
  6. 错误处理原则 — 第 14 节明确了 "metadata 获取失败不 panic" 的原则
  7. 24 条验收标准 — 虽然部分需要量化,但覆盖面是充分的

五、实施建议优先级

Phase 0: 方案修订 (在开始编码前)

  1. 明确 ServiceInfo key/match key 格式,包括空值规则和 common.MatchKey 空 protocol 修复 (C-1, W-10)
  2. 明确 revision 哈希算法必须 MD5,补充完整计算规格 (C-3)
  3. 补充 MetadataInfo 并发安全 API 语义分层,明确哪些路径必须用 snapshot (C-4)
  4. 明确持久化 cache key/format 迁移策略,cacheOnce 作用域修复 (C-5)
  5. 补充 EMPTY_REVISION 常量名引用 (W-9)
  6. 量化所有验收标准中的模糊项

Phase 1: PR 1-2 (低风险基础设施)

按方案原文执行 PR 1 (配置验证) 和 PR 2 (proto 对齐)。这两步风险低,可以先行。

Phase 2: PR 3-5 (核心变更,需先补测试)

在执行 PR 3-5 之前,先补充 P0 测试项。核心变更需要并发测试和失败场景测试保护。

Phase 3: PR 6 (注册语义变更,最后做)

One-instance registration 是最大风险的变更,必须在前面所有 PR 稳定且有回归测试保护后再做。


六、总结

维度 评分 说明
问题识别 ★★★★ 准确识别了 meta-v、revision、cache key 等核心问题
架构设计 ★★★☆ Go-first 方向正确,但并发安全和迁移路径需补强
Java 兼容性 ★★★☆ 大部分对齐,key 格式和 hash 算法需明确
测试计划 ★★ 框架存在,但并发/故障/性能测试严重缺失
实施计划 ★★★★ 6 PR 分步合理,风险隔离好
总体 ★★★☆ 方向正确、结构清晰,细节需补强

核心建议: 在开始编码前,先修订方案中的 CRITICAL 问题(重点是 revision wire contract、并发安全 API 语义、持久化 cache 迁移),补充 P0 测试基础设施,再按 PR 1→2→3→4→5→6 的顺序执行。PR 6 (one-instance registration) 应作为最后的收敛步骤,不要提前。

评论与回复

评论 1 · Oxidaner

永久链接 · 2026-05-24T05:57:56Z

This evaluation is related to the article dubbo-go-metadata-design-spec-zh.version-0

Dubbo-go Metadata 设计方案

目标仓库:apache/dubbo-go
基准分支:develop
相关讨论:apache/dubbo-go discussions #3300
相关问题示例:

1. 背景

Dubbo 3 正在逐步把服务发现模型从 接口级注册 演进到 应用级注册。

在应用级注册模式下,注册中心应该只保存轻量的应用实例信息。完整的服务元数据,包括导出的服务、协议、端口、方法、参数和服务定义,应通过 metadata 系统获取。

dubbo-go 已经具备 metadata 基础能力:

  • metadata/metadata_service.go
    • MetadataService
    • DefaultMetadataService
    • MetadataService V1 导出
    • MetadataService V2 导出
  • metadata/info/metadata_info.go
    • MetadataInfo
    • ServiceInfo
  • metadata/client.go
    • 本地 metadata RPC 获取
    • 远程 metadata report 获取
  • registry/servicediscovery/service_discovery_registry.go
    • provider 侧服务发现注册
  • registry/servicediscovery/service_instances_changed_listener_impl.go
    • consumer 侧实例变更处理
    • 按 revision 获取 metadata
    • 生成服务 URL

但是当前实现仍然存在几个问题:

  1. MetadataService V1 / V2 的导出策略还不够显式。
  2. meta-v 可能和实际导出的 MetadataService 版本不一致。
  3. metadata-service-protocol 已经存在于配置中,但不是所有 metadata 初始化路径都会把它传入 metadata options。它的默认行为、测试,以及它和 MetadataService V1/V2 导出策略之间的关系,都需要明确。
  4. MetadataInfo revision 计算还没有完全对齐 Java Dubbo。
  5. consumer 侧 metadata cache 只使用 revision,可能导致不同应用之间互相冲突。
  6. develop 上已经增强了多 provider URL 生成能力,但应用级实例语义、endpoint metadata 和回归覆盖仍需收敛。
  7. dubbo-go 的 MetadataServiceV2 proto 落后于当前 Java Dubbo 契约。

本设计的目标是让 dubbo-go metadata 机制具备更好的兼容性、确定性和生产可用性。


2. 目标

2.1 功能目标

  1. 支持 provider 侧 MetadataService V1 和 V2 导出。
  2. 支持 local 和 remote 两种 metadata 存储模式。
  3. 支持将应用 metadata 发布到 metadata report。
  4. 支持 consumer 按应用实例 revision 获取 metadata。
  5. 支持 Java Dubbo 3.3 consumer -> dubbo-go provider 的 metadata 互操作。
  6. 支持 dubbo-go consumer -> Java Dubbo 3.3 provider 的 metadata 互操作。
  7. 支持同一应用下的多个 provider 实例。
  8. 支持同一应用实例下的多个协议 endpoint。
  9. 确保 meta-v 严格匹配实际导出的 MetadataService 版本。
  10. 避免 metadata 获取失败导致 consumer panic。

2.2 非目标

第一阶段不需要实现完整 OpenAPI 生成。

但是 MetadataServiceV2 proto 和服务描述应该对齐 Java Dubbo,并包含 GetOpenAPIInfo。该方法第一阶段可以返回空定义。


3. 当前状态

3.1 MetadataInfo

当前 dubbo-go 有一个类似如下的 MetadataInfo 模型:

type MetadataInfo struct {
    App      string
    Revision string
    Tag      string
    Services map[string]*ServiceInfo

    exportedServiceURLs   map[string][]*common.URL
    subscribedServiceURLs map[string][]*common.URL
}

MetadataInfo 负责记录当前应用导出和订阅的服务。

主要文件:

metadata/info/metadata_info.go

3.2 MetadataService

当前 dubbo-go 有一个类似如下的 MetadataService 接口:

type MetadataService interface {
    GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]*common.URL, error)
    GetExportedServiceURLs() ([]*common.URL, error)
    GetSubscribedURLs() ([]*common.URL, error)
    Version() (string, error)
    GetMetadataInfo(revision string) (*info.MetadataInfo, error)
    GetMetadataServiceURL() (*common.URL, error)
}

主要文件:

metadata/metadata_service.go

3.3 MetadataService V2 Proto

当前 dubbo-go V2 proto 只包含:

service MetadataServiceV2 {
    rpc GetMetadataInfo(MetadataRequest) returns (MetadataInfoV2);
}

主要文件:

metadata/triple_api/proto/metadata_service_v2.proto

当前 Java Dubbo V2 proto 同时包含:

service MetadataServiceV2 {
    rpc GetMetadataInfo(MetadataRequest) returns (MetadataInfoV2);
    rpc GetOpenAPIInfo(OpenAPIRequest) returns (OpenAPIInfo);
}

因此,dubbo-go 应该对齐 Java Dubbo 的 V2 proto 契约。

3.4 ServiceInstance Metadata Keys

dubbo-go 已经定义了重要的 metadata 常量:

ExportedServicesRevisionPropertyName   = "dubbo.metadata.revision"
SubscribedServicesRevisionPropertyName = "dubbo.subscribed-services.revision"
MetadataStorageTypePropertyName        = "dubbo.metadata.storage-type"
MetadataServiceURLParamsPropertyName   = "dubbo.metadata-service.url-params"
MetadataServiceURLsPropertyName        = "dubbo.metadata-service.urls"
MetadataVersion                        = "meta-v"
ServiceInstanceEndpoints               = "dubbo.endpoints"

主要文件:

common/constant/key.go

这些 key 和 Java Dubbo 的服务实例 metadata 模型整体对齐。


4. 设计概览

4.1 Dubbo-go 适配边界

Java Dubbo 应该被视为 wire format、metadata key、MetadataService V2 proto、revision 语义和应用级服务发现行为的互操作契约。不应该把 Java Dubbo 的实现架构直接复制到 dubbo-go。

dubbo-go 实现应保持以下 Go-first 约束:

  1. metadata 状态继续保留在现有 metadata package 中,按 registryId 组织,并通过同步和快照 helper 增强。不要引入 Java 风格的 ApplicationModel 或 ConfigManager 层。
  2. MetadataService 导出策略和导出能力应实现为当前 metadata/export 路径内的小型 Go struct 或 resolver function。除非现有包结构自然需要,否则不要创建 Java 风格的 exporter framework。
  3. metadata report 集成继续通过现有 metadata/report.MetadataReport 接口完成,并把选中的 report 传递到 registry / service discovery 路径中。consumer 路径不要依赖全局第一个 report。
  4. one-instance registration 应作为协议、metadata fetch、cache 和 mapping 修复都有测试覆盖之后的最终收敛步骤。第一阶段实现可以在这些前置条件稳定之前保留当前 per-exported-URL registration 作为兼容桥。
  5. 只有当某个 URL 参数确实是实例级,并且在所有导出服务上只有一个一致值时,才把它提升到 ServiceInstance metadata。如果不同服务值不同,应保留在 ServiceInfo.Params 中,并在 consumer 侧从服务 metadata 恢复。
  6. 除非 Java 兼容性要求修改 wire contract,否则保留当前 dubbo-go 默认行为。例如第一阶段继续让空 metadata-service-protocol 解析为 dubbo,而不是静默加入 Java 风格的 tri 自动探测。

简而言之:

用 Java Dubbo 对齐协议和 metadata 兼容性。
用 dubbo-go 现有 metadata、registry、report package 边界落地实现。

4.2 流程

metadata 流程应为:

Provider Service Export
        |
        v
MetadataInfo Manager
        |
        +--------------------+
        |                    |
        v                    v
Local MetadataService     Remote MetadataReport
V1 / V2                   Nacos / ZK / etcd
        |                    |
        +---------+----------+
                  |
                  v
ServiceInstance Metadata
revision / storage-type / meta-v / url-params / endpoints
                  |
                  v
Consumer ServiceInstancesChangedListener
                  |
                  v
Fetch Metadata by revision
                  |
                  v
Convert ServiceInfo to provider URLs
                  |
                  v
Notify Registry Directory

4.3 核心原则

  1. 注册中心里的实例 metadata 应保持轻量。
  2. 完整服务信息应存储在 MetadataInfo 中。
  3. consumer 应使用 revision 判断是否需要再次获取 metadata。
  4. local 模式下,consumer 通过 provider MetadataService RPC 获取 metadata。
  5. remote 模式下,consumer 从 metadata report 获取 metadata。
  6. meta-v 必须匹配实际导出的 MetadataService 版本。

5. Metadata 存储模式

5.1 Local 模式

配置:

dubbo:
  application:
    metadata-type: local

行为:

  1. Provider 导出本地 MetadataService。
  2. Provider 将以下 key 写入服务实例 metadata:
    • dubbo.metadata.storage-type=local
    • dubbo.metadata.revision={revision}
    • dubbo.endpoints=[...]
    • dubbo.metadata-service.url-params={...}
    • meta-v=1.0.0 或 meta-v=2.0.0
    • 可选写入 dubbo.metadata-service.urls=[...],仅用于需要完整 metadata service URL 的兼容场景
  3. Consumer 收到服务实例事件。
  4. Consumer 根据实例 metadata 构造 MetadataService URL。
  5. Consumer 调用 MetadataService RPC 获取完整 MetadataInfo。

5.2 Remote 模式

配置:

dubbo:
  application:
    metadata-type: remote

  metadata-report:
    protocol: nacos
    address: 127.0.0.1:8848

行为:

  1. Provider 不需要暴露本地 MetadataService。
  2. Provider 将 MetadataInfo 发布到 metadata report。
  3. Provider 将以下 key 写入服务实例 metadata:
    • dubbo.metadata.storage-type=remote
    • dubbo.metadata.revision={revision}
    • dubbo.endpoints=[...]
  4. Consumer 通过 app + revision 从 metadata report 获取 MetadataInfo。

主要文件:

metadata/report_instance.go
metadata/client.go

6. MetadataService 版本策略

6.1 问题

一个已知兼容性问题是:

instance metadata:
  meta-v = 2.0.0

provider actually exported:
  org.apache.dubbo.metadata.MetadataService, version=1.0.0

这种情况下,Java Dubbo 3.x consumer 可能会尝试调用:

org.apache.dubbo.metadata.MetadataServiceV2:2.0.0

但 dubbo-go provider 实际只导出了 MetadataService V1,可能出现类似错误:

don't have this exporter, key: {app}/org.apache.dubbo.metadata.MetadataServiceV2:2.0.0

因此,meta-v 不能只从业务协议或 endpoint 信息推断,必须来自实际 MetadataService 导出结果。

6.2 Strategy 和 Capability 模型

不要把期望导出的行为和实际导出的能力混在一起。

MetadataServiceExportStrategy 表示 dubbo-go 计划导出什么。它来自:

  • metadata-type
  • metadata-service-protocol
  • 默认协议解析
type MetadataServiceExportStrategy struct {
    Protocol string
    ExportV1 bool
    ExportV2 bool
}

建议策略规则,以及请求的导出全部成功时预期的 metadata version:

metadata-type metadata-service-protocol planned Export V1 planned Export V2 meta-v from capability
local dubbo yes no 1.0.0
local tri yes yes 2.0.0
remote any no no not required
local empty yes no 1.0.0

导出前,strategy 必须 normalize 并校验 metadata-service-protocol:

  1. 空值保持当前 dubbo-go 默认行为,解析为 dubbo。
  2. dubbo 只导出 MetadataService V1。
  3. tri 导出 MetadataService V1 + V2。
  4. 任何其他显式配置的值都必须配置校验失败。不能把它隐式当成 triple,也不应该静默 fallback 到 dubbo,否则会隐藏拼写错误并产生误导性的 metadata capability。

当前 dubbo-go 需要修复的实现细节:metadata.NewOptions() 默认 protocol 是 dubbo,但调用 WithMetadataProtocol("") 会把这个默认值覆盖成空字符串。由于当前 exporter 把非 dubbo 当成 triple,空 protocol 可能意外导出 V1 + V2。option 层或 strategy resolver 必须在导出前 normalize 空值。

V2-only metadata export 不是 dubbo-go 现有配置,第一阶段不应引入。Java Dubbo 有 V2-only metadata export 逻辑,但 dubbo-go phase 1 应使用保守兼容策略:

local + tri 同时导出 V1 和 V2。
只有 V2 实际导出成功时才声明 V2。

第一阶段中,空 metadata-service-protocol 应保持当前 dubbo-go 默认行为,解析为 dubbo / V1。Java Dubbo 可以从已配置的业务协议自动探测 triple,但 dubbo-go 如果要加入同样的自动探测,应作为单独的兼容性变更,并配套显式测试。

如果社区后续需要 V2-only export,应作为单独的兼容性决策提出。

MetadataServiceExportCapability 表示实际导出了什么。只有 exporter 创建成功之后才写入:

type MetadataServiceExportCapability struct {
    Protocol string

    V1Exported bool
    V1URL      *common.URL

    V2Exported bool
    V2URL      *common.URL
}

这不需要新框架。在 dubbo-go phase 1 中,capability 可以放在现有 metadata package 中,靠近 serviceExporter;由 exportDubbo、exportTripleV1 和 exportV2 设置;再由当前负责写 meta-v 的 ServiceInstance customizer 读取。

硬规则:

meta-v=2.0.0 only if MetadataServiceV2 is actually exported and reachable.

实现不应只从 dubbo.metadata-service.url-params 推断 meta-v。应在导出完成后根据 export capability 解析:

func ResolveMetadataVersion(cap MetadataServiceExportCapability) string {
    if cap.V2Exported && cap.V2URL != nil {
        return constant.MetadataServiceV2Version
    }
    if cap.V1Exported && cap.V1URL != nil {
        return constant.MetadataServiceV1Version
    }
    return ""
}

如果某个协议导出路径目前无法返回 error,导出代码仍然应该只在 exporter 对象创建成功后记录 capability。V2 导出失败或被跳过时,绝不能发布 meta-v=2.0.0。


7. 配置设计

当前 ApplicationConfig 已经有类似字段:

MetadataType            string
MetadataServicePort     string
MetadataServiceProtocol string

主要文件:

global/application_config.go
config/application_config.go

当前 develop 有多个 metadata 初始化路径。server.go 在 server startup 期间已经把 Application.MetadataServiceProtocol 传给 metadata options,但 config/metadata_config.go 当前还没有这么做。需要更新它,确保基于 config 的启动路径使用同一个 protocol 值。

当前预期初始化形态:

func initMetadata(rc *RootConfig) error {
    opts := metadata.NewOptions(
        metadata.WithAppName(rc.Application.Name),
        metadata.WithMetadataType(rc.Application.MetadataType),
        metadata.WithPort(getMetadataPort(rc)),
        metadata.WithMetadataProtocol(rc.Application.MetadataServiceProtocol),
    )
    return opts.Init()
}

第一阶段最小改动是打通并测试所有活跃 metadata 初始化路径,明确默认行为,并在 metadata-service-protocol 为空时保持当前行为。基于 rc.Protocols 的协议自动探测可以后续再加,但必须有显式兼容性测试覆盖。

这必须在依赖后续 server startup 路径之前修复,因为当前 dubbo-go metadata export 被 package-level exportOnce 保护。第一次 Options.Init() 调用会生效;如果第一次调用没有传入 MetadataServiceProtocol,后续即使传入 tri 也不会重新导出 MetadataServiceV2。

MetadataService port 解析也应该感知 protocol。Java Dubbo 的内部 metadata service builder 会先使用显式配置的 metadata service port,然后从选中协议的运行中 server 或 protocol config 获取端口,最后 fallback 到随机端口。Dubbo-go 应保持同样的优先级语义,但不要引入 startup-order coupling:

  1. 如果配置了 metadata-service-port,使用它。
  2. 如果 metadata-service-protocol=tri 且没有配置 metadata service port,优先使用 RootConfig.Protocols 中配置的 tri protocol port;只有在不改变启动顺序、不制造 import cycle 的前提下,才使用已经可用的 running tri server port。
  3. 如果 metadata-service-protocol=dubbo 或为空,保留当前 dubbo/default protocol fallback。
  4. 如果没有匹配的 protocol port,使用随机端口并清晰记录日志。
  5. 不要把这个 metadata service port 用作应用实例 primary port;primary instance port 仍然从业务 endpoints 中选择。

建议用户配置:

dubbo:
  application:
    name: demo-provider
    metadata-type: local
    metadata-service-protocol: tri
    metadata-service-port: 20881

  protocols:
    tri:
      name: tri
      port: 50051

  registries:
    nacos:
      protocol: nacos
      address: 127.0.0.1:8848
      registry-type: service

主要更新文件:

config/metadata_config.go
metadata/options.go
server/server.go

8. MetadataInfo 设计

8.1 增加线程安全

当前 metadata 使用全局 map,应通过 manager 封装。

建议模型:

type MetadataManager struct {
    mu    sync.RWMutex
    infos map[string]*info.MetadataInfo
}

保持现有 package-level API 兼容,并增加 service unexport / unsubscribe 需要的缺失 remove helper:

func GetMetadataInfo(registryId string) *info.MetadataInfo
func AddService(registryId string, url *common.URL)
func RemoveService(registryId string, url *common.URL)
func AddSubscribeURL(registryId string, url *common.URL)
func RemoveSubscribeURL(registryId string, url *common.URL)

MetadataInfo 还应暴露不可变发布快照 / deep copy helper:

func (m *MetadataInfo) Clone() *MetadataInfo

clone 必须深度复制 App、Revision、Tag、Services、exported service URLs 和 subscribed service URLs,确保发布 remote metadata 或写入 consumer cache 后,不会观察到 provider live metadata object 后续的原地 mutation。

主要文件:

metadata/metadata.go
metadata/info/metadata_info.go

8.2 Revision 计算

Revision 计算应迁移到 MetadataInfo。

建议 API:

func (m *MetadataInfo) CalAndGetRevision() string {
    m.mu.Lock()
    defer m.mu.Unlock()

    if m.Revision != "" && !m.updated {
        return m.Revision
    }

    if len(m.Services) == 0 {
        m.Revision = "0"
        m.updated = false
        return m.Revision
    }

    m.Revision = m.calRevisionLocked()
    m.updated = false
    return m.Revision
}

建议确定性计算方式:

func (m *MetadataInfo) calRevisionLocked() string {
    keys := make([]string, 0, len(m.Services))
    for key := range m.Services {
        keys = append(keys, key)
    }
    sort.Strings(keys)

    var builder strings.Builder
    builder.WriteString(m.App)

    for _, key := range keys {
        builder.WriteString(m.Services[key].ToDescString())
    }

    return revision.Resolve(builder.String())
}

ServiceInfo.ToDescString():

func (s *ServiceInfo) ToDescString() string {
    return s.GetMatchKey() +
        strconv.Itoa(s.Port) +
        s.Path +
        sortedParamsString(s.Params)
}

只有稳定的服务语义应参与 revision。timestamp、进程本地地址等 runtime-only 或 noisy value,以及不会改变可调用服务行为的其他值,必须排除。

不要直接把当前 dubbo-go IncludeKeys 当成最终 revision 参数集合。现有集合包含 timestamp 等值,它们对 URL metadata 有用,但对稳定服务 revision 来说过于 noisy。

对 dubbo-go 来说,第一阶段先在 metadata/info 附近实现一个小型 internal filter function 或静态 include/exclude table,不要新增 Java 风格 SPI:

  1. 为 revision input 定义显式 service metadata 参数过滤函数。
  2. 只有当 service-level params 和 method-level params 会影响可调用服务语义时,才把它们纳入 revision。
  3. 排除 timestamp、pid、bind address、generated local address 等 runtime-only params 和其他进程本地值。
  4. instance-level params 和 service-level revision params 保持分离。
  5. 只有当 dubbo-go 有明确用例,并且有第三方定制测试覆盖时,后续再引入扩展 hook。

这更接近 Java Dubbo 当前 MetadataInfo.calRevision() 的设计,即 revision 基于:

app + sorted service desc strings

修改 revision algorithm 会影响 cache。旧 revision cache 和新 revision cache 不能混用。实现上应在 algorithm 变化时清理本地 metadata cache,或给 cache key/storage format 加版本,避免意外复用旧 revision entry。

主要更新文件:

metadata/info/metadata_info.go
registry/servicediscovery/customizer/service_revision_customizer.go

9. MetadataService V1 / V2 导出

9.1 MetadataService V1

服务名:

org.apache.dubbo.metadata.MetadataService

版本:

1.0.0

用途:

  • 保留 MetadataService V1 作为 legacy 和迁移 fallback。
  • V2 不可用时,支持 dubbo protocol metadata service。
  • 避免破坏仍依赖 V1 metadata 调用的现有 dubbo-go 用户。

Dubbo 2.7 兼容性不是本工作的主要设计目标。主要目标应是 Java Dubbo 3.3 和当前 dubbo-go develop。V1 应作为低成本 fallback 保留,但第一阶段不应增加 2.7 专属行为,也不应把它作为硬性验收要求。

9.2 MetadataService V2

服务名:

org.apache.dubbo.metadata.MetadataServiceV2

版本:

2.0.0

协议:

tri

必需方法:

GetMetadataInfo
GetOpenAPIInfo

建议第一阶段 GetOpenAPIInfo 实现:

func (m *MetadataServiceV2) GetOpenAPIInfo(
    ctx context.Context,
    req *tripleapi.OpenAPIRequest,
) (*tripleapi.OpenAPIInfo, error) {
    return &tripleapi.OpenAPIInfo{Definition: ""}, nil
}

主要更新文件:

metadata/triple_api/proto/metadata_service_v2.proto
metadata/metadata_service.go

MetadataInfo conversion 必须在两个方向都保留 ServiceInfo.Port。

Provider 侧 conversion:

func convertV2(serviceInfos map[string]*info.ServiceInfo) map[string]*tripleapi.ServiceInfoV2 {
    serviceInfoV2s := make(map[string]*tripleapi.ServiceInfoV2, len(serviceInfos))
    for key, serviceInfo := range serviceInfos {
        serviceInfoV2s[key] = &tripleapi.ServiceInfoV2{
            Name:     serviceInfo.Name,
            Group:    serviceInfo.Group,
            Version:  serviceInfo.Version,
            Protocol: serviceInfo.Protocol,
            Port:     int32(serviceInfo.Port),
            Path:     serviceInfo.Path,
            Params:   serviceInfo.Params,
        }
    }
    return serviceInfoV2s
}

Consumer 侧 conversion:

func convertMetadataInfoV2(v2 *tripleapi.MetadataInfoV2) *info.MetadataInfo {
    services := make(map[string]*info.ServiceInfo, len(v2.Services))
    for key, service := range v2.Services {
        services[key] = &info.ServiceInfo{
            Name:     service.Name,
            Group:    service.Group,
            Version:  service.Version,
            Protocol: service.Protocol,
            Port:     int(service.Port),
            Path:     service.Path,
            Params:   service.Params,
        }
    }
    return info.NewMetadataInfoWithParams(v2.App, v2.Version, services)
}

10. ServiceInstance Metadata

Provider 应根据 metadata storage mode 写入不同的 service instance metadata。

10.1 通用 Metadata

以下 metadata 应在 local 和 remote 模式下都写入:

dubbo.metadata.storage-type = local | remote
dubbo.metadata.revision = {revision}
dubbo.endpoints = [{"protocol":"tri","port":50051},{"protocol":"dubbo","port":20880}]

dubbo.endpoints 描述同一应用实例暴露的 protocol ports:

[
  {"protocol": "tri", "port": 50051},
  {"protocol": "dubbo", "port": 20880}
]

Consumer URL expansion 应保留 dubbo-go 的 ServiceInstance.ToURLs(serviceInfo) 形态,但调整选择规则以匹配 Dubbo 3 metadata contract:

  1. 如果 ServiceInfo.Port > 0,使用 ServiceInfo.Protocol 和 ServiceInfo.Port 构造 URL。
  2. 如果 ServiceInfo.Port 缺失或为 0,根据 ServiceInfo.Protocol 从 dubbo.endpoints 中选择 endpoint。
  3. 如果同一个 protocol 存在多个 endpoint,ServiceInfo.Port 是唯一可靠的消歧方式;测试必须覆盖该情况,或者显式拒绝 same-protocol multi-port export。

Dubbo-go customizer 必须从当前 instance 的 ServiceMetadata / 当前 registry MetadataInfo 计算 dubbo.endpoints,不能从全局 metadata service 的聚合 exported URL list 计算。当前 DefaultMetadataService.GetExportedServiceURLs() 会跨 registry id 聚合 metadata,因此在 instance customizer 中使用它可能把一个 registry context 的 endpoint 或 revision 泄漏到另一个 context。

dubbo.subscribed-services.revision 和 dubbo.metadata.revision 不是同一个值,不应该从 exported-services revision 复制。如果 dubbo-go 保留该 key 用于 subscribed URL metadata,它必须从 subscribed URLs 独立计算,不应被当成通用 provider instance metadata。

主要文件:

registry/service_instance.go
registry/servicediscovery/customizer/

10.2 本地 MetadataService Metadata

当 metadata-type=local 且本地 MetadataService 已导出时,provider 还应额外写入:

dubbo.metadata-service.url-params = {...}
meta-v = 1.0.0 | 2.0.0

这些 key 用于 consumer 构造 MetadataService URL,并从 provider 获取完整 MetadataInfo。

dubbo.metadata-service.urls 是可选兼容 metadata,不是每个 dubbo-go local provider 都应该默认写出的标准字段。

dubbo.metadata-service.url-params

示例:

{
  "protocol": "tri",
  "port": "20881",
  "version": "2.0.0",
  "release": "dubbo-golang-3.x.x"
}

主要文件:

registry/servicediscovery/customizer/metadata_service_url_params_customizer.go

该 JSON 必须描述 metadata service endpoint,而不是 business service endpoint。如果 V1 和 V2 都通过 triple 导出,该字段可以指向优先使用的 V2-capable endpoint,但 export capability 仍然是 meta-v 的事实来源。

dubbo.metadata-service.urls

dubbo.metadata-service.urls 是 Java Dubbo 为 Dubbo Spring Cloud 和 metadata discovery 兼容使用的兼容字段。标准 dubbo-go 应用级注册不应默认写出它。

重要 Java 兼容性说明:

Java Dubbo 不会把 dubbo.metadata-service.urls 当成 url-params 后面的 fallback。
如果 dubbo.metadata-service.urls 存在,Java Dubbo 可能选择 SpringCloudMetadataServiceURLBuilder 路径。

因此,dubbo-go 只有在该值确实是可完整使用的 metadata service URL list,并且目标就是该兼容路径时,才应写入该 key。

推荐格式:

[
  "tri://127.0.0.1:20881/org.apache.dubbo.metadata.MetadataServiceV2?group=demo-provider&interface=org.apache.dubbo.metadata.MetadataServiceV2&version=2.0.0",
  "tri://127.0.0.1:20881/org.apache.dubbo.metadata.MetadataService?group=demo-provider&interface=org.apache.dubbo.metadata.MetadataService&version=1.0.0&serialization=hessian2"
]

规则:

  1. 该值是 metadata service URL string 的 JSON array。
  2. URL 描述 MetadataService endpoint,而不是 business service endpoint。
  3. 如果 V2 和 V1 都已导出,优先列出 V2 URL,再列出 V1。
  4. 当该 key 存在时,Java Dubbo consumer 必须能够直接使用这个 URL list。
  5. dubbo-go consumer 可以在 url-params 缺失、非法或无法构造可用 MetadataService URL 时,把这个 key 作为 local fallback。
  6. 解析出的 URL capability 仍必须和 meta-v 校验;urls 不能导致在 V2 实际没有导出时推断出 meta-v=2.0.0。

meta-v

meta-v 应来自实际 export capability:

func ResolveMetadataVersion(cap MetadataServiceExportCapability) string {
    if cap.V2Exported && cap.V2URL != nil {
        return constant.MetadataServiceV2Version
    }
    if cap.V1Exported && cap.V1URL != nil {
        return constant.MetadataServiceV1Version
    }
    return ""
}

主要文件:

registry/servicediscovery/customizer/metadata_service_version_customizer.go

当前 customizer 从 dubbo.metadata-service.url-params 推导 meta-v。这不充分,因为 URL params 可以写 tri,但 V2 export 未必真的成功。customizer 应读取已记录的 metadata service export capability。

10.3 Remote Metadata Report 模式

当 metadata-type=remote 时,provider 不需要 MetadataService RPC metadata key:

dubbo.metadata-service.url-params
dubbo.metadata-service.urls
meta-v

完整 MetadataInfo 会发布到 metadata report,consumer 使用 app + revision 获取。


11. Provider 生命周期

11.1 服务导出

预期流程:

ServiceOptions.Export()
    -> build provider URL
    -> registry protocol Register(url)
    -> metadata.AddService(registryId, url)
    -> serviceNameMapping.Map(url)

主要文件:

server/action.go
registry/servicediscovery/service_discovery_registry.go
metadata/metadata.go

11.2 接口到应用 Mapping

应用级服务发现仍然需要 interface-to-application mapping。

这里有两条独立的发现链路:

interface -> provider applications mapping
provider application instance -> MetadataInfo

Provider 侧:

  1. 每个 exported provider URL 仍必须写入 service name mapping。
  2. 注册一个 ServiceInstance 并不意味着可以移除 serviceNameMapping.Map(url)。
  3. MetadataInfo 描述一个应用导出了什么,但 mapping 告诉 consumer 应该订阅哪些应用。

Consumer 侧:

  1. 如果配置了 provided-by,consumer 可以直接订阅这些应用。
  2. 如果没有配置 provided-by,consumer 必须通过 service name mapping 解析 provider applications。
  3. 解析出 applications 后,consumer 订阅 application instances,并按 {providerApp}:{revision} 获取 MetadataInfo。

Remote metadata report 和 service name mapping 是两类职责:

  1. Metadata report 存储和获取完整 MetadataInfo。
  2. Service name mapping 解析 interface -> provider applications。
  3. 它们可以共享同一个后端,但不能被当成同一份数据。

当前 dubbo-go 默认的 metadata-based service name mapping 依赖 metadata report instances。没有 metadata report backend 时,consumer 应使用 provided-by 或其他显式 mapping source。因此,mapping 的验收测试应包含一个配置好的、支持 service app mapping 的 metadata report backend。

Mapping listener 能力和 backend 有关。当前 dubbo-go 中 Nacos 和 Zookeeper 有 listener-oriented 实现,而 Etcd 没有提供同等动态 mapping listener 行为。因此,验证动态 interface -> application mapping 的测试必须指定支持 listener 的 backend,而不是假设所有 metadata report backend 行为相同。

多 registry 场景下,mapping lookup 和 mapping listener 也应该使用当前 registry/service discovery context 关联的 metadata report,而不是全局第一个 metadata report。当前 provider mapping 会写入所有 metadata reports,因为 exported provider URLs 不携带 registry id;但 consumer 侧 Get / listener registration 仍需要使用正确 backend,避免监听错误的 mapping source。

11.3 注册和刷新应用实例

建议 provider 侧注册流程:

func (s *serviceDiscoveryRegistry) RegisterService() error {
    registryId := s.url.GetParam(constant.RegistryIdKey, constant.DefaultKey)

    metaInfo := metadata.GetMetadataInfo(registryId)
    if metaInfo == nil {
        return fmt.Errorf("metadata info not found, registry id = %s", registryId)
    }

    revision := metaInfo.CalAndGetRevision()

    if metadata.GetMetadataType() == constant.RemoteMetadataStorageType {
        report := metadata.GetMetadataReportByRegistry(registryId)
        if report == nil {
            return errors.New("metadata report not found")
        }
        if err := report.PublishAppMetadata(metaInfo.App, revision, metaInfo.Clone()); err != nil {
            return err
        }
    }

    instance := createInstance(metaInfo)
    instance.GetMetadata()[constant.ExportedServicesRevisionPropertyName] = revision

    return s.serviceDiscovery.Register(instance)
}

Application instance registration 必须做到每个进程、每个 registry 幂等。

Java Dubbo 把生命周期拆成:

registerMetadataAndInstance -> serviceDiscovery.register()
refreshMetadataAndInstance  -> serviceDiscovery.update()
unregisterMetadataAndInstance -> serviceDiscovery.unregister()

Dubbo-go 应遵循同样的语义模型:

  1. 初始 provider service export 将 exported URLs 收集到 MetadataInfo。
  2. 第一次 application registration 为进程注册一个 ServiceInstance。
  3. 后续 service export/unexport 或 metadata 语义变化会重新计算 revision。
  4. 如果 revision 或 instance metadata 变化,remote 模式下先发布更新后的 metadata。
  5. 然后更新或重新注册已有 ServiceInstance,而不是为同一个进程新增另一个 instance。
  6. shutdown 时注销这个单一 application instance。

如果某个 registry implementation 没有 native Update 操作,dubbo-go 可以用相同 stable instance ID 执行 unregister + register 来实现 refresh。

重要语义变更:

一个应用进程应只注册一个应用实例。

这是目标状态,不是第一步必须完成的实现。不要在 metadata fetch、cache key、endpoint expansion 和 service name mapping 都有回归覆盖之前切换到该模型。早期 PR 中,dubbo-go 可以保留当前 per-exported-URL registration 作为兼容桥。

多协议和多端口应通过:

dubbo.endpoints

表示。

这是 dubbo-go 在 Dubbo 3 应用级服务发现上长期应收敛的模型。实现仍需要尊重 dubbo-go 当前 registry abstraction 和 rollout 风险:短期实现可以继续把每个 exported service URL 注册为一个 instance 作为 bug-fix bridge,但这不是目标设计,因为注册中心应存应用实例,而 Dubbo metadata 应描述服务和 endpoints。

11.4 ServiceInstance 主地址

当每个进程只注册一个 application instance 时,provider 必须选择稳定的 primary instance address。

这和 Java Dubbo ServiceInstanceHostPortCustomizer 的方向一致:如果 instance 还没有 port,就从 exported business service URLs 里选一个,优先选择配置的 application protocol,找不到则 fallback 到第一个 exported URL。

建议规则:

  1. Host:使用进程注册 IP / advertised host。
  2. Port:使用 primary business endpoint。优先使用配置的 preferred/default business protocol endpoint port;如果不可用,使用第一个 exported service URL port。metadata-service port 不应作为 primary instance port,除非不存在 business endpoint。
  3. ID:使用稳定 instance ID,优先为 {host}:{primaryPort},除非 registry implementation 需要其他格式。
  4. 所有实际 business protocol ports 必须写入 dubbo.endpoints。
  5. MetadataService address 在 local 模式下通过 dubbo.metadata-service.url-params 写入,dubbo.metadata-service.urls 仅用于可选兼容场景。它不能作为主要 instance identity。
  6. Instance-level metadata 只能包含整个 application instance 一致的值。service-specific 或冲突值必须保留在 ServiceInfo.Params。

原因:

ServiceInstance 主地址代表应用实例身份。
MetadataService 只是用于获取 metadata 的内部地址。

如果把 metadata-service port 作为 primary port,instance identity 可能变得不稳定,尤其是 metadata-service port 是随机端口时。


12. Consumer 生命周期

当前 consumer 流程应保留并增强:

Interface subscribe
    -> if provided-by exists, use configured applications
    -> otherwise resolve applications through service name mapping
    -> subscribe provider application instances
ServiceInstancesChangedEvent
    -> group instances by app + revision
    -> get MetadataInfo from cache
    -> if cache miss:
         local: fetch from MetadataService RPC
         remote: fetch from MetadataReport
    -> MetadataInfo.Services
    -> instance.ToURLs(serviceInfo), preferring ServiceInfo.Port when present
    -> notify registry directory

主要文件:

registry/servicediscovery/service_instances_changed_listener_impl.go

12.1 Metadata 获取

Metadata fetch 应保留当前基于 storage-type 的判断:

if storageType == constant.RemoteMetadataStorageType {
    metadataInfo, err = metadata.GetMetadataFromMetadataReport(revision, instance)
} else {
    metadataInfo, err = metadata.GetMetadataFromRpc(revision, instance)
}

主要文件:

metadata/client.go
registry/servicediscovery/service_discovery_registry.go
registry/servicediscovery/service_instances_changed_listener_impl.go

Remote metadata fetch 必须使用当前 registry / service discovery context 关联的 metadata report,而不是任意全局第一个 report。Java Dubbo 在获取 remote metadata 时会把 metadata report 通过当前 service discovery flow 传递下去。Dubbo-go 应避免把 GetMetadataReport() 作为唯一 lookup path;需要把 registry id、registry cluster 或已选中的 metadata report 传入 listener/client 路径,确保 provider publish 和 consumer fetch 使用同一个 backend。

12.2 本地 RPC 获取 Fallback

建议 fallback 顺序:

1. 如果 meta-v=2.0.0,优先调用 MetadataServiceV2.GetMetadataInfo。
2. 如果 V2 失败且存在 V1 参数,fallback 到 MetadataService V1。
3. 如果响应是 string,为迁移兼容按 JSON 解析。
4. 如果某个 instance 失败,尝试同一 app + revision 下的其他 instance。
5. 如果所有尝试都失败,跳过该 revision 并记录 warning,不要 panic。

必需实现细节:

1. 如果 dubbo.metadata-service.url-params 缺失或非法,不要解引用 nil URL。
2. 标准路径优先使用 dubbo.metadata-service.url-params。
3. 对 dubbo-go consumer,如果 url-params 不能构造可用 URL,且 optional key 存在,则尝试 dubbo.metadata-service.urls。
4. V2 fallback 到 V1 时,用以下信息重建 URL:
   - interface/path = org.apache.dubbo.metadata.MetadataService
   - version = 1.0.0
   - method = getMetadataInfo
5. 只有返回非 nil 且包含 services 的 MetadataInfo 后,才写入 cache。

12.3 Cache Key

当前 cache 不应只使用 revision。

推荐 cache key:

{app}:{revision}

app 部分必须是 provider application,通常是 instance.GetServiceName(),而不是 listener 中保存的 consumer application。

建议 helper:

func buildMetadataCacheKey(instance registry.ServiceInstance, revision string) string {
    return instance.GetServiceName() + ":" + revision
}

两层 cache 都应使用这个 key:

cacheKey := buildMetadataCacheKey(instance, revision)

if metadataInfo, ok := metaCache.Get(cacheKey); ok {
    return metadataInfo.(*info.MetadataInfo), nil
}

metaCache.Set(cacheKey, metadataInfo)

ServiceInstancesChangedListenerImpl.revisionToMetadata 也应使用 {app}:{revision} 作为 key。否则内存 cache 仍然可能把不同 provider application 中碰巧 revision 相同的 metadata 混在一起。

cache manager 作用域也需要检查。当前 dubbo-go 使用 package-level metaCache 和 cacheOnce,并用第一个 app name 初始化。改成 {app}:{revision} key 后,要么使用一个明确 app-neutral 的共享 metadata cache 文件,要么按 provider app 维护 cache manager。不能让第一个订阅到的 app 决定后续所有 provider app 的持久化 cache storage。

原因:

不同应用可能生成相同 revision。
只使用 revision 会导致跨应用 metadata cache 冲突。

13. 兼容性矩阵

Provider Consumer metadata-type MetadataService 预期
dubbo-go dubbo-go local V1/V2 OK
dubbo-go dubbo-go remote metadata report OK
dubbo-go Java Dubbo 3.3 local + tri V2 preferred, V1 fallback OK
dubbo-go Java Dubbo 3.3 remote metadata report OK
Java Dubbo 3.3 dubbo-go local V2 preferred, V1 fallback OK
Java Dubbo 3.3 dubbo-go remote metadata report OK
dubbo-go Java Dubbo 2.7 local V1 Best effort / legacy fallback
Java Dubbo 2.7 dubbo-go local V1 Best effort / legacy fallback

关键兼容性规则:

meta-v 必须匹配实际导出的 MetadataService version。

Java Dubbo 2.7 兼容性应视为 legacy migration support。本设计不应围绕 Dubbo 2.7 应用级服务发现优化,因为目标基准是 Dubbo 3.x metadata 和 service-discovery 行为。


14. 错误处理

Metadata 是服务发现关键路径的一部分。单个异常 provider instance 不应导致 consumer 崩溃。

必需行为:

  1. Metadata RPC 失败返回 error,不 panic。
  2. Metadata 反序列化失败时跳过当前 instance。
  3. 如果一个 instance 失败,尝试同一 app + revision 下的其他 instance。
  4. 空 metadata 不应写入 cache。
  5. 缺失 revision 时记录 warning 并跳过。
  6. meta-v=2.0.0 且 V2 call 失败时,应在可能的情况下尝试 V1 fallback。
  7. 当 optional key 存在时,dubbo-go consumer 可以从非法 url-params fallback 到 dubbo.metadata-service.urls。

15. 指标和日志

建议指标:

metadata.push.rt
metadata.subscribe.rt
metadata.rpc.fetch.rt
metadata.report.fetch.rt
metadata.cache.hit
metadata.cache.miss
metadata.fetch.error

建议日志:

[METADATA_REGISTER] metadata revision changed: old -> new, app: demo, services: 3
[METADATA_REGISTER] publish remote metadata app=demo revision=xxx
[METADATA_SERVICE] export V1 url=...
[METADATA_SERVICE] export V2 url=...
[METADATA_SUBSCRIBE] fetch metadata app=demo revision=xxx storage=local
[METADATA_SUBSCRIBE] cache hit app=demo revision=xxx
[METADATA_SUBSCRIBE] fallback V2 -> V1 app=demo revision=xxx

16. 测试计划

16.1 单元测试

MetadataInfo

  • AddService 正确更新 services。
  • RemoveService 正确更新 services。
  • 相同 services 以不同插入顺序加入时生成相同 revision。
  • 参数变化会导致 revision 变化。
  • 方法级参数变化会导致 revision 变化。
  • runtime-only 参数变化,例如 timestamp 或 bind address,不会导致 revision 变化。
  • 空 services 返回 revision "0"。
  • MetadataInfo.Clone() 返回 publish/cache snapshot,后续 AddService / RemoveService 调用不会改变该 snapshot。

MetadataService Strategy and Capability

  • metadata-service-protocol=dubbo 只导出 V1。
  • metadata-service-protocol=tri 导出 V1 + V2。
  • metadata-service-protocol=tri 且没有 metadata-service-port 时,如果配置了 tri protocol port,则使用它。
  • 不支持的 metadata-service-protocol 值不会静默导出 V2。
  • metadata-type=remote 不导出本地 MetadataService。
  • Export capability 只在 exporter 创建成功后记录。
  • meta-v 匹配已记录的 export capability。
  • V2 export failure 绝不会产生 meta-v=2.0.0。
  • 标准 dubbo-go registration 不默认写出 dubbo.metadata-service.urls。
  • 当写出 dubbo.metadata-service.urls 时,它是 metadata service URL string 的 JSON array,并且 Java Dubbo Spring Cloud compatibility builder 可以直接使用。
  • Dubbo-go consumer 优先使用 url-params,只有当 url-params 不可用或无法使用时才 fallback 到 urls。

Proto Conversion

  • MetadataInfo -> MetadataInfoV2 不丢字段。
  • MetadataInfoV2 -> MetadataInfo 不丢字段。
  • ServiceInfo.Port、Path 和 Params 正确转换。
  • V2 conversion 保留 ServiceInfo.Port。

Metadata Cache

  • 相同 app 和相同 revision 复用 metadata。
  • 不同 app 即使 revision 相同,也不共享 metadata cache entries。
  • nil 或空 metadata 不写入 cache。
  • revision algorithm 改变后,不复用旧 algorithm 生成的本地 metadata cache entries。

ServiceInstance Metadata

  • 一致的 instance-level metadata,例如所有 exported services 上只有一个 environment 值时,会写入 application instance,并复制到生成的 consumer URLs。
  • 冲突的 per-service values 不会提升到 ServiceInstance metadata;它们保留在各自服务的 ServiceInfo.Params 中。
  • one-instance registration 不会把第一个 exported URL 的任意参数复制到整个 application instance。
  • ServiceInstance customizers 从 instance 自己的 ServiceMetadata / 当前 registry metadata 计算 revision 和 endpoints,而不是从全局聚合 exported URLs 计算。

Service Name Mapping

  • Provider export 仍会为每个 exported provider URL 调用 service name mapping。
  • 当没有配置 provided-by 且 metadata report backend 可用时,consumer 通过 service name mapping 解析 provider applications。
  • Metadata report data 和 service name mapping data 通过独立的逻辑 API 存取。
  • Dynamic mapping listener 测试使用支持 mapping listener 的 backend,例如 Nacos 或 Zookeeper;没有等价 listener 能力的 backend 应由 fallback behavior tests 覆盖。
  • Multi-registry mapping lookup/listener 使用当前 registry/service discovery context 关联的 metadata report。

16.2 集成测试

Local Metadata

Go provider:
  metadata-type=local
  metadata-service-protocol=tri

Go consumer:
  service discovery subscribe
  fetch MetadataServiceV2
  generate provider URL
  invoke successfully

Remote Metadata

Go provider:
  metadata-type=remote
  metadata-report=nacos

Go consumer:
  service discovery subscribe
  fetch metadata from metadata report
  invoke successfully

还应覆盖 multi-registry remote metadata:

registry A 将 app metadata 发布到 metadata report A
registry B 将 app metadata 发布到 metadata report B

通过 registry B 订阅的 consumer 必须从 metadata report B 获取,
而不是从全局第一个 metadata report 获取。

Java Dubbo 3.3 Compatibility

Java Dubbo 3.3 consumer -> Go provider V2
Java Dubbo 3.3 consumer -> Go provider V1 fallback
Go consumer -> Java Dubbo 3.3 provider V2
Go consumer -> Java Dubbo 3.3 provider V1 fallback

Dubbo 2.7 应继续作为 best-effort legacy 场景,在可行时由 V1 fallback 覆盖,但不应阻塞第一阶段验收标准。

Multi-provider

same application, 3 provider instances:
  port=20000
  port=20001
  port=20002

consumer 应看到 3 个 provider URLs。

Multi-protocol

同一个 provider 暴露:
  tri:50051
  dubbo:20880

instance metadata 应包含:
  dubbo.endpoints=[tri, dubbo]

consumer 应根据 ServiceInfo.Protocol 生成 URLs。

同一个 provider 在不同端口暴露同一个 protocol:
  tri:50051 for GreetService
  tri:50052 for UserService

consumer 应根据 ServiceInfo.Protocol + ServiceInfo.Port 生成 URLs。

Application-level Mapping

consumer reference without provided-by:
  interface=org.apache.dubbo.samples.GreetService

metadata report backend:
  service app mapping is enabled

mapping:
  GreetService -> greet-provider

consumer 应订阅 greet-provider instances,
按 greet-provider:{revision} 获取 MetadataInfo,
并成功调用。

17. 实施计划

PR 1:验证 MetadataService Protocol 配置

范围:

config/metadata_config.go
metadata/options.go
server/server.go

任务:

  • 保留并测试当前把 Application.MetadataServiceProtocol 传给 metadata options 的路径。
  • 同样通过 config/metadata_config.go 传递 Application.MetadataServiceProtocol。
  • 增加回归测试,证明第一个 metadata Options.Init() 调用在 exportOnce 阻止重新导出前已经收到配置的 protocol。
  • 确保 metadata-type=remote 不导出本地 metadata service。
  • 确保 WithMetadataProtocol("") 不会覆盖默认 dubbo protocol,也不会意外导出 triple MetadataService。
  • 当 metadata-service-protocol 为空时,保留现有默认行为:phase 1 解析为 dubbo / V1,而不是 Java 风格 protocol auto-detection。
  • 校验 metadata service protocol 值,任意非 dubbo/non-tri 值都应 fail fast,而不是静默导出 triple 或 fallback 到 dubbo。
  • 让 metadata service port fallback 感知 protocol:显式 metadata service port 优先,然后是选中 metadata service protocol port,最后随机端口并记录清晰 warning。
  • 增加单元测试。

PR 2:对齐 MetadataService V2 Proto

范围:

metadata/triple_api/proto/metadata_service_v2.proto
metadata/metadata_service.go

任务:

  • 增加 GetOpenAPIInfo。
  • 增加 OpenAPIRequest。
  • 增加 OpenAPIInfo。
  • 增加 OpenAPIFormat。
  • 实现空 GetOpenAPIInfo。
  • 在 MetadataInfo -> MetadataInfoV2 conversion 中保留 ServiceInfo.Port。
  • 在 MetadataInfoV2 -> MetadataInfo conversion 中保留 ServiceInfo.Port。
  • 重新生成 generated code。
  • 更新 service descriptor。

PR 3:增加 Export Capability 并修复 meta-v 生成

范围:

registry/servicediscovery/customizer/metadata_service_version_customizer.go
metadata/metadata_service.go
metadata/options.go

任务:

  • 增加 MetadataServiceExportStrategy 表示计划导出行为。
  • 增加 MetadataServiceExportCapability 存储实际导出结果。
  • 只在 exporter 创建成功后记录 capability。
  • 根据实际 export capability 生成 meta-v。
  • 确保只有 V2 已导出时才写出 meta-v=2.0.0。
  • 停止只从 dubbo.metadata-service.url-params 推导 meta-v。
  • 将 dubbo.metadata-service.urls 定义为可选兼容 JSON array,内容为 metadata service URL strings。
  • 标准 dubbo-go registration 不默认写出 dubbo.metadata-service.urls。
  • 如果写出 dubbo.metadata-service.urls,确保 Java Dubbo 可以直接使用 URL list。
  • 保持 dubbo-go phase 1 策略:local + tri 导出 V1 + V2,不引入 V2-only export。
  • 增加 Java Dubbo 3.3 consumer 兼容测试。

PR 4:改进 Consumer Metadata Fetch

范围:

metadata/client.go
registry/servicediscovery/service_discovery_registry.go
registry/servicediscovery/service_instances_changed_listener_impl.go

任务:

  • 将持久化 cache key 改为 provider {app}:{revision}。
  • 将 revisionToMetadata 内存 map key 改为 provider {app}:{revision}。
  • 修复 cache manager 作用域,避免 package-level cacheOnce 让第一个订阅到的 app 拥有所有 provider app 的持久化 cache storage。
  • 增加 V2 -> V1 fallback。
  • 避免 nil metadata panic。
  • 对 dubbo-go consumer,当 url-params 非法时,如果 optional key 存在,则 fallback 到 dubbo.metadata-service.urls。
  • 将 dubbo.metadata-service.urls 解析为 metadata service URL string 的 JSON array,并按 meta-v 和 fallback 顺序选择 V2/V1。
  • 不缓存 nil 或空 metadata。
  • 尝试同 revision 下的其他 instance。
  • 使用当前 registry/service discovery context 关联的 metadata report,而不是全局第一个 metadata report。
  • 增加集成测试。

PR 5:改进 MetadataInfo Revision 和 Snapshot

范围:

metadata/metadata.go
metadata/info/metadata_info.go
registry/servicediscovery/customizer/service_revision_customizer.go

任务:

  • 用同步机制封装全局 metadata maps,并增加 package-level RemoveService / RemoveSubscribeURL helper。
  • 增加 CalAndGetRevision()。
  • 增加 MetadataInfo.Clone(),或等价的 publish snapshot helper,用于 remote metadata publication 和 consumer cache write。
  • 增加 ServiceInfo.ToDescString()。
  • 替换 customizer revision 逻辑,让 exported-services revision 从当前 instance ServiceMetadata / registry MetadataInfo 计算,而不是从全局聚合 exported URLs 计算。
  • 增加轻量 internal service metadata parameter filter,排除 timestamp、pid、bind address、generated local address 等 runtime-only params。phase 1 不增加新的 extension SPI。
  • 清理或 version local metadata cache,避免旧 revision entries 和新 revision entries 混用。
  • 增加确定性 revision 测试。

PR 6:收敛应用级实例语义

范围:

registry/servicediscovery/service_discovery_registry.go
registry/service_instance.go
registry/servicediscovery/customizer/
metadata/mapping/metadata/service_name_mapping.go

任务:

  • 用回归测试保护 develop 的 multi-provider URL generation 行为。
  • 将 provider registration 逐步迁移到每个进程一个 application instance。
  • 将所有 exported services 存储到 MetadataInfo。
  • 将 protocol ports 存储到 dubbo.endpoints,并让 consumer URL expansion 在 ServiceInfo.Port 存在时优先使用它。
  • 从当前 instance ServiceMetadata / registry metadata 计算 dubbo.endpoints,不要从 metadata.GetMetadataService().GetExportedServiceURLs() 计算。
  • 选择稳定的 ServiceInstance primary address:host 来自进程注册 IP / advertised host;port 来自 preferred/default business protocol endpoint,否则使用第一个 exported service URL;只有没有 business endpoint 时才使用 metadata-service port;ID 优先使用 {host}:{primaryPort},除非 registry 要求其他格式。
  • 每个进程、每个 registry 保持一个已注册 application instance。
  • metadata 变化通过 service discovery update 刷新;如果没有 native update,则使用相同 stable instance ID 执行 unregister + register。
  • revision 变化或导出额外服务时,不追加重复 ServiceInstances。
  • instance-level metadata,例如 environment,只有在所有 exported services 上只有一个一致值时才写到 registered application instance。如果不同 exported service 的值不同,保留在 ServiceInfo.Params 并在 consumer 侧从 service metadata 恢复。
  • 为每个 exported provider URL 保留 serviceNameMapping.Map(url)。
  • 验证当没有 provided-by,且配置了支持 service app mapping 的 metadata report backend 时,consumer 可以通过 service name mapping 发现。
  • 在支持 listener 的 backend 上验证 dynamic mapping listener 行为,例如 Nacos 或 Zookeeper;对没有 listener 支持的 backend 验证 fallback behavior。
  • 验证 multi-registry service name mapping lookup/listener 不使用无关的全局第一个 metadata report。
  • 在改变 registration semantics 之前,增加 Nacos multi-provider 和 multi-protocol 集成测试。
  • 不要把 registration semantics 改动和 metadata protocol 或 meta-v 修复放在同一个 PR。

18. 验收标准

  1. 当配置 metadata-service-protocol: tri 时,provider 实际导出 MetadataServiceV2。
  2. 当 meta-v=2.0.0 时,Java Dubbo 3.3 consumer 可以调用 dubbo-go provider 的 MetadataServiceV2.GetMetadataInfo。
  3. 当配置 metadata-service-protocol: dubbo 时,不得写出 meta-v=2.0.0。
  4. 当配置 metadata-service-protocol: tri 且省略 metadata-service-port 时,metadata service port fallback 使用可用的 tri protocol port,而不是 dubbo protocol port。
  5. 当 metadata-type=remote 时,provider 不依赖本地 MetadataService,consumer 可以从 metadata report 获取 metadata。
  6. 在 multi-provider 场景下,consumer directory 能看到所有 provider URLs,并且有回归测试保护。
  7. 在 multi-protocol 场景下,consumer 根据 ServiceInfo.Protocol 选择正确 endpoint,并使用 ServiceInfo.Port 消歧 same-protocol multi-port services。
  8. Metadata fetch failures 不会 panic。
  9. 相同 {app}:{revision} 不会触发重复 metadata fetch。
  10. Revision 变化会触发 metadata refresh。
  11. Go / Java Dubbo 3.3 双向 metadata 兼容测试通过。
  12. Remote metadata fetch 使用当前 registry/service discovery context 关联的 metadata report。
  13. 不同 applications 即使 revision 相同,也不会共享 cached metadata。
  14. Metadata cache manager scope 是 app-neutral 或 per-provider-app;不会意外绑定到第一个 subscribed app。
  15. 长期应用级注册使用每个 application process 一个 ServiceInstance,services 存储在 MetadataInfo,ports 存储在 dubbo.endpoints。
  16. One-instance registration 选择稳定 primary business endpoint 作为 ServiceInstance address,不优先使用 metadata-service port 作为 primary port。
  17. 当没有配置 provided-by,并且配置了支持 mapping 的 metadata report backend 时,consumer 可以通过 service name mapping 发现 provider applications。
  18. Multi-registry service name mapping lookup/listener 使用当前 registry/service discovery context 关联的 metadata report,并且 dynamic listener 断言仅限支持 mapping listener 的 backend。
  19. Revision algorithm 变化不会复用旧 algorithm 的 stale local metadata cache entries。
  20. Service export/unexport 刷新现有 application instance metadata 和 revision,而不是注册 duplicate instances。
  21. Remote metadata publication 和 consumer metadata cache 存储稳定 metadata snapshots,而不是共享的 mutable provider metadata objects。
  22. One-instance registration 只提升 application instance 级一致的 metadata values;冲突的 service-level values 保留在 ServiceInfo.Params。
  23. Revision 和 endpoint customizers 使用当前 instance / registry metadata,不会跨 registry contexts 泄漏全局聚合 exported URLs。
  24. dubbo.metadata-service.urls 有明确 JSON URL-list 格式,不默认写出;一旦写出,Java Dubbo 的 Spring Cloud compatibility path 可以直接使用。

19. 推荐第一步

推荐顺序:

  1. 验证 metadata-service-protocol wiring 和默认行为,并补测试。
  2. 对齐 MetadataServiceV2 proto 与 Java Dubbo,包括 GetOpenAPIInfo 和 ServiceInfo.Port conversion。
  3. 增加 export strategy / capability recording,并根据实际 export capability 修复 meta-v 生成。
  4. 改进 consumer metadata fetch、fallback 和 {providerApp}:{revision} cache keys。
  5. 将 revision calculation 移入 MetadataInfo,并清理或 version 旧 metadata cache。
  6. 最后收敛 provider registration 到每个进程一个 application instance,并由 multi-provider、multi-protocol 和 service name mapping 回归测试保护。

这些改动足够小,社区更容易接受;同时也足够重要,可以修复真实的 Java / Go 互操作问题。


20. 总结

dubbo-go 不需要从零重建 metadata。

正确方向是增强现有架构:

MetadataInfo
+ MetadataService V1/V2
+ ServiceInstance metadata
+ MetadataReport
+ Consumer metadata fetch by revision

最重要的设计规则是:

Service instance metadata 必须描述实际 provider capability。

具体来说:

meta-v=2.0.0 表示 MetadataServiceV2 已实际导出且可访问。

只要保证这一点,Java Dubbo 和 dubbo-go 的 metadata 互操作就会更可预测。

评论 2 · Alanxtl

永久链接 · 2026-05-31T09:35:29Z

复盘下来,删减的直接原因很明确:#2534 是按 #2432 的方案做的一次主动瘦身,不是误删。

时间线大概是这样:

  1. 2023-09 的 #2432 先把当时 metadata 模块梳理了一遍,指出 metadata 逻辑散在 common/extension、config/instance、metadata/definition、metadata/identifier、metadata/service、registry/event 等很多地方。
  2. #2432 的评论里给了“优化 draft”:目标是简化 metadata center 处理逻辑,把 metadata 逻辑集中到 metadata 模块。
  3. 2024-03 的 #2534 落地了这个方案,commit 是 d6464e40 Refactor metadata,一次性 1703 insertions / 5227 deletions。
  4. 后面 #2760 合 dev-3.3 时,又把这套删减后的形态带进开发线,所以现在看起来就是 metadata 被“大幅删了”。

它当时的设计假设是:dubbo-go 3.x 主要服务发现路径已经转向应用级服务发现,所以 metadata center 不再重点存 provider/consumer/service-definition 级元数据,只保留应用级 MetadataInfo、service-app mapping、MetadataService。

所以被删的核心东西是:

  • metadata/definition:provider service definition / method definition 这套被判断为“不再需要”
  • metadata/identifier:老的 service/provider/consumer identifier 被判断为“不再需要”
  • metadata/service/local|remote|exporter:拆开的 MetadataService 本地/远程/exporter 抽象被压扁到 metadata_service.go(原文作者本地路径:/dubbo-go/metadata/metadata_service.go:51)
  • metadata/report/delegate:带失败重试、周期上报、provider/consumer metadata 上报的 delegate 层被删
  • common/extension/metadata_*、config/instance/metadata_report*:扩展点和全局实例缓存被认为太分散,迁到/收缩到 metadata 包内部

保留下来的新模型就是现在这套:MetadataInfo 作为应用级服务发现元数据容器,见 metadata_info.go(原文作者本地路径:/dubbo-go/metadata/info/metadata_info.go:58);MetadataReport 只负责 app metadata 和 mapping,见 report.go(原文作者本地路径:/dubbo-go/metadata/report/report.go:29);服务发现过程通过 revision 拿远端 app metadata。

我觉得当时这个删减有它的工程动机:老 metadata 模块确实重、散、和 config/registry/server lifecycle 纠缠很深,而且很多能力对 dubbo-go 当时的应用级服务发现主路径不是必需的。

评论 3 · Alanxtl

永久链接 · 2026-06-25T09:08:40Z

close via https://github.com/apache/dubbo-go/issues/3188

Clone this wiki locally