上一篇讲 RPC 框架 时,客户端负载均衡有一个隐含前提:手里的实例列表是对的。列表从哪来、多久更新、实例死了多久被摘掉——这就是服务注册与发现。做不好,再炫的负载均衡算法也只是在一堆僵尸 IP 里挑来挑去。
广告微服务里,发现层决定了「一次发布抖动会不会变成全站超时」:竞价、预算、定向几十个服务,每次滚动发布都是对注册表与客户端缓存的压力测试。本篇把发现模式、主流注册中心、健康检查与优雅发布一次讲透,并和 ZooKeeper / etcd 内部机制、负载均衡 串起来。
本文是微服务与 RPC 系列的第 2 篇。 系列顺序:① RPC 框架(开篇);② 服务注册与发现深挖(本篇);③ 服务网格 Service Mesh 深挖;④ API 网关与分布式追踪。
一句话定位:服务发现解决「此刻谁在提供这个服务、是否还活着」——模式选客户端或服务端,一致性选 AP 或 CP,真正决定线上体验的往往是健康检查延迟与优雅下线顺序。
TL;DR
- 客户端发现:调用方从注册中心拉/听实例列表,进程内 LB 直连 Provider(Dubbo、gRPC resolver 典型)。
- 服务端发现:调用方只认 VIP/DNS,由 LB/网关持有后端池并健康探测(K8s Service、云 LB)。
- ZooKeeper / etcd:偏 CP,强一致 + Watch/临时节点或 Lease;写路径延迟更高,适合「错了比慢了更糟」的元数据。
- Consul:服务发现 + DNS/HTTP API,多 DC;健康检查一等公民。
- Nacos:发现与配置一体,可在 AP/CP 间取舍,国内 Java 微服务常用,常带推空保护/权重。
- 健康检查决定摘除 SLA:心跳 TTL、主动 TCP/HTTP/gRPC 探活、临时节点/Lease 过期——越慢,僵尸接流量越久。
- 优雅下线顺序:先摘流量 → 排空 in-flight → 注销 → 停进程;反序必有一段「死进程仍在注册表」。
- 元数据(机房、单元、版本、权重)是路由与灰度的燃料;没有元数据的发现只是「IP 列表」。
- AdTech 血泪:滚动发布未先 deregister,注册缓存 30s 仍指向已杀 Pod → RST/超时尖刺 → 竞价成功率下跌。
Table of contents
Open Table of contents
1. 两种发现形态
两端形态解决同一问题,把复杂放在客户端还是中间层,决定了运维与 SDK 成本。
1.1 客户端发现
流程:Provider 启动向注册中心 Register → Consumer Subscribe/Watch 拿到地址列表 → 本地 LoadBalance 选一个直连。
- 优点:少一跳;可做加权、一致性哈希、单元化/机房亲和等细粒度路由;与 Dubbo Cluster 天然契合。
- 缺点:每个语言都要接入同一套注册协议;客户端缓存与推空保护逻辑易各写各的、行为不一致。
增量推送(Watch)比定时全量拉取更实时,但要处理「推送丢失 / 乱序 / 空列表」;定时 pull 可作为兜底对账。实战上很多事故不是「完全没有发现」,而是半新半旧:一部分客户端已更新、另一部分还握着旧列表,发布窗口里故障率呈波浪——这时要看「缓存版本分位数」,而不是只看注册中心上的实例数。
1.2 服务端发现
流程:Provider 挂到 LB 后端池(或由控制面同步 Endpoints)→ Consumer 只访问稳定名字(DNS / VIP)。
- 优点:调用方极简;健康检查与摘除集中在一处;对多语言友好。
- 缺点:多一跳延迟;复杂流量策略要在 LB/网关表达;调试「到底打到哪台」稍麻烦。
K8s 里 Service + Endpoints + kube-proxy/IPVS/eBPF 本质是平台托管的服务端发现;Ingress/Gateway 再往北伸一层。
1.3 混合与演进
现实里常见混合:南北向走网关/LB(服务端发现),东西向走客户端发现或 Mesh。Service Mesh(下一篇)可以看成:把「客户端发现 + 精细路由」从业务 SDK 搬进 Sidecar,兼得多语言一致与细粒度治理。
2. 注册中心四剑客
没有万能注册中心,只有「一致性 / 可用性 / 运维成本」的三角。
2.1 ZooKeeper
靠 ZAB 保序与多数派;临时节点 + Session 实现「进程死了节点自动删」——早期 Dubbo 的默认搭档。适合偏协调/配置的 CP 场景;超大规模实例频繁变更时,写与 Watch 风暴要小心(详见 ZK/etcd 深挖)。
坑点:Session 超时与 JVM GC 过长叠加时,可能被误判过期 → 闪断摘除 → 抖动。Session 超时要大于可接受 GC pause,并监控 session 重建。
2.2 etcd
Raft + Lease + Watch,Kubernetes 的元数据底座。服务发现可以自建在 etcd 上,也可以间接享受「K8s Endpoints ≈ 平台级发现」。Lease KeepAlive 续不上 → key 过期 → 自然摘除,语义清晰。
注意:etcd 不是为「百万级实例每秒狂抖」设计的海量发现库;海量变化要分片、或把发现数据面交给专用组件,etcd 保关键元数据。
2.3 Consul
把服务注册、健康检查、DNS/HTTP 查询、多数据中心产品化。适合「我想用 DNS 做发现、又要认真健康检查」的团队;与 Nomad/Vault 同生态。健康检查可以是 TTL、TCP、HTTP、gRPC,失败阈值可配——把「摘除 SLA」显式化。
2.4 Nacos
国内微服务常见选择:配置中心 + 服务发现一体;支持按场景选 AP(高可用、最终一致)或 CP。推空保护、权重、元数据路由对广告这种「多机房/多单元」很有用——但推空保护也会掩盖真实空列表故障,必须配监控。
| 维度 | ZK | etcd | Consul | Nacos |
|---|---|---|---|---|
| 一致性 | CP | CP | 偏 CP | AP/CP 可选 |
| 摘除手段 | 临时节点 | Lease | Health Check | 心跳 + 探活 |
| 查询方式 | Watch/API | Watch/API | DNS/HTTP | HTTP/gRPC |
| 典型搭档 | 老 Dubbo | K8s | Hashi 栈 | Spring Cloud Alibaba |
2.5 元数据:发现的第二公民
注册不只是 IP:port。务必要带上:
- 机房 / 可用区 / 单元;
- 版本 / 构建号(灰度、回滚);
- 权重(预热、容量);
- 协议与序列化(Dubbo 多协议并存时);
- 环境标签(prod/staging、压测流量染色)。
没有元数据,Router 就无法做「只把压测流量打到压测组」「同单元优先」。发现系统是带 schema 的小数据库,不是 DNS 的简陋替代。
2.6 反模式速查
- 把注册中心当数据库:存大对象、业务配置洪流——会拖垮 Watch;
- 应用进程内硬编码 IP:发现系统形同虚设,弹性扩缩废掉;
- 无限长客户端缓存无对账:永远缺一次全量校准;
- 用 DNS TTL 扮演健康检查:TTL 分钟级时,死人会继续被解析到。
3. 健康检查与实例生命周期
列表的正确性 = 注册及时 + 续约可靠 + 探活够快 + 下线顺序对。
3.1 三种探活
- 客户端心跳 / TTL:实例定期续约,过期即删。实现简单;进程假死(卡死但仍能发心跳)可能漏检。
- 主动探测:注册中心或独立 checker 打 TCP/HTTP/gRPC Health。能发现「进程在、服务挂」。
- 会话/临时节点:ZK ephemeral、etcd lease——连接或租约断即删,和进程生命周期绑定紧。
摘除 SLA 要算清楚:探测间隔 × 失败次数 + 传播延迟。若合计 30–60s,滚动发布就必须用「先摘后杀」,不能指望探活兜底。
gRPC 的标准 Health Checking 协议值得统一:探活命中真实服务状态,而不是只探操作系统端口开没开。
组合策略往往最好:会话/Lease 负责「进程没了」的快速剔除,主动探测负责「进程在但业务挂了」。单一手段要么漏检,要么误杀。误杀在广告场景的代价是瞬时容量塌方——探活参数要配合容量冗余一起调,不能只追「摘得越快越好」。
探活失败的告警也要分级:单个实例不健康是常态(发布会触发),同服务不健康比例突增才是火灾。把「实例不健康数」和「服务可用实例比例」拆开告,能少很多无效夜班。
3.2 优雅发布标准顺序
- 实例标为 不可用(或从注册中心显式 deregister / 权重打 0);
- 等调用方缓存/Watch 生效,并排空 in-flight(可设宽限期);
- 确认 QPS 归零后再 停进程;
- K8s 场景配合
preStop+terminationGracePeriodSeconds+ readiness 探针。
反模式:直接 kill Pod,注册还在 → 调用方连 RST → 超时重试风暴(接上篇 FailOver 雪崩)。
上线也有对称问题:新实例刚启动就接 100% 权重 → 缓存未热、连接未建,瞬间被打挂。用权重预热(从 0 爬升)或 readiness 挡住未就绪流量。
3.3 缓存与推空保护
客户端必有本地缓存(抗注册中心抖动)。推空保护:短时间收到空列表则继续用旧列表——防「注册中心故障 → 全集群以为没后端」。副作用:真上线失败/真被摘光时,会多撑一会儿僵尸流量,要用「空列表持续 N 秒告警」对冲。
缓存还有时效:过长 → 脏;过短 → 打爆注册中心。配合 Watch 的增量更新 + 周期性全量对账最稳。
3.4 订阅风暴与容量
大规模 Consumer 对同一服务 Watch:注册中心故障恢复瞬间,可能出现风暴式重连与全量拉取。手段包括:分片、连接限流、退避重试、服务端推送批次化。容量规划要把「恢复场景」算进去,不能只算稳态 QPS。
4. AP vs CP:发现场景怎么选
- 选 CP(ZK/etcd):元数据、配置、强一致路由规则、「看错列表后果不可接受」。代价是分区时可能写不进去或选主抖一下。
- 选 AP(Nacos AP、历史 Eureka):超大实例集、频繁上下线、宁可短暂不一致也要注册中心本身高可用。
实务建议:
- 发现数据可用最终一致,但「变更传播延迟」必须可观测、可告警;
- 真正强一致的开关与路由规则另放 CP 存储,别和海量心跳挤在一起;
- 多活时按 机房与区域部署 规划:跨 DC 的 Watch/心跳参数要整体变松或分区自治,避免跨洋强一致幻想。
CAP 提醒:分区来临时,你要么暂时写不进注册(CP),要么暂时读到旧列表(AP)。两边都想要完美,最后两边都不完美——选你更能承受的那一种失败模式。
4.1 多机房发现拓扑
常见三种:
- 全局一套注册中心:简单,但跨城延迟与分区风险高;
- 每机房自治 + 少量全局服务:同城调用吃本地列表,跨城走明确的全局入口;
- 联邦(Consul WAN / 自研同步):机房间同步服务目录,查询仍本地优先。
广告投放多单元时,错误拓扑的典型症状是:明明同城有实例,流量却被哈希到异地——元数据里缺 zone,或 Router 没读元数据。发现系统要和跨区域调度同一张图,不能各画各的。
4.2 安全:注册表也是攻击面
谁能注册 budget-service?若注册中心无鉴权,恶意或误配置实例可以冒名接流量。最低要求:
- 注册/注销需要身份(mTLS、token、或平台侧 service account);
- 命名空间隔离(租户/环境不可互写);
- 审计日志:谁在何时注册了哪个实例。
Mesh 的 SPIFFE 身份(下一篇)把「谁在说话」再往前推一步;但注册中心本身的写权限仍要守住。
5. 小结:发现层的验收问题
交付验收时,用这五个问题自检就够:
- 谁有权注册这个服务名? 无鉴权就等于任何人都能劫持流量。
- 实例从健康变不健康,最长多久被所有调用方摘掉? 写不出秒数,说明没有 SLA。
- 发布会不会先 deregister? 如果靠探活兜底,窗口期必有僵尸命中。
- 客户端收到空列表怎么办? 推空保护有没有告警对冲?
- 元数据够不够路由? 机房、单元、版本、权重缺一样,灰度与就近就会退化成随机。
能清楚回答这五问,注册中心换哪家反而其次;答不上来,换产品也治不好发布抖动。
参考
- Netflix. Eureka at a Glance:经典客户端发现与 AP 思路。
- HashiCorp. Consul Service Discovery:DNS/HTTP 发现与健康检查。
- Nacos. Nacos Architecture:AP/CP、推空保护与配置/发现一体。
- Kubernetes. Endpoints & Readiness:平台级服务端发现与探针语义。
- Google. gRPC Health Checking Protocol:标准化探活约定。