Alanxtl opened a new issue, #3615:
URL: https://github.com/apache/dubbo-go/issues/3615

   # [Bug] 应用级 Nacos 服务发现可能在 Provider 重启后永久停留在 `No provider`
   
   > Suggested English title: **[Bug] Application-level Nacos discovery can 
remain permanently empty after a transient MetadataService fetch failure during 
provider restart**
   
   ## 1. 摘要
   
   Dubbo-Go `v3.3.2` 使用 Nacos 应用级服务发现时,单实例 Provider 重启通常会产生新的 Metadata 
revision。Consumer 收到新实例事件后,会向 Provider 拉取 `MetadataInfo`。
   
   如果这次 MetadataService RPC 因瞬时网络错误而失败,当前实现会跳过该 Provider,并继续使用空 URL 列表刷新 
Directory。此次失败不会向上返回,也没有针对 Metadata revision 的重试。如果 Nacos 实例列表之后不再变化,Consumer 
可能永久停留在 `No provider available`,直至重启 Consumer 后重新执行初始查询。
   
   这是源码可以确认的应用级服务发现恢复缺陷。Provider 尚未完成启动时的短暂不可用不在本文讨论范围;本文仅关注 Provider 和 
MetadataService 已恢复后,Consumer 
仍可能永久无法恢复的问题。历史现场尚未保留完整框架日志,因此本文不声称已经证明该历史事故必然命中此缺陷。
   
   ## 2. 影响
   
   - 场景:Nacos + application-level service discovery + Triple。
   - Provider:单实例即可触发,不依赖多 Provider 或负载均衡。
   - 触发条件:Provider 重启产生新 revision,并且 Consumer 第一次获取新 revision 元数据时发生瞬时失败。
   - 后果:Provider 和 Nacos 均已恢复正常,但 Consumer 本地 Directory 仍为空;业务调用持续返回 `No 
provider available`。
   - 恢复方式:重启 Consumer,或人为制造新的 Nacos 实例变化事件。
   - 可用性风险:一次瞬时 MetadataService 失败可能扩大为持续到人工干预的服务中断。
   
   ## 3. 环境
   
   | 项目 | 版本或配置 |
   |---|---|
   | Dubbo-Go | `v3.3.2` |
   | Go | 复现环境 `go.mod` 为 `1.26.5` |
   | Nacos SDK | 复现环境解析为 `github.com/nacos-group/nacos-sdk-go/v2 
v2.3.2`;现场二进制待确认 |
   | Nacos Server | 未采集 |
   | Registry group | `DEMO_GROUP`(Mock) |
   | Registry namespace | `demo`(Mock) |
   | Protocol | Triple |
   | Service discovery | Application-level,错误 URL 为 
`service-discovery-registry://...` |
   | Metadata storage type | 现场配置未采集;local 直接 RPC,remote 在 report 失败后回退 RPC |
   | Runtime | Kubernetes |
   | Provider replicas | 1 |
   | Consumer | `demo-consumer`(Mock) |
   | Provider | `demo-provider`(Mock) |
   
   Mock Consumer 与 Mock Provider 均使用 Dubbo-Go `v3.3.2` 和 Nacos SDK 
`v2.3.2`,因此不能用 Consumer 之间的 Dubbo/Nacos 依赖版本差异解释现象。
   
   ## 4. 现象
   
   > 范围说明:Provider 尚未完成启动时短暂返回 `No provider available` 
属于正常单实例空窗,不是本文报告的问题。以下仅讨论 Provider 已恢复后 Consumer 仍无法自行恢复的情况。
   
   ### 4.1 永久故障现象
   
   有使用者报告:
   
   1. Provider 重启完成并已运行较长时间。
   2. Consumer 仍持续返回 `No provider available`。
   3. 不重启 Provider,只重启 Consumer 后立即恢复。
   
   该历史现场目前未能稳定复现,也没有保留下面第 10 节列出的完整框架日志。不过,它与第 6 节的源码缺陷链路一致。
   
   ### 4.2 脱敏后的错误
   
   ```text
   Failed to invoke the method DemoMethod.
   No provider available for the service
   tri://.../?interface=demo.v1.DemoService&group=&version=
   from registry service-discovery-registry://nacos:***@nacos.example:8848
   on the consumer ... using the dubbo version 3.3.2.
   ```
   
   错误由 Consumer 在 Invoker 列表为空时生成,请求尚未进入 Provider 的 `DemoMethod` 方法:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/cluster/cluster/base/cluster_invoker.go#L95-L102>
   
   因此 `DemoMethod` 参数、Handler、Service 及其下游依赖不是这条错误的直接来源。
   
   ## 5. 预期行为与实际行为
   
   ### 预期行为
   
   Provider 已重新注册且 MetadataService 可用后,Consumer 应在不重启进程的情况下自动重新获取元数据、重建 Invoker 
并恢复调用。
   
   一次瞬时 MetadataService 失败可以造成短时不可用,但不应把一次瞬时失败放大为永久空 Directory。
   
   ### 实际行为
   
   Consumer 只在 Nacos 实例事件到达时尝试解析该 revision。Metadata 获取失败后实例被跳过,Directory 
可被空列表覆盖;后续没有实例变化事件时,不会再次尝试解析,Consumer 可以一直返回 `No provider available`。
   
   ## 6. 源码链路
   
   ### 6.1 Provider 下线后,空实例列表会清空 Consumer Directory
   
   Dubbo-Go 创建 Nacos Client 时默认将 `UpdateCacheWhenEmpty` 设置为 `true`:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/remoting/nacos/builder.go#L98-L118>
   
   因此单实例 Provider 下线后,Nacos 空列表会正常进入 
Dubbo-Go。`ServiceInstancesChangedListenerImpl` 最终会向 Directory 调用 
`NotifyAll`,完整快照为空时 Directory 会移除已有 Invoker:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/servicediscovery/service_instances_changed_listener_impl.go#L87-L190>
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/directory/directory.go#L295-L407>
   
   这部分逻辑本身符合预期。
   
   ### 6.2 每次 Provider 重启通常都会产生新 Metadata revision
   
   Provider 导出服务时会把当前启动时间写入服务 URL 的 `timestamp` 参数:
   
   - <https://github.com/apache/dubbo-go/blob/v3.3.2/server/action.go#L336-L360>
   
   `timestamp` 又属于生成 `ServiceInfo` 时的 `IncludeKeys`,会参与 revision 计算:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/metadata/info/metadata_info.go#L47-L60>
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/metadata/info/metadata_info.go#L271-L292>
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/metadata/info/metadata_info.go#L434-L456>
   
   因此,即使接口、group、version、协议和方法均未变化,进程重启通常仍会得到新 revision。Consumer 无法直接复用上一次 
revision 的内存缓存,需要重新获取元数据。
   
   ### 6.3 新 revision 的 Metadata RPC 只有一次尝试
   
   对于 local/default metadata storage,Consumer 直接调用 Provider 的内部 
MetadataService;该函数只进行一次调用,内部超时为 5 秒,没有重试:
   
   - <https://github.com/apache/dubbo-go/blob/v3.3.2/metadata/client.go#L43-L78>
   
   对于 remote metadata storage,metadata report 失败后会回退一次 RPC;如果 report 和 RPC 
都失败,同样没有后续重试:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/servicediscovery/service_instances_changed_listener_impl.go#L270-L345>
   
   ### 6.4 Metadata 获取失败后跳过实例,但事件仍按成功结束
   
   `ServiceInstancesChangedListenerImpl.OnEvent` 获取 Metadata 
失败后直接跳过当前实例。该实例不会进入 `serviceToRevisionServices`,所以无法生成业务 Service URL:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/servicediscovery/service_instances_changed_listener_impl.go#L124-L155>
   
   随后代码会:
   
   1. 用本次构建出的 `newServiceURLs` 覆盖旧 `serviceUrls`;
   2. 对每个业务监听器执行 `NotifyAll`;
   3. 即使所有健康实例都因 Metadata 失败被跳过,仍可能通知空列表;
   4. `OnEvent` 最终返回 `nil`,上层不会感知本次刷新实际失败。
   
   对应源码:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/servicediscovery/service_instances_changed_listener_impl.go#L155-L190>
   
   ### 6.5 没有 Metadata 重试事件源
   
   失败的 revision 没有进入成功缓存,也没有加入重试队列。当前 listener 只有在新的 
`ServiceInstancesChangedEvent` 到达时才会再次执行构建。
   
   Nacos 实例在重新上线后保持稳定时,即使 Nacos 后续再次拉取同一个快照,Nacos SDK 也只在实例内容变化时触发业务 
callback。因此,单纯等待不会保证再次进入 Metadata 获取逻辑。
   
   - 
<https://github.com/nacos-group/nacos-sdk-go/blob/v2.3.2/clients/naming_client/naming_cache/service_info_holder.go#L74-L99>
   - 
<https://github.com/nacos-group/nacos-sdk-go/blob/v2.3.2/clients/naming_client/naming_cache/service_info_holder.go#L143-L163>
   
   最终状态可能是:
   
   ```text
   Nacos:存在健康 Provider
   Provider:业务服务和 MetadataService 均已可用
   Consumer listener:保存了 Nacos instance
   Consumer serviceUrls / Directory:为空
   业务调用:持续 No provider
   ```
   
   重启 Consumer 会重新执行 `GetInstances` 和 Metadata 获取;此时 Provider 已完全可用,所以能够恢复。
   
   ## 7. 第二个相关恢复缺口:应用级 AddListener 失败不重试
   
   应用级服务发现初始化顺序为:
   
   1. `GetInstances` 获取初始快照;
   2. 同步处理快照;
   3. 安装本地 listener;
   4. 异步执行 Nacos `AddListener`。
   
   如果 `AddListener` 失败,当前代码只记录错误,不重试,也不会从 `SubscribeURL` 返回失败:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/servicediscovery/service_discovery_registry.go#L526-L599>
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/nacos/service_discovery.go#L285-L335>
   
   Dubbo-Go `v3.3.2` CHANGELOG 提到的 Nacos 指数退避订阅重试来自 PR #3178,但实现位于接口级 
`registry/nacos/registry.go`:
   
   - 
<https://github.com/apache/dubbo-go/blob/v3.3.2/registry/nacos/registry.go#L191-L235>
   - <https://github.com/apache/dubbo-go/pull/3178>
   
   本问题的错误 URL 是 `service-discovery-registry://...`,走的是应用级服务发现路径,因此没有使用上述接口级退避重试。
   
   这不是第 6 节 Metadata 失败缺陷的必要条件,但同样可能造成 Consumer 长期收不到 Provider 变化。
   
   ## 8. 与已有 Issue/PR 的区别
   
   ### PR #3309
   
   PR #3309 修复多个 Provider/多个 revision 下 Provider 被覆盖以及环境元数据保留问题:
   
   - <https://github.com/apache/dubbo-go/pull/3309>
   
   当前场景是单 Provider,不依赖多个 Provider,因此不是该问题的直接复现。
   
   ### Issue #3468 / PR #3479
   
   该问题是接口级 Nacos 注册中,初始快照没有写入 listener 的差异基线,滚动更新后旧 Provider 可能残留:
   
   - <https://github.com/apache/dubbo-go/issues/3468>
   - <https://github.com/apache/dubbo-go/pull/3479>
   
   该修复已包含在 `v3.3.2`,并且处理的是接口级旧地址残留;本文是应用级服务发现中 Metadata 失败后 Directory 为空,两者不同。
   
   ### 当前 develop 分支
   
   检查日期:`2026-08-08`。
   
   当时官方 `develop` 提交为:
   
   ```text
   0a0ce09631ccb72264f86223fdbbf61021c5a01e
   ```
   
   下面两个核心文件在该提交与本地 `v3.3.2` 的 SHA-256 完全一致,说明截至检查时 develop 尚未修改相关恢复逻辑:
   
   | 文件 | SHA-256 |
   |---|---|
   | `registry/servicediscovery/service_instances_changed_listener_impl.go` | 
`b7e8ac0417a34636db5a88e2c4c1b2104f89ac0c7232e3ea9933a0f6c0766aa7` |
   | `registry/servicediscovery/service_discovery_registry.go` | 
`0242385c4eab96b9095e960c866c453f8f24fa5026f61f853b2a96095ccec298` |
   
   不可变源码链接:
   
   - 
<https://github.com/apache/dubbo-go/blob/0a0ce09631ccb72264f86223fdbbf61021c5a01e/registry/servicediscovery/service_instances_changed_listener_impl.go>
   - 
<https://github.com/apache/dubbo-go/blob/0a0ce09631ccb72264f86223fdbbf61021c5a01e/registry/servicediscovery/service_discovery_registry.go>
   
   ## 9. 建议的确定性复现方式
   
   下面的方式可以把“瞬时 Metadata 失败”与普通 Provider 停机空窗区分开:
   
   1. 启动一个 Consumer 和一个单实例 Provider,确认业务调用正常。
   2. 停止 Provider,等待 Consumer 收到实例列表 `size=0` 并清空 Directory。
   3. 重新启动 Provider,使其以新 timestamp/revision 注册到 Nacos。
   4. 仅在 Consumer 第一次处理新实例事件期间,临时阻断 Consumer 到新 Provider MetadataService/Triple 
端口的网络,保持时间超过 Metadata RPC 超时。
   5. 等 Consumer 出现 `failed to get metadata ... skipping this instance` 后恢复网络。
   6. 不再修改 Nacos 实例、Provider 配置或 revision。
   7. 持续调用业务接口。
   
   预期的当前行为:
   
   ```text
   网络恢复且 Provider 健康后,Consumer 仍持续 No provider;重启 Consumer 后恢复。
   ```
   
   框架期望行为:
   
   ```text
   Consumer 自动重试未解析成功的 revision,并在 MetadataService 可用后重建 Invoker,无需重启。
   ```
   
   也可以在框架测试中把 Metadata 获取抽象为可注入依赖:第一次返回瞬时错误,之后返回成功;不再发送第二个 Nacos 实例事件,验证 
listener 最终仍能通知非空 Provider 列表。
   
   ## 10. 现场日志判定方法
   
   如果再次出现问题,可根据 Consumer 日志快速定性:
   
   | 日志表现 | 判断 |
   |---|---|
   | Provider 上线后出现 `received instance notification event ... size=1`,紧接着出现 
`failed to get metadata ... skipping this instance` | 命中第 6 节缺陷的直接证据 |
   | Provider 上线后始终没有 `received instance notification event ... size=1` | Nacos 
推送、重订阅或应用级 listener 丢失 |
   | 出现 `add instance listener catch error` | 应用级 `AddListener` 安装失败且没有重试 |
   | 已收到 `size=1`、Metadata 成功且已生成业务 URL,但仍然 `No provider` | 检查 
environment/tag/router 是否过滤为空,不属于本文主缺陷 |
   | 只有 `size=0`,之后长期没有新事件 | Consumer 未收到 Provider 重新上线事件 |
   
   建议保留以下关键词前后至少 30 秒的完整日志:
   
   ```text
   [Registry][ServiceDiscovery] received instance notification event
   [Registry][ServiceDiscovery] failed to get metadata from instance
   [Metadata][RPC] could not get the metadata info from remote provider
   [Registry][ServiceDiscovery] add instance listener catch error
   No provider available
   ```
   
   ## 11. 建议修复方向
   
   ### 11.1 为 Metadata revision 增加可取消、去重的重试
   
   建议按照 `(providerApp, registryId, revision, instance)` 对失败任务去重,并采用带抖动的指数退避:
   
   - 重试间隔有上限,避免高频请求 Provider。
   - 同一个 revision 只允许一个重试任务,避免事件风暴产生 goroutine。
   - Provider 被删除、revision 变化或 listener 销毁时取消旧任务。
   - Metadata 成功后,在锁内确认实例仍属于最新 `allInstances`,再根据最新快照重建 URL。
   - 不要在持有 listener 全局互斥锁时等待退避或执行网络 RPC。
   - 长期故障期间持续低频重试,避免有限次数耗尽后再次永久失效。
   
   ### 11.2 区分“注册中心确实为空”与“实例存在但元数据暂时无法解析”
   
   不应把“所有实例都因 Metadata 瞬时失败被跳过”无条件等价为“注册中心返回空 Provider 列表”。建议显式记录 unresolved 
instances,并通过指标或状态暴露:
   
   ```text
   registry_instances > 0
   resolved_service_urls = 0
   metadata_fetch_failures > 0
   ```
   
   不能简单永久保留已下线的旧 Provider URL,否则会把空目录问题变成流量继续打向旧 Pod。更安全的方案是保持 Directory 
不可用但持续重试最新实例,成功后自动恢复。
   
   ### 11.3 复用退避机制修复应用级 AddListener
   
   应用级 `serviceDiscovery.AddListener` 应复用可取消的 
failback/指数退避机制,并在成功前保持明确状态和指标,而不是只记录一次日志。
   
   ## 12. 建议回归用例
   
   1. 新 revision 第一次 Metadata RPC 失败、第二次成功,没有第二个 Nacos 事件,最终恢复非空 Directory。
   2. 重试过程中 Provider 被移除,不得重新加入已删除 Provider。
   3. 重试过程中 revision 再次变化,只允许最新 revision 生效。
   4. 多次重复事件只创建一个相同 key 的重试任务。
   5. 持续失败时请求频率受控,无 goroutine、timer 和缓存泄漏。
   6. listener 销毁后所有重试能够取消。
   7. application-level `AddListener` 第一次失败后能够自动重试成功。
   8. 多 Provider 中单个实例 Metadata 失败时,其他健康 Provider 不受影响。
   9. Provider 真正为空时仍能及时清空 Directory,不回退到已下线旧地址。
   
   ## 13. 当前结论边界
   
   | 结论 | 置信度 |
   |---|---|
   | Provider 业务方法不是 `No provider` 的直接来源 | 源码确认 |
   | v3.3.2 应用级 listener 在 Metadata 获取失败后缺少自动恢复机制 | 源码确认 |
   | 应用级 `AddListener` 失败没有重试 | 源码确认 |
   | 历史“长时间不恢复”事故必然由 Metadata 单次失败导致 | 尚未确认,缺少事故时关键日志 |
   
   本文报告的重点不是把未复现的历史事故强行归因,而是指出:**当前 v3.3.2 以及检查时的 develop 分支中,存在一条可以确定地把瞬时 
MetadataService 故障放大为永久 Consumer 不可用的恢复缺口。**
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to