前一篇立了地基:Dubbo 的三角角色、分层与一次调用路径。Cluster 要挑人,前提是 Directory 里的地址列表是对的——列表从哪来、多久更新、空了怎么办、大块元数据要不要跟地址一起推,就是本篇的主题。
通用发现模型见服务注册与发现深挖;这里聚焦 Dubbo 视角:订阅模型、推空保护、应用级发现与元数据中心,以及广告集群滚动发布时最常见的「僵尸实例」坑。
本文是 Dubbo 深挖系列的第 2 篇。 系列顺序:① 架构开篇;② 注册中心、服务发现与元数据(本篇);③ Cluster 容错与负载均衡;④ 协议与 Triple;⑤ 流量治理。
一句话定位:注册中心负责「此刻谁还活着」;元数据中心负责「接口长什么样」;配置中心负责「策略怎么变」——三者拆开后,地址推送才能又快又稳。
TL;DR
- Dubbo 默认客户端发现:Consumer 订阅后缓存 Provider 列表,热路径直连;注册中心不在每次 RPC 热路径上。
- 接口级发现 vs 应用级发现:接口级按服务维度注册,精细但注册量大;应用级先发现应用实例再查元数据,更适合大规模。
- 推空保护:注册中心异常返回空列表时,禁止用空列表覆盖本地缓存——否则全站「No provider」。
- 本地缓存 / 文件备份:进程重启后即使注册中心短暂不可用,仍可能靠磁盘缓存起步(需接受短暂脏数据窗口)。
- 元数据外置:方法签名等大块数据走元数据中心,缩小 watch 通知体积。
- 优雅上下线顺序:先摘注册 → 排空 in-flight → 停端口/进程;反序必抖。
- AdTech 血泪:滚动杀 Pod 未先 deregister,Consumer 缓存仍指向已死实例 → RST/超时尖刺 → 竞价成功率下跌。
Table of contents
Open Table of contents
1. 订阅与通知:地址如何进到 Directory
经典路径:
- Provider register(含接口、group、version、权重、应用名等);
- Consumer subscribe 同一服务键;
- Registry notify 全量或增量地址列表;
- Consumer 侧 RegistryDirectory(或应用级 Directory)更新 Invoker 列表,供 Cluster 使用。
服务键要一致:group/interface:version(具体格式随版本略有差异)。group 或 version 对不上,表现就是「注册中心明明有服务,Consumer 却 No provider」——排障第一眼查三元组,而不是怀疑注册中心挂了。
2. 接口级发现 vs 应用级发现
| 模式 | 注册什么 | 优点 | 代价 |
|---|---|---|---|
| 接口级(经典) | 每个服务接口一条(或一组)地址映射 | 模型直观、按接口订阅 | 接口多、实例多时注册中心压力大 |
| 应用级(2.7+ 推) | 先按应用/实例发现,再拉服务元数据 | 注册量下降、更贴云原生 | 多一跳元数据获取,心智稍复杂 |
大规模广告中台(成百接口 × 成千实例)更该认真评估应用级发现 + 元数据中心;小集群接口级仍然好懂、好排障。
3. 注册中心选型直觉(Dubbo 场景)
| 组件 | 一致性倾向 | Dubbo 场景直觉 |
|---|---|---|
| ZooKeeper | 偏 CP | 老牌、临时节点会话模型清晰;写路径与运维成本更高 |
| Nacos | 可 AP/CP 取舍 | 国内 Java 栈常见,发现+配置一体,推空/权重友好 |
| Redis 等 | 视用法 | 某些部署图省事;要自己补齐健康与通知语义 |
选型不是信仰问题:看团队现有运维能力、多机房模型、是否已有 Nacos/ZK。机制细节(Watch、Lease、健康检查)仍以通用发现篇与 ZK/etcd 篇为准。
4. 推空保护与本地缓存
这是 Dubbo 生产里最值钱的两道闸:
4.1 推空保护
当注册中心抖动、权限配置错误或订阅条件写错时,可能推下来空列表。若 Consumer 用空列表覆盖内存中的健康列表,结果是:
所有 Invoker 被清空 → 立刻大量失败 → 比「部分实例不健康」致命得多。
正确策略:空推送拒绝覆盖(或可配置严格模式仅在确认「服务已下线」时清空)。排障时若突然全集群 No provider,先问「是不是被空列表刷了」。
4.2 本地文件缓存
Consumer 常把最近一次成功的地址列表落盘。作用:
- 注册中心短暂不可用时,重启后仍能拿旧列表起步;
- 代价是可能打到已下线实例,需要依赖调用失败、健康摘除与后续通知纠正。
广告竞价链路可以接受「偶发打到僵尸再 Failover/快速失败」,往往不能接受「缓存被清空后的全站失败」。
5. 元数据中心与配置中心为什么要拆
早期「全能注册中心」把接口完整描述塞进节点,会导致:
- 通知体积膨胀,滚动发布时 watch 风暴;
- 无关变更频繁唤醒 Consumer;
- 配置与地址耦合,改个超时也像一次「服务变更」。
拆分后:
- Registry:活着的实例与轻量标签(机房、单元、权重);
- Metadata:接口契约(给泛化、网关、文档、校验用);
- Config:超时、重试、路由规则、开关——可运行期覆盖 URL 参数。
6. 实例生命周期与优雅上下线
和通用发现篇同一铁律,落到 Dubbo 操作清单:
上线
- 进程启动、端口 listen、线程池就绪;
- 再 export / register;
- 就绪探针通过后才接流量(K8s 场景对齐 readiness)。
下线
- unregister / 摘流量(注册中心 + 必要时主动通知);
- 等待 Consumer 刷新(或固定 sleep 覆盖缓存 TTL);
- 排空 in-flight(停新请求、等旧请求结束);
- 关端口、停进程。
反序——先杀进程再指望会话超时摘除——等于把健康检查延迟内的流量送进黑洞。下一篇 Cluster 再狠,也救不回「列表里是死人」。
7. 多机房 / 单元化需要的元数据
发现不只是 IP 列表。路由与就近依赖标签,例如:
region/zone/unit(单元)env(prod / staging)version/tag(灰度)weight
没有标签,路由规则就没有燃料;标签乱写,则会「看起来有实例,路由后为空」。
8. 生产反模式
- 用空列表覆盖缓存(关推空保护却不理解后果)。
- 多环境共用一个注册命名空间,靠「大家小心」隔离。
- 只扩 Provider 副本,不关心 Consumer 订阅是否指向正确 group。
- 把元数据当调试信息,从不用于网关/泛化,却仍把巨物塞进注册中心。
- 优雅下线只写在 Wiki,没写进发布流水线。
9. 常见误解 ↔ 正解
| 常见误解 | 正解 |
|---|---|
| 注册中心挂了 RPC 全停 | 热路径直连;有缓存仍可能短期可用 |
| No provider = 注册中心坏了 | 更常见是 group/version/订阅条件不一致或被空列表清空 |
| 发现等于负载均衡 | 发现只提供候选人;LB/路由在 Cluster 层 |
| 元数据中心可有可无 | 大规模与应用级发现下它是减负关键件 |
| 下线等 ZK 会话超时就行 | 对竞价链路太慢,必须主动摘注册 |
10. 速查表
- 订阅 → 通知 → Directory:地址进入调用栈的唯一正路。
- 三元组:group / interface / version 对齐。
- 推空保护 + 本地缓存:防全站清空。
- 三中心:地址 / 元数据 / 配置职责分离。
- 下线顺序:先摘流量,再停进程。
- 标签:机房、单元、版本是路由燃料。
地址列表可靠之后,下一篇讲 Cluster 如何在这份列表上挑人、容错、按规则裁剪——Cluster 容错、负载均衡与路由。
延伸阅读
本系列内部串读:
- Dubbo 深挖(开篇)· 架构全景、SPI 与一次调用全链路
- Dubbo 深挖 · Cluster 容错、负载均衡与路由
- Dubbo 深挖 · 协议栈与 Triple / 序列化
- Dubbo 深挖 · 流量治理、Filter 链与优雅上下线
相关:
一手资料:
- Apache Dubbo. Registry:注册中心与服务发现说明。
- Apache Dubbo. Metadata:元数据中心职责与使用。
- Nacos. Service Discovery:常见配套注册中心的发现模型。