Dubbo-go Metadata Design Spec — 综合审查报告 #3342
Replies: 3 comments
-
|
This evaluation is related to the article dubbo-go-metadata-design-spec-zh.version-0 Dubbo-go Metadata 设计方案
1. 背景Dubbo 3 正在逐步把服务发现模型从 接口级注册 演进到 应用级注册。 在应用级注册模式下,注册中心应该只保存轻量的应用实例信息。完整的服务元数据,包括导出的服务、协议、端口、方法、参数和服务定义,应通过 metadata 系统获取。 dubbo-go 已经具备 metadata 基础能力:
但是当前实现仍然存在几个问题:
本设计的目标是让 dubbo-go metadata 机制具备更好的兼容性、确定性和生产可用性。 2. 目标2.1 功能目标
2.2 非目标第一阶段不需要实现完整 OpenAPI 生成。 但是 MetadataServiceV2 proto 和服务描述应该对齐 Java Dubbo,并包含 3. 当前状态3.1 MetadataInfo当前 dubbo-go 有一个类似如下的 type MetadataInfo struct {
App string
Revision string
Tag string
Services map[string]*ServiceInfo
exportedServiceURLs map[string][]*common.URL
subscribedServiceURLs map[string][]*common.URL
}
主要文件: 3.2 MetadataService当前 dubbo-go 有一个类似如下的 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)
}主要文件: 3.3 MetadataService V2 Proto当前 dubbo-go V2 proto 只包含: service MetadataServiceV2 {
rpc GetMetadataInfo(MetadataRequest) returns (MetadataInfoV2);
}主要文件: 当前 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 Keysdubbo-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"主要文件: 这些 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 约束:
简而言之: 4.2 流程metadata 流程应为: 4.3 核心原则
5. Metadata 存储模式5.1 Local 模式配置: dubbo:
application:
metadata-type: local行为:
5.2 Remote 模式配置: dubbo:
application:
metadata-type: remote
metadata-report:
protocol: nacos
address: 127.0.0.1:8848行为:
主要文件: 6. MetadataService 版本策略6.1 问题一个已知兼容性问题是: 这种情况下,Java Dubbo 3.x consumer 可能会尝试调用: 但 dubbo-go provider 实际只导出了 MetadataService V1,可能出现类似错误: 因此, 6.2 Strategy 和 Capability 模型不要把期望导出的行为和实际导出的能力混在一起。
type MetadataServiceExportStrategy struct {
Protocol string
ExportV1 bool
ExportV2 bool
}建议策略规则,以及请求的导出全部成功时预期的 metadata version:
导出前,strategy 必须 normalize 并校验
当前 dubbo-go 需要修复的实现细节: V2-only metadata export 不是 dubbo-go 现有配置,第一阶段不应引入。Java Dubbo 有 V2-only metadata export 逻辑,但 dubbo-go phase 1 应使用保守兼容策略: 第一阶段中,空 如果社区后续需要 V2-only export,应作为单独的兼容性决策提出。
type MetadataServiceExportCapability struct {
Protocol string
V1Exported bool
V1URL *common.URL
V2Exported bool
V2URL *common.URL
}这不需要新框架。在 dubbo-go phase 1 中,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 导出失败或被跳过时,绝不能发布 7. 配置设计当前 MetadataType string
MetadataServicePort string
MetadataServiceProtocol string主要文件: 当前 当前预期初始化形态: 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 初始化路径,明确默认行为,并在 这必须在依赖后续 server startup 路径之前修复,因为当前 dubbo-go metadata export 被 package-level MetadataService port 解析也应该感知 protocol。Java Dubbo 的内部 metadata service builder 会先使用显式配置的 metadata service port,然后从选中协议的运行中 server 或 protocol config 获取端口,最后 fallback 到随机端口。Dubbo-go 应保持同样的优先级语义,但不要引入 startup-order coupling:
建议用户配置: 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主要更新文件: 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)
func (m *MetadataInfo) Clone() *MetadataInfoclone 必须深度复制 主要文件: 8.2 Revision 计算Revision 计算应迁移到 建议 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())
}
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 对 dubbo-go 来说,第一阶段先在
这更接近 Java Dubbo 当前 修改 revision algorithm 会影响 cache。旧 revision cache 和新 revision cache 不能混用。实现上应在 algorithm 变化时清理本地 metadata cache,或给 cache key/storage format 加版本,避免意外复用旧 revision entry。 主要更新文件: 9. MetadataService V1 / V2 导出9.1 MetadataService V1服务名: 版本: 用途:
Dubbo 2.7 兼容性不是本工作的主要设计目标。主要目标应是 Java Dubbo 3.3 和当前 dubbo-go 9.2 MetadataService V2服务名: 版本: 协议: 必需方法: 建议第一阶段 func (m *MetadataServiceV2) GetOpenAPIInfo(
ctx context.Context,
req *tripleapi.OpenAPIRequest,
) (*tripleapi.OpenAPIInfo, error) {
return &tripleapi.OpenAPIInfo{Definition: ""}, nil
}主要更新文件: MetadataInfo conversion 必须在两个方向都保留 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 MetadataProvider 应根据 metadata storage mode 写入不同的 service instance metadata。 10.1 通用 Metadata以下 metadata 应在 local 和 remote 模式下都写入:
[
{"protocol": "tri", "port": 50051},
{"protocol": "dubbo", "port": 20880}
]Consumer URL expansion 应保留 dubbo-go 的
Dubbo-go customizer 必须从当前 instance 的
主要文件: 10.2 本地 MetadataService Metadata当 这些 key 用于 consumer 构造 MetadataService URL,并从 provider 获取完整
|
| 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 崩溃。
必需行为:
- Metadata RPC 失败返回 error,不 panic。
- Metadata 反序列化失败时跳过当前 instance。
- 如果一个 instance 失败,尝试同一
app + revision下的其他 instance。 - 空 metadata 不应写入 cache。
- 缺失 revision 时记录 warning 并跳过。
meta-v=2.0.0且 V2 call 失败时,应在可能的情况下尝试 V1 fallback。- 当 optional key 存在时,dubbo-go consumer 可以从非法
url-paramsfallback 到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 -> MetadataInfoV2conversion 中保留ServiceInfo.Port。 - 在
MetadataInfoV2 -> MetadataInfoconversion 中保留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/RemoveSubscribeURLhelper。 - 增加
CalAndGetRevision()。 - 增加
MetadataInfo.Clone(),或等价的 publish snapshot helper,用于 remote metadata publication 和 consumer cache write。 - 增加
ServiceInfo.ToDescString()。 - 替换 customizer revision 逻辑,让 exported-services revision 从当前 instance
ServiceMetadata/ registryMetadataInfo计算,而不是从全局聚合 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. 验收标准
- 当配置
metadata-service-protocol: tri时,provider 实际导出MetadataServiceV2。 - 当
meta-v=2.0.0时,Java Dubbo 3.3 consumer 可以调用 dubbo-go provider 的MetadataServiceV2.GetMetadataInfo。 - 当配置
metadata-service-protocol: dubbo时,不得写出meta-v=2.0.0。 - 当配置
metadata-service-protocol: tri且省略metadata-service-port时,metadata service port fallback 使用可用的 tri protocol port,而不是 dubbo protocol port。 - 当
metadata-type=remote时,provider 不依赖本地 MetadataService,consumer 可以从 metadata report 获取 metadata。 - 在 multi-provider 场景下,consumer directory 能看到所有 provider URLs,并且有回归测试保护。
- 在 multi-protocol 场景下,consumer 根据
ServiceInfo.Protocol选择正确 endpoint,并使用ServiceInfo.Port消歧 same-protocol multi-port services。 - Metadata fetch failures 不会 panic。
- 相同
{app}:{revision}不会触发重复 metadata fetch。 - Revision 变化会触发 metadata refresh。
- Go / Java Dubbo 3.3 双向 metadata 兼容测试通过。
- Remote metadata fetch 使用当前 registry/service discovery context 关联的 metadata report。
- 不同 applications 即使 revision 相同,也不会共享 cached metadata。
- Metadata cache manager scope 是 app-neutral 或 per-provider-app;不会意外绑定到第一个 subscribed app。
- 长期应用级注册使用每个 application process 一个 ServiceInstance,services 存储在
MetadataInfo,ports 存储在dubbo.endpoints。 - One-instance registration 选择稳定 primary business endpoint 作为 ServiceInstance address,不优先使用 metadata-service port 作为 primary port。
- 当没有配置
provided-by,并且配置了支持 mapping 的 metadata report backend 时,consumer 可以通过 service name mapping 发现 provider applications。 - Multi-registry service name mapping lookup/listener 使用当前 registry/service discovery context 关联的 metadata report,并且 dynamic listener 断言仅限支持 mapping listener 的 backend。
- Revision algorithm 变化不会复用旧 algorithm 的 stale local metadata cache entries。
- Service export/unexport 刷新现有 application instance metadata 和 revision,而不是注册 duplicate instances。
- Remote metadata publication 和 consumer metadata cache 存储稳定 metadata snapshots,而不是共享的 mutable provider metadata objects。
- One-instance registration 只提升 application instance 级一致的 metadata values;冲突的 service-level values 保留在
ServiceInfo.Params。 - Revision 和 endpoint customizers 使用当前 instance / registry metadata,不会跨 registry contexts 泄漏全局聚合 exported URLs。
dubbo.metadata-service.urls有明确 JSON URL-list 格式,不默认写出;一旦写出,Java Dubbo 的 Spring Cloud compatibility path 可以直接使用。
19. 推荐第一步
推荐顺序:
- 验证
metadata-service-protocolwiring 和默认行为,并补测试。 - 对齐 MetadataServiceV2 proto 与 Java Dubbo,包括
GetOpenAPIInfo和ServiceInfo.Portconversion。 - 增加 export strategy / capability recording,并根据实际 export capability 修复
meta-v生成。 - 改进 consumer metadata fetch、fallback 和
{providerApp}:{revision}cache keys。 - 将 revision calculation 移入
MetadataInfo,并清理或 version 旧 metadata cache。 - 最后收敛 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 互操作就会更可预测。
Beta Was this translation helpful? Give feedback.
-
|
复盘下来,删减的直接原因很明确:#2534 是按 #2432 的方案做的一次主动瘦身,不是误删。 时间线大概是这样:
它当时的设计假设是:dubbo-go 3.x 主要服务发现路径已经转向应用级服务发现,所以 metadata center 不再重点存 provider/consumer/service-definition 级元数据,只保留应用级 MetadataInfo、service-app mapping、MetadataService。 所以被删的核心东西是:
保留下来的新模型就是现在这套: 我觉得当时这个删减有它的工程动机:老 metadata 模块确实重、散、和 config/registry/server lifecycle 纠缠很深,而且很多能力对 dubbo-go 当时的应用级服务发现主路径不是必需的。 |
Beta Was this translation helpful? Give feedback.
-
|
close via #3188 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
审查日期: 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 格式,包括:
/,:)0.0.0时省略版本段关键实现细节: 当前
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:相同服务不同插入顺序 → 相同 revisionTestRevision_Deterministic_ParamInsertionOrder:params key/value 顺序不同 → 相同 revisionTestRevision_Deterministic_MethodParamInsertionOrder:method-level params 顺序不同 → 相同 revisionTestRevision_JavaGoldenVectors:同一 metadata,Go/Java revision 完全一致C-3: Hash 算法必须与 Java 一致
Java
RevisionResolver.calRevision()使用 MD5。方案中说用revision.Resolve()但未指明具体算法。风险: 如果 Go 用 SHA-256 而 Java 用 MD5,相同元数据会产生不同 revision,导致消费者误判元数据变更。
建议: 这是互操作契约,不是实现偏好,方案中应明确写"必须 MD5"而不是"建议 MD5"。完整规格:
添加跨语言 revision 一致性测试(golden vector 测试)。
C-4: map 并发安全 — 不能返回可变引用
Java
synchronized天然保护了所有访问。Go 的RWMutex如果只保护GetMetadataInfo()调用,但返回*MetadataInfo指针,调用方可以在锁外读写Servicesmap。风险: Go map 并发读写会 panic。即使有 RWMutex,返回的指针被并发访问仍然崩溃。
设计文档虽然要求
MetadataManager加RWMutex,也要求Clone()深拷贝,但同时保留了GetMetadataInfo(registryId string) *info.MetadataInfo这个返回 live pointer 的 API。Manager 的锁只能保护 map lookup,不能保护返回对象后的访问。建议:
GetMetadataInfoSnapshot(registryID string) *info.MetadataInfo:返回 deep clone,用于发布/cache/consumer 读取MutateMetadataInfo(registryID string, fn func(*info.MetadataInfo) error) error:受锁保护的写路径GetMetadataInfo如需保留兼容,注明"仅用于兼容旧调用,不应用于新写路径"Clone()方法必须深度复制所有 map(Services、exported URLs、subscribed URLs)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 是正常的升级行为,不是核心风险。
建议:
cacheOnce用第一个 app 初始化的问题需要修复:改为 app-neutral 共享 cache 或 per-provider-app cache managerWARNING (强烈建议修复)
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 分层语义,影响互操作。
建议: 方案应补充说明:
extendParams/instanceParams字段语义对等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 非确定性。
建议: 将选择规则写成可测试的确定性规格:
dubbo.application.preferred-protocol或同等配置)(protocol priority, host, port, service key))明确协议优先级顺序(如 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的 instancemetadata-service-protocol=tri且 V2 导出失败但 V1 成功,capability 只能声明 V1,写meta-v=1.0.0dubbo.metadata.storage-type=local的 provider instanceremote模式下,本地 MetadataService export failure 不影响启动W-7: 大规模场景下的内存管理(后续扩展风险)
MetadataInfo 包含所有服务的完整描述。100+ 服务 x 1000+ 方法 = MB 级元数据。此项不应阻塞 PR 1-3,但需要在方案中有明确的阈值策略。
建议: 第一阶段增加 metadata size warning 阈值,而非实现压缩:
具体阈值需结合 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 路径和外部反序列化后的对象初始化。建议:
TestMetadataInfo_ZeroValueSafeReadTestMetadataInfo_DeserializedNilServices_AddServiceTestServiceInfo_NilParams_ToDescStringTestServiceInfo_NilParams_GetMethodsW-9: EMPTY_REVISION 常量命名未明确
Java Dubbo 3.3
RevisionResolver.EMPTY_REVISION的值是"0",与设计文档 §8.2 的revision="0"一致,值已对齐。文档缺口: 方案未显式引用该常量名,读者可能误以为
"0"是 dubbo-go 自定义值,或者在实现时随意改为其他哨兵值。建议: 在设计文档中补充:
并在代码中定义对应常量,不要直接使用字面量
"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 测试。二、测试人员审查 — 测试计划缺口
现有测试规范质量评分
综合评分: ★★ (2/5) — 框架存在但存在显著缺口
P0 测试缺口 (必须修复)
go test -race,100 goroutines 并发 Add/Remove/Clone/CalAndGetRevision)P1 测试缺口 (重要)
P2 测试缺口 (后续扩展,不应阻塞 phase 1)
测试基础设施需求
三、与 Java Dubbo 3.3 源码对比验证
已验证对齐的项 ✓
RevisionResolver.EMPTY_REVISION = "0""0",值已对齐;需补充常量名引用需要对齐但方案未明确的项⚠️
{group}/{interface}:{version}:{protocol},空值省略MatchKey空 protocolRevisionResolver.EMPTY_REVISION"0"已对齐,但未引用常量名四、方案优点 (值得肯定的设计决策)
五、实施建议优先级
Phase 0: 方案修订 (在开始编码前)
common.MatchKey空 protocol 修复 (C-1, W-10)cacheOnce作用域修复 (C-5)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 稳定且有回归测试保护后再做。
六、总结
核心建议: 在开始编码前,先修订方案中的 CRITICAL 问题(重点是 revision wire contract、并发安全 API 语义、持久化 cache 迁移),补充 P0 测试基础设施,再按 PR 1→2→3→4→5→6 的顺序执行。PR 6 (one-instance registration) 应作为最后的收敛步骤,不要提前。
Beta Was this translation helpful? Give feedback.
All reactions