GitHub user Oxidaner added a comment to the discussion: Dubbo-go Metadata Design Spec — 综合审查报告
This evaluation is related to the article dubbo-go-metadata-design-spec-zh.version-0 # Dubbo-go Metadata 设计方案 > 目标仓库:[apache/dubbo-go](https://github.com/apache/dubbo-go) > 基准分支:`develop` > 相关讨论:[apache/dubbo-go discussions > #3300](https://github.com/apache/dubbo-go/discussions/3300) > 相关问题示例: > > - [#2939: MetadataService V1 exported but > meta-v=2.0.0](https://github.com/apache/dubbo-go/issues/2939) > - [#3188: dubbo-go metadata report > discussion](https://github.com/apache/dubbo-go/issues/3188) > - [#3302: Nacos multi-provider directory only exposes one > instance](https://github.com/apache/dubbo-go/issues/3302) ## 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` 模型: ```go type MetadataInfo struct { App string Revision string Tag string Services map[string]*ServiceInfo exportedServiceURLs map[string][]*common.URL subscribedServiceURLs map[string][]*common.URL } ``` `MetadataInfo` 负责记录当前应用导出和订阅的服务。 主要文件: ```text metadata/info/metadata_info.go ``` ### 3.2 MetadataService 当前 dubbo-go 有一个类似如下的 `MetadataService` 接口: ```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) } ``` 主要文件: ```text metadata/metadata_service.go ``` ### 3.3 MetadataService V2 Proto 当前 dubbo-go V2 proto 只包含: ```proto service MetadataServiceV2 { rpc GetMetadataInfo(MetadataRequest) returns (MetadataInfoV2); } ``` 主要文件: ```text metadata/triple_api/proto/metadata_service_v2.proto ``` 当前 Java Dubbo V2 proto 同时包含: ```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 常量: ```go 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" ``` 主要文件: ```text 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 自动探测。 简而言之: ```text 用 Java Dubbo 对齐协议和 metadata 兼容性。 用 dubbo-go 现有 metadata、registry、report package 边界落地实现。 ``` ### 4.2 流程 metadata 流程应为: ```text 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 模式 配置: ```yaml 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 模式 配置: ```yaml 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`。 主要文件: ```text metadata/report_instance.go metadata/client.go ``` --- ## 6. MetadataService 版本策略 ### 6.1 问题 一个已知兼容性问题是: ```text instance metadata: meta-v = 2.0.0 provider actually exported: org.apache.dubbo.metadata.MetadataService, version=1.0.0 ``` 这种情况下,Java Dubbo 3.x consumer 可能会尝试调用: ```text org.apache.dubbo.metadata.MetadataServiceV2:2.0.0 ``` 但 dubbo-go provider 实际只导出了 MetadataService V1,可能出现类似错误: ```text 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` - 默认协议解析 ```go 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 应使用保守兼容策略: ```text local + tri 同时导出 V1 和 V2。 只有 V2 实际导出成功时才声明 V2。 ``` 第一阶段中,空 `metadata-service-protocol` 应保持当前 dubbo-go 默认行为,解析为 `dubbo` / V1。Java Dubbo 可以从已配置的业务协议自动探测 triple,但 dubbo-go 如果要加入同样的自动探测,应作为单独的兼容性变更,并配套显式测试。 如果社区后续需要 V2-only export,应作为单独的兼容性决策提出。 `MetadataServiceExportCapability` 表示实际导出了什么。只有 exporter 创建成功之后才写入: ```go 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 读取。 硬规则: ```text meta-v=2.0.0 only if MetadataServiceV2 is actually exported and reachable. ``` 实现不应只从 `dubbo.metadata-service.url-params` 推断 `meta-v`。应在导出完成后根据 export capability 解析: ```go 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` 已经有类似字段: ```go MetadataType string MetadataServicePort string MetadataServiceProtocol string ``` 主要文件: ```text global/application_config.go config/application_config.go ``` 当前 `develop` 有多个 metadata 初始化路径。`server.go` 在 server startup 期间已经把 `Application.MetadataServiceProtocol` 传给 metadata options,但 `config/metadata_config.go` 当前还没有这么做。需要更新它,确保基于 config 的启动路径使用同一个 protocol 值。 当前预期初始化形态: ```go 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 中选择。 建议用户配置: ```yaml 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 ``` 主要更新文件: ```text config/metadata_config.go metadata/options.go server/server.go ``` --- ## 8. MetadataInfo 设计 ### 8.1 增加线程安全 当前 metadata 使用全局 map,应通过 manager 封装。 建议模型: ```go type MetadataManager struct { mu sync.RWMutex infos map[string]*info.MetadataInfo } ``` 保持现有 package-level API 兼容,并增加 service unexport / unsubscribe 需要的缺失 remove helper: ```go 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: ```go 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。 主要文件: ```text metadata/metadata.go metadata/info/metadata_info.go ``` ### 8.2 Revision 计算 Revision 计算应迁移到 `MetadataInfo`。 建议 API: ```go 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 } ``` 建议确定性计算方式: ```go 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()`: ```go 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 基于: ```text app + sorted service desc strings ``` 修改 revision algorithm 会影响 cache。旧 revision cache 和新 revision cache 不能混用。实现上应在 algorithm 变化时清理本地 metadata cache,或给 cache key/storage format 加版本,避免意外复用旧 revision entry。 主要更新文件: ```text metadata/info/metadata_info.go registry/servicediscovery/customizer/service_revision_customizer.go ``` --- ## 9. MetadataService V1 / V2 导出 ### 9.1 MetadataService V1 服务名: ```text org.apache.dubbo.metadata.MetadataService ``` 版本: ```text 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 服务名: ```text org.apache.dubbo.metadata.MetadataServiceV2 ``` 版本: ```text 2.0.0 ``` 协议: ```text tri ``` 必需方法: ```text GetMetadataInfo GetOpenAPIInfo ``` 建议第一阶段 `GetOpenAPIInfo` 实现: ```go func (m *MetadataServiceV2) GetOpenAPIInfo( ctx context.Context, req *tripleapi.OpenAPIRequest, ) (*tripleapi.OpenAPIInfo, error) { return &tripleapi.OpenAPIInfo{Definition: ""}, nil } ``` 主要更新文件: ```text metadata/triple_api/proto/metadata_service_v2.proto metadata/metadata_service.go ``` MetadataInfo conversion 必须在两个方向都保留 `ServiceInfo.Port`。 Provider 侧 conversion: ```go 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: ```go 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 模式下都写入: ```text dubbo.metadata.storage-type = local | remote dubbo.metadata.revision = {revision} dubbo.endpoints = [{"protocol":"tri","port":50051},{"protocol":"dubbo","port":20880}] ``` `dubbo.endpoints` 描述同一应用实例暴露的 protocol ports: ```json [ {"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。 主要文件: ```text registry/service_instance.go registry/servicediscovery/customizer/ ``` ### 10.2 本地 MetadataService Metadata 当 `metadata-type=local` 且本地 MetadataService 已导出时,provider 还应额外写入: ```text 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` 示例: ```json { "protocol": "tri", "port": "20881", "version": "2.0.0", "release": "dubbo-golang-3.x.x" } ``` 主要文件: ```text 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 兼容性说明: ```text Java Dubbo 不会把 dubbo.metadata-service.urls 当成 url-params 后面的 fallback。 如果 dubbo.metadata-service.urls 存在,Java Dubbo 可能选择 SpringCloudMetadataServiceURLBuilder 路径。 ``` 因此,dubbo-go 只有在该值确实是可完整使用的 metadata service URL list,并且目标就是该兼容路径时,才应写入该 key。 推荐格式: ```json [ "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: ```go func ResolveMetadataVersion(cap MetadataServiceExportCapability) string { if cap.V2Exported && cap.V2URL != nil { return constant.MetadataServiceV2Version } if cap.V1Exported && cap.V1URL != nil { return constant.MetadataServiceV1Version } return "" } ``` 主要文件: ```text 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: ```text dubbo.metadata-service.url-params dubbo.metadata-service.urls meta-v ``` 完整 `MetadataInfo` 会发布到 metadata report,consumer 使用 `app + revision` 获取。 --- ## 11. Provider 生命周期 ### 11.1 服务导出 预期流程: ```text ServiceOptions.Export() -> build provider URL -> registry protocol Register(url) -> metadata.AddService(registryId, url) -> serviceNameMapping.Map(url) ``` 主要文件: ```text server/action.go registry/servicediscovery/service_discovery_registry.go metadata/metadata.go ``` ### 11.2 接口到应用 Mapping 应用级服务发现仍然需要 interface-to-application mapping。 这里有两条独立的发现链路: ```text 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 侧注册流程: ```go 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 把生命周期拆成: ```text 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。 重要语义变更: ```text 一个应用进程应只注册一个应用实例。 ``` 这是目标状态,不是第一步必须完成的实现。不要在 metadata fetch、cache key、endpoint expansion 和 service name mapping 都有回归覆盖之前切换到该模型。早期 PR 中,dubbo-go 可以保留当前 per-exported-URL registration 作为兼容桥。 多协议和多端口应通过: ```text 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`。 原因: ```text ServiceInstance 主地址代表应用实例身份。 MetadataService 只是用于获取 metadata 的内部地址。 ``` 如果把 metadata-service port 作为 primary port,instance identity 可能变得不稳定,尤其是 metadata-service port 是随机端口时。 --- ## 12. Consumer 生命周期 当前 consumer 流程应保留并增强: ```text 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 ``` 主要文件: ```text registry/servicediscovery/service_instances_changed_listener_impl.go ``` ### 12.1 Metadata 获取 Metadata fetch 应保留当前基于 storage-type 的判断: ```go if storageType == constant.RemoteMetadataStorageType { metadataInfo, err = metadata.GetMetadataFromMetadataReport(revision, instance) } else { metadataInfo, err = metadata.GetMetadataFromRpc(revision, instance) } ``` 主要文件: ```text 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 顺序: ```text 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。 ``` 必需实现细节: ```text 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: ```text {app}:{revision} ``` `app` 部分必须是 provider application,通常是 `instance.GetServiceName()`,而不是 listener 中保存的 consumer application。 建议 helper: ```go func buildMetadataCacheKey(instance registry.ServiceInstance, revision string) string { return instance.GetServiceName() + ":" + revision } ``` 两层 cache 都应使用这个 key: ```go 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。 原因: ```text 不同应用可能生成相同 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 | 关键兼容性规则: ```text 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. 指标和日志 建议指标: ```text metadata.push.rt metadata.subscribe.rt metadata.rpc.fetch.rt metadata.report.fetch.rt metadata.cache.hit metadata.cache.miss metadata.fetch.error ``` 建议日志: ```text [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 ```text Go provider: metadata-type=local metadata-service-protocol=tri Go consumer: service discovery subscribe fetch MetadataServiceV2 generate provider URL invoke successfully ``` #### Remote Metadata ```text Go provider: metadata-type=remote metadata-report=nacos Go consumer: service discovery subscribe fetch metadata from metadata report invoke successfully ``` 还应覆盖 multi-registry remote metadata: ```text 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 ```text 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 ```text same application, 3 provider instances: port=20000 port=20001 port=20002 consumer 应看到 3 个 provider URLs。 ``` #### Multi-protocol ```text 同一个 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 ```text 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 配置 范围: ```text 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 范围: ```text 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` 生成 范围: ```text 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 范围: ```text 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 范围: ```text 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:收敛应用级实例语义 范围: ```text 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。 正确方向是增强现有架构: ```text MetadataInfo + MetadataService V1/V2 + ServiceInstance metadata + MetadataReport + Consumer metadata fetch by revision ``` 最重要的设计规则是: ```text Service instance metadata 必须描述实际 provider capability。 ``` 具体来说: ```text meta-v=2.0.0 表示 MetadataServiceV2 已实际导出且可访问。 ``` 只要保证这一点,Java Dubbo 和 dubbo-go 的 metadata 互操作就会更可预测。 GitHub link: https://github.com/apache/dubbo-go/discussions/3342#discussioncomment-17038241 ---- This is an automatically sent email for [email protected]. To unsubscribe, please send an email to: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
