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]

Reply via email to