Theory · Microservice · Service Discovery

服务注册与发现

实例时刻在变(发布/扩缩/宕机),硬编码地址必然失效 —— 注册中心是微服务的"活体通讯录"

一句话本质

服务启动时注册自身地址,消费者订阅实例列表;健康检查持续剔除坏实例,watch 推送拓扑变化

三大核心机制

注册与注销 · 健康检查(心跳/lease/主动探测)· 发现与变更推送(watch/长轮询/长连接)

关键取舍

CP(etcd/ZK:强一致,分区时拒绝服务)vs AP(Eureka/Nacos-Distro:可用优先,允许短暂不一致)

这份 deck 回答"实例地址动态变化怎么办":注册与发现的完整闭环。顺序:为什么需要 → 客户端/服务端发现两种模式 → 注册中心功能模型 → 健康检查 → CP/AP 取舍 → etcd 与 Nacos 深入 → 产品对比 → K8s 原生发现 → 优雅上下线 → Go 实践 → QA。etcd 版本信息 2026-07 经 GitHub Releases 核实。

Why Service Discovery Matters

先看现场:地址写死,扩容救不了、宕机躲不开

一个能在脑子里跑起来的事故

时刻发生的事
配置里订单服务调用库存服务,地址写在配置文件:inventory.host = 10.1.2.3:8080
23:40大促,库存服务 CPU 90%,运维紧急扩容到 6 个实例
23:45CPU 还是 90% —— 订单服务仍然只打那一个 IP,新加的 5 个实例一个请求都没接到。
00:10偏偏 10.1.2.3 这台 OOM 宕机 → 订单服务100% 调用失败;另外 5 个实例活得好好的,但没人知道它们在。
00:30改配置 → 滚动重启订单服务 8 个实例 → 20 分钟后才恢复。
关键观察:故障不是"实例不够",而是没人知道实例在哪。扩容之所以白做、宕机之所以致命,都是同一个原因——调用方手里的那份地址是静态的、会过期的,而真实拓扑每分钟都在变。

地址写死的三个死法

① 新实例进不来:扩容/发布产生的新实例不在调用方的配置里 → 扩容无效,流量全压在老实例上。
② 坏实例摘不掉:宕机/假死的实例仍在配置里 → 单点故障直接放大成调用方整体失败。
③ 变更要重启:每加一台机器都要改配置、滚动重启所有调用方 → 恢复以十分钟计,且重启本身还在制造新的不确定性。

注册中心换掉了什么

它换掉的不是"地址"本身,而是地址的来源:从"写在配置文件里的常量"变成一份动态数据——每个服务自己上报"我是谁、我在哪、我还活着",调用方订阅这份列表并在进程内缓存。于是扩容自动生效、宕机自动摘除、发布不用重启别人,恢复时间从 20 分钟压到秒级

本 deck 的路线

先把这套"动态通讯录"的问题模型说清(第 3 页)→ 再看两种发现模式谁去查这份列表(第 4 页)→ 拆开注册、续约、摘除、推送四个动作(第 5-6 页)→ 然后是最难的取舍:CP 还是 AP(第 7 页)→ 落到 etcd / Nacos / K8s 三套实现(第 8-11 页)→ 最后是优雅上下线与 Go 实践。读完你应该能回答:"实例变了,调用方怎么知道"。

动机页:以大促扩容失效 → 单点宕机 → 改配置重启 20 分钟的时间线建立痛感,归纳硬编码地址的三个死法(新实例进不来 / 坏实例摘不掉 / 变更要重启)。指出注册中心换掉的是“地址的来源”(静态常量 → 动态数据 + 进程内缓存),为第 3 页的问题模型与后面 CP/AP 取舍铺垫。

Why

为什么需要注册中心:拓扑永远是动态的

开场那场事故的根因,可以拆成三问:谁在变(发布/扩缩/宕机)、为什么 DNS 不够注册中心的答案是什么

谁在变

发布滚动重启(实例 IP 换)、HPA 自动扩缩、宕机与自愈、Pod 重建 IP 必变——写死 IP/端口不可行

靠 DNS 行不行

DNS 缓存延迟(TTL)、无法及时摘除坏实例、负载均衡策略弱、客户端看不到完整实例列表——只适合粗粒度入口

注册中心的答案

服务把"我活着+我的地址"上报为可查询数据;消费者拿到全量健康实例列表自己做决策,变更秒级推送

注册 = 声明生命周期

  • 注册:实例上线 → 上报 {服务名, IP, 端口, 版本, 权重, 元数据}
  • 保活:定期续约证明自己活着(心跳 / lease 续期)
  • 注销:正常下线主动删除;异常宕机靠租约超时自动清理

发现 = 消费拓扑数据

  • 拉:启动时拉全量健康实例列表(缓存本地)
  • 推:订阅变更(watch/推送),拓扑变化秒级感知
  • 用:客户端负载均衡挑一个实例发起调用(见负载均衡 deck)

注册中心的两个角色要分清:服务发现(谁在哪儿)与配置中心(行为参数是什么)是两类数据、两种访问模式——etcd 两样都能干,Nacos 也把两者打包,但工程上常分开部署(见配置中心 deck)。

先立问题:为什么不能写死地址、为什么 DNS 不够——发布/扩缩/宕机让拓扑动态变化,DNS 有缓存且无法及时摘除坏实例。然后给出注册中心的模型:注册侧声明生命周期(注册/续约/注销),发现侧消费拓扑(拉全量+推变更)。最后提醒注册中心与配置中心是两种数据模式,虽然 etcd/Nacos 都能做,但职责上要分开。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
服务名 / 实例服务名是逻辑名字(如 order),实例是真正干活的一个进程(IP:端口)。一个服务名背后通常有多个实例
注册 / 注销
Register / Deregister
实例上线时把地址写进注册中心叫注册;正常下线时主动删除叫注销(宕机则靠租约过期自动删)
租约 / 心跳
Lease / Heartbeat
注册时带一个有效期(如 15 秒),实例必须不断续期;不再续期 = 租约过期 = 自动摘除——这是宕机也能被发现的原因
存活 vs 就绪
liveness / readiness
存活=进程还在(不行就重启);就绪=能处理请求(不行就摘流量)。一个证明"活着",一个证明"能干活"
拉 / 推
Pull / Watch
=主动查一次全量列表;=订阅后由注册中心在变更时通知。生产组合是"启动时拉全量 + 之后收推送 + 定时拉取兜底"
客户端发现
服务端发现
客户端发现=调用方自己拿列表、自己挑实例直连;服务端发现=调用方只认一个固定地址(VIP/LB),由它转发
CP / AP网络分区时的取舍:CP=宁可拒绝服务也要保证数据一致;AP=宁可数据暂时不一致也要继续可用
优雅下线
Drain
下线不是"直接退出":先摘掉新流量 → 等存量请求处理完 → 再关连接退出,中间那段等待叫 drain
命名空间 / 分组
namespace / group
同一套注册中心内的隔离手段:namespace 分环境(dev/prod),group 分机房或单元——比部署多套更省事

如果这些概念还不熟,先看这三篇

微服务总览 → 为什么一个服务会有很多实例(本 deck 要解决的问题的前提)
负载均衡 → 拿到实例列表之后,怎么从里面挑一个
服务间通信与 RPC → 查到的地址最后由谁去连

本 deck 怎么用这些词

统一直觉:注册中心就是一份"谁还活着、在哪"的表。服务方负责(注册 + 续约),调用方负责(拉全量 + 订阅变更),表里的数据靠租约过期自动删除保持新鲜——所以连宕机都不需要谁来通知。后面所有产品的差别,只在这份表怎么保持一致变更怎么通知

一个提前建立的直觉

这份表是可缓存的数据:调用方把它缓存在自己的进程里,所以注册中心短暂不可用不影响已有调用(只是新实例上线、坏实例摘除会延迟)。理解这一点,后面"为什么发现场景偏 AP""注册中心挂了还能不能调"就都是同一个结论的不同说法。

阅读提示:术语不用背,忘了回来查这一页。真正要记住的只有两件事:租约过期 = 自动摘除,以及实例列表是缓存在调用方进程里的
前置页:术语按“数据(服务名/实例)→ 写入(注册/租约)→ 校验(存活/就绪)→ 读取(拉/推、两种发现)→ 取舍(CP/AP)→ 运维(drain/隔离)”组织。最小心智模型把注册中心归纳为一份表,并提前建立“列表缓存在调用方进程内”这一关键直觉,直接支撑后面 Q1、CP/AP 两页的结论。

Patterns

两种发现模式:客户端发现 vs 服务端发现

上一页说调用方要"读那份表"——那么问题来了:谁去读?读完之后谁来决定连哪个实例

客户端发现与服务端发现两种模式 上半部分客户端发现:消费者从注册中心拉取实例列表后在进程内做负载均衡,直连目标服务。下半部分服务端发现:消费者请求负载均衡器或 VIP,由其查询注册中心后转发请求,客户端无需感知实例列表。两种模式都以注册中心为拓扑数据源。 A · 客户端发现 CLIENT-SIDE DISCOVERY —— gRPC/K8s SDK/Eureka 客户端 消费者服务 进程内 LB(P2C/EWMA) 本地缓存实例列表 注册中心 etcd / Nacos / Consul 实例表 + watch 推送变更 order ① order ② order ③ order 服务 订阅/拉取 直连选定实例 B · 服务端发现 SERVER-SIDE DISCOVERY —— K8s Service / 云 LB / 网关 消费者服务 只认一个稳定地址 LB / VIP / Service kube-proxy · 云负载均衡 · 网关 转发时查询注册中心/endpoint order ① order ② order ③ order 服务 请求 VIP 转发 查 endpoint 列表 取舍:客户端发现少一跳、可做精细化负载均衡,但 SDK 与语言绑定;服务端发现客户端零成本、跨语言,但 LB 是集中组件(K8s 用 iptables/eBPF 把它做进节点内核,规避了单点)。
两种模式是发现领域最重要的一张图。客户端发现:消费者订阅注册中心,进程内 LB 直连——少一跳、能做 P2C/EWMA 这类精细策略,代价是 SDK 与语言绑定。服务端发现:消费者只请求 VIP/LB,由转发层查 endpoint——客户端零成本,代价是 LB 组件与一跳开销;K8s 用 kube-proxy 在节点内做转发规避了 LB 单点。Mesh 的本质就是把客户端发现的 SDK 下沉到 Sidecar。

Model

注册中心功能模型与数据模型

实例记录(数据模型)

Instance {
  service:    "order"          // 服务名
  id:         "10.1.2.3:8080"  // 实例唯一标识
  addr:       "10.1.2.3:8080"  // host:port
  weight:     100              // 负载均衡权重
  version:    "v2.3.1"         // 用于灰度路由
  metadata:   {"region":"gz","az":"gz-1"}
  healthy:    true             // 健康标记
  lease:      TTL=15s          // 生命周期(续约)
}

五个动作(API 面)

  • Register:写入实例记录,绑定 TTL/lease
  • Renew:周期续约(心跳),证明存活
  • Deregister:主动下线,立即摘除
  • Query:按服务名查健康实例列表
  • Subscribe/Watch:注册变更回调/推送

元数据决定玩法

version/metadata.region/weight 是灰度发布、同城双活、流量调权的挂载点——注册中心不只是"通讯录",还是路由决策的数据源(服务网格 deck 的 DestinationRule 同理)。

面试话术:"注册中心解决三件事——服务在哪(寻址)、谁还活着(健康)、拓扑变了怎么通知(推送);实例记录上的元数据支撑灰度与多活路由。"

先讲数据模型:一条实例记录的核心字段——服务名、地址、权重、版本、元数据、健康标记、租约。再讲五个 API 动作:注册、续约、注销、查询、订阅。元数据是加分项:版本/区域/权重字段是灰度路由和多活调权的挂载点,注册中心本质是路由决策的数据源。

Health Check

健康检查:心跳续约 vs 主动探测

模式一:心跳/租约(注册中心侧)

  • 实例定期续约(Nacos 心跳默认 5s;etcd lease TTL 客户端 keepalive)
  • 超时未续约 → 租约过期 → 自动摘除(宕机也能摘,无需人工)
  • 优点:注册中心无脑简单;缺点:只能证明"进程活着",不证明"能处理请求"——DB 挂了照样心跳正常

模式二:主动探测(平台侧)

  • 探活端点:/healthz(存活)与 /readyz(就绪)分离
  • K8s:liveness 探针失败 → 重启容器;readiness 失败 → 从 Service endpoint 摘除
  • 更深:依赖健康(DB 连接池、MQ 连接)纳入 readiness——解决"进程活着但服务不可用"
5s / 15sNacos 临时实例:心跳 5s,15s 未收到标记不健康,30s 删除(默认值)
TTL leaseetcd:实例注册进 lease(如 10s),gRPC keepalive 自动续期,进程崩溃租约到期自动删除
推+拉兜底生产组合拳:SDK 心跳保活 + 平台 readiness 探测 + 客户端调用失败被动摘除(passive health check)

优雅上下线三件套:上线先 readiness 通过再进 LB(预热:JIT/缓存/连接池就绪);下线先摘流量再退出(deregister → 等存量请求 drain → 关连接);滚动发布依赖就绪探针——这是发布不 5xx 的关键,比"发布后重启看看"靠谱得多。

健康检查两种模式:心跳/租约只能证明进程活着,不能证明能处理请求;主动探测(K8s 探针)把"存活"和"就绪"分开,readiness 可以纳入依赖健康。给出 Nacos 5s/15s/30s 与 etcd lease 的默认值增加可信度。优雅上下线三件套是高频追问:上线预热后进流量、下线先摘流量再 drain、滚动发布依赖 readiness。

CAP Trade-off

CP 还是 AP:注册中心的根本分歧

CP 路线:etcd / ZooKeeper / Nacos-持久实例

  • 基于 Raft/ZAB 多数派复制,分区时少数派拒绝服务(保一致性)
  • 适合存强一致元数据:选主、分布式锁、持久任务分配
  • 用于发现的风险:网络分区时客户端拉不到列表 → 无法发起调用;ETCD 3 台集群,2 台可用即服务可用

AP 路线:Eureka / Nacos-临时实例(Distro)

  • 节点间异步复制,分区时各自可读可写(保可用性),接受短暂不一致
  • 发现的现实约束:"拿到一份稍旧的实例列表"远好于"拿不到列表"——旧列表里大多数实例仍然可用
  • 不一致窗口靠客户端容错兜底:调到坏实例 → 重试/熔断摘除(配合容错 deck)

为什么发现场景天然偏 AP

实例列表是可过期数据(eventually consistent 天然合理):每次调用本身就有超时与重试保护;而 CP 系统在分区期间"拒绝服务"会放大故障——注册中心挂了不能成为全站挂的理由。这就是 Eureka 论文式取舍、Nacos 临时实例用 Distro 协议的原因。

但是——K8s 为什么"CP 的 etcd"也能做发现

K8s 把拓扑数据放在 etcd(CP),但 endpoint 订阅走的是 AP 的 watch/informer 缓存(每节点 informer 本地缓存 + 失败重连),调用路径不直接依赖 etcd 强一致读。等价于"强一致存储 + 弱一致读取",兼取两家。

CAP 这页是本 deck 的理论核心。CP 路线(etcd/ZK)分区时拒绝服务,适合选主和锁这类强一致元数据;AP 路线(Eureka/Nacos Distro)分区时继续可用,接受列表短暂过期——因为"稍旧的列表"远好于"没有列表",且调用层本来就有重试熔断兜底。K8s 的混合设计是高级答案:etcd 强一致存储 + informer AP 式缓存读取。

Deep Dive · etcd v3.7

etcd 做服务发现:lease + watch 是全部秘密

机制拆解

  • Raft 复制:写请求走 Leader → 多数派落盘 → 提交;3 节点容 1 故障
  • lease 租约Grant(TTL) 发租约,key 挂在租约上;KeepAlive 周期续约;进程崩溃 → 租约过期 → key 自动删除(实例摘除)
  • watch:从指定 revision 订阅前缀变化,事件推送(PUT/DELETE),断线可从历史 revision 续看(MVCC 保留)
  • MVCC + revision:每次写全局单调递增 revision,天然支持"增量同步 + 断点续传"

Go 客户端最小实现

// 注册(key 挂 lease,随续约存活)
lease := clientv3.NewLease(cli)
gresp, _ := lease.Grant(ctx, 10)
cli.Put(ctx, "services/order/10.1.2.3:8080",
  `{"weight":100,"v":"2.3.1"}`,
  clientv3.WithLease(gresp.ID))
ka, _ := lease.KeepAlive(ctx, gresp.ID)
for range ka {}            // 阻塞续约

// 发现(前缀拉全量 + watch 增量)
resp, _ := cli.Get(ctx, "services/order/",
  clientv3.WithPrefix())
wch := cli.Watch(ctx, "services/order/",
  clientv3.WithPrefix(),
  clientv3.WithRev(resp.Header.Revision+1))
for wresp := range wch {   // PUT/DELETE 事件
  updateLocalEndpoints(wresp)
}
v3.7.1当前稳定版(3.5/3.6/3.7 三线维护,2026-07 Release 核实)
3 节点标准部署:2/3 多数派可写;5 节点容 2 故障但写延迟升高,慎用
watch 语义按 revision 顺序推送,不丢不重;客户端重启用上次 revision 续看

注意:etcd 每次写都要多数派确认,QPS 有上限——不适合高频心跳(KeepAlive 是 O(实例数) 的低频续约,不是 1s 心跳)。海量临时实例场景选 Nacos/Eureka 更合适。

etcd 页讲三个机制加一段代码:lease(租约到期自动删 key = 实例摘除)、watch(按 revision 增量推送,断点续看)、MVCC(全局 revision 是增量同步的基础)。代码是最小可背模板:注册挂 lease + KeepAlive 阻塞续约,发现用前缀 Get + Watch。两个陷阱要主动讲:etcd 写是多数派确认,不能扛高频心跳;海量临时实例应该选 Nacos。版本 v3.7.1 是 2026-07 核实。

Deep Dive · Nacos 3.x

Nacos:一台注册中心,两种一致性协议

临时实例(AP · Distro 协议)——发现的默认形态

  • 客户端 SDK 直连注册:心跳 5s;15s 未续约标记不健康;30s 删除
  • Distro:各节点平等受理写,异步批量复制到其他节点(AP),新增节点靠全量同步收敛
  • 客户端订阅走 UDP/长连接推送 + 定时全量拉取兜底(旧版本 UDP,2.x 起默认 gRPC 长连接)

持久实例(CP · Raft/JRaft 协议)

  • 实例由服务端管理(如 DB/静态配置的服务),不随客户端掉线消失
  • 写走 JRaft 多数派提交,分区时不可写(CP 语义)
  • 健康检查由服务端主动探测(HTTP/TCP),失败只标记不删除

配置中心的推送(同一系统另一面)

1.x 配置变更用长轮询(客户端 30s 挂住,配置 MD5 变化立即返回);2.x 起默认 gRPC 长连接双向推送。细节见配置中心 deck。

3.x 的两个新角色(2025-2026)

Nacos 3.0(2025-04)起内置 MCP Registry(AI 工具服务注册),3.1 增加 A2A 注册——AI Agent/工具服务发现复用同一套注册模型;当前 GA 3.2.x,架构上控制面与数据面分离(console/server 拆分)。

高频追问"Nacos 为什么又 AP 又 CP"——按实例类型选协议:临时实例(微服务进程)生命周期跟客户端走,AP 保证分区可用;持久实例(DB、静态服务)生命周期跟数据走,CP 保证元数据不丢。一套系统按数据语义选一致性,这是 Nacos 最值得背的设计。

Nacos 的核心卖点是双协议:临时实例走 Distro(AP,心跳 5s/15s/30s,异步批量复制),持久实例走 JRaft(CP,服务端主动探测)。订阅推送 2.x 起默认 gRPC 长连接,旧版 UDP。追问点:"为什么双协议"——按实例生命周期语义选一致性。补一句 3.x 的 MCP/A2A Registry 展示知识新鲜度(2025-04 发布 3.0,GA 3.2.x)。

Landscape

注册中心选型对比:etcd / Consul / Nacos / ZooKeeper / Eureka / K8s

产品一致性健康检查推送K-V/特性定位与现状
etcd v3.7 CP(Raft) lease TTL 续约 watch(revision) 强一致 K-V + MVCC + 事务 K8s 元数据存储事实标准;选主/锁首选;海量高频心跳不适合
Consul CP(Raft)+ 局部 AP agent 多种(TCP/HTTP/gRPC/脚本) blocking query 长轮询 + watch K-V + 多数据中心 + 服务网格(Connect) 多 DC 场景强;HashiCorp 许可证变更后社区分化,仍维护
Nacos 3.2.x 临时 AP(Distro)+ 持久 CP(JRaft) 心跳 5s / 服务端探测 gRPC 长连接推送 + 拉兜底 注册 + 配置一体,控制台完善 国内 Java 系标配;3.x 加 MCP/A2A Registry(AI 时代再定位)
ZooKeeper CP(ZAB) 会话(session)+ 临时节点 watcher(一次性,需重设) 树形 znode 老牌协调者;watcher 语义弱、JVM 心跳开销,新项目少选
Eureka 2.x AP(对等复制) 心跳 30s + 自保护 客户端 30s 拉增量 纯注册中心 Netflix 系经典;2.x 停滞,仅存量维护——面试考概念为主
K8s Service etcd CP 存储 + AP 式读取 readiness/liveness 探针 informer watch 缓存 EndpointSlice + DNS(CoreDNS) 云原生默认答案:K8s 内运行时基本不再需要外置注册中心

选型口诀

K8s 内 → Service/DNS;自建 IDC 多语言 → Nacos(要控制台)或 etcd(要强一致/选主);多数据中心 → Consul;纯面试背诵顺序:etcd(Raft/lease/watch)→ Nacos(双协议)→ Consul(多 DC)。

Eureka 自保护机制(考概念)

一分钟内心跳低于阈值(判定可能是注册中心自身网络故障)时,停止摘除过期实例,宁可保留可能已死的列表——AP 极端化的体现:注册中心"怀疑自己"而不是怀疑全部服务。

对比题按三列主线答:一致性(CP/AP)、健康检查方式、推送机制。etcd 强一致但不适合高频心跳;Nacos 双协议注册配置一体;Consul 强在多数据中心;ZK watcher 一次性要重设;Eureka 概念题(自保护)为主;K8s 是云原生默认答案。选型口诀按环境分流:K8s 内用原生,自建选 Nacos/etcd,多 DC 选 Consul。

Kubernetes Native

K8s 原生服务发现:Service / EndpointSlice / DNS

机制链条

  • Service:一组 Pod 的稳定虚拟 IP(ClusterIP)+ 标签选择器——拓扑变化的"稳定门面"
  • EndpointSlice:Service 背后的健康 Pod 列表(分片管理,避免大服务单对象过大)
  • CoreDNSorder.prod.svc.cluster.local 解析到 ClusterIP(A 记录);headless service 直接解析出所有 Pod IP
  • kube-proxy:把 ClusterIP 规则写进节点 iptables/IPVS/eBPF,请求在节点内核层转发到真实 Pod

与"注册中心"的关系

  • Pod 不"主动注册"——控制面根据标签选择器 + 探针被动生成 endpoint:健康检查从应用自报变成平台验证
  • 订阅模型:客户端 SDK(client-go informer)watch EndpointSlice 到本地缓存,然后进程内直连 Pod(客户端发现)
  • 也可以不订阅:直接请求 ClusterIP,由节点转发(服务端发现)
  • 局限:跨集群/混合云发现弱;非 K8s 实例(DB、老机房服务)不在模型内——K8s 之外仍需外置注册中心

ClusterIP 模式

稳定 VIP + 内核转发;四层负载均衡,无应用层路由——配合 Mesh 的 DestinationRule 才有细粒度流量治理

Headless 模式

clusterIP: None,DNS 直接返回全部 Pod IP——有状态服务(StatefulSet)与客户端直连场景(gRPC 长连接池)常用

gRPC 的坑

gRPC 基于 HTTP/2 长连接,ClusterIP 的内核 LB 只在建连时分流——长连接会固化到单 Pod。解法:headless + 客户端 LB,或 Mesh/服务端 LB(详见负载均衡 deck)

K8s 原生发现四件套:Service 稳定 VIP、EndpointSlice 健康列表、CoreDNS 寻址、kube-proxy 内核转发。与外置注册中心的本质区别:注册是平台被动生成(探针验证)而非应用自报心跳。三个模式要点:ClusterIP 四层均衡、headless 返回全部 Pod IP、gRPC 长连接在 ClusterIP 下分流失效的经典坑(引出客户端 LB 需求)。

Graceful Lifecycle

优雅上下线:发布不 5xx 的完整闭环

上线:先就绪,再进流量

  • ① 进程启动 → ② 依赖就绪(DB 连接池/缓存预热)→ ③ /readyz 才返回 200 → ④ 注册中心/endpoint 收录 → ⑤ 流量进入
  • 预热手段:小流量试探、连接池提前建立、本地缓存拉热——避免"第一个请求超时"
  • K8s 的 minReadySeconds + readinessGate 控制放量节奏

下线:先摘流量,再退出

  • ① 收到 SIGTERM → ② 主动注销/置 not-ready(摘除新流量)→ ③ drain:等存量请求处理完(优雅关闭超时如 30s)→ ④ 关闭连接、flush → ⑤ 退出
  • gRPC 服务注意 GracefulStop():停止接新请求,等 in-flight RPC 结束
  • 残留风险:注册中心推送有延迟,客户端仍可能调到——靠调用方重试兜底(配合容错 deck)

为什么"先摘流量"不能只依赖注册中心

实例列表缓存在每个调用方的进程内,推送延迟 + 客户端刷新周期意味着最长几秒窗口内仍有流量进来。所以下线顺序必须先 deregister(新流量停)+ drain(存量跑完),K8s 里对应 preStop 钩子 + terminationGracePeriodSeconds 的配合。

发布验证闭环

readiness 通过 ≠ 功能正确:金丝雀放量 + 核心指标(错误率/延迟 P99)自动观测,异常自动回滚——发布安全的最后防线在可观测与自动决策,不在探针(见可观测 deck)。

优雅上下线是发布不 5xx 的闭环:上线先依赖就绪再 readiness 通过再进流量(预热防首请求超时);下线收到 SIGTERM 后主动注销、drain 存量请求再退出(gRPC 用 GracefulStop)。核心洞察:注册中心推送有延迟,摘流量不能只依赖它,所以顺序是 deregister + drain,K8s 用 preStop 钩子落地。最后接发布验证:金丝雀 + 指标自动回滚。

Go Practice

Go 侧落地:SDK 封装要点与框架内置能力

自研注册 SDK 的六个要点

  • 注册幂等:重试注册用同一实例 ID 覆盖,避免僵尸实例堆积
  • 续约与摘钩:KeepAlive 断线自动重建 lease;进程 panic 时 defer 注销(尽力而为)
  • 发现缓存:实例列表本地内存缓存 + watch 增量更新;watch 断线用 revision 续看,兜底定时全量拉
  • 优雅下线钩子:对接 signal.Notify(SIGTERM)→ deregister → drain
  • 元数据带上:version/region/weight 一次配齐,路由能力留好口
  • 可观测:注册成功/失败、列表大小、watch 延迟都打指标

框架内置(不用自己写)

// Kratos: 注册到 etcd 只需Registrar
import (
  "github.com/go-kratos/kratos/contrib/registry/etcd/v2"
)
r := registry.New(cli)
app := kratos.New(
  kratos.Registrar(r),     // 上线自动注册
  kratos.Discovery(r),     // 下游自动发现
)

// go-zero: 配置即用
Discovery:
  Etcd:
    Hosts: ["etcd:2379"]
    Key:   order.rpc

go-zero/Kratos 都把注册发现做进框架:服务结构体 + Registrar 接口即完成全生命周期;下下游发现自动配合内置负载均衡(P2C/EWMA)。

多环境隔离两板斧:命名空间 namespace(dev/staging/prod 数据隔离)与分组 group / 集群 cluster(同城分区、单元化隔离)。同一套注册中心,用元数据切环境比部署多套更常见;混合云/多机房则推荐每机房一套 + 数据同步(Nacos sync / etcd mirror)。

Go 落地两条路:框架内置(Kratos Registrar、go-zero Discovery 配置即用)和自研 SDK。自研六要点:注册幂等、续约断线重建、发现缓存+watch 续看、优雅下线钩子、元数据配齐、注册可观测。多环境隔离讲 namespace/group 两板斧,多机房讲每机房一套+同步——这两点在架构面常被追问。

Cheat Sheet

一页带走:机制 · 取舍 · 选型 · 上下线

① 一份表 + 四个动作(全部机制)

Register实例上线写入地址,并绑定租约(TTL)
Renew周期续约(心跳)证明活着;停止续约 → 租约过期 → 自动摘除(宕机也能摘)
Deregister正常下线主动删除,立即摘流量
Query + Watch拉全量列表 + 订阅增量变更(定时拉取兜底)

② 两种发现模式

客户端发现 ★调用方自己查列表、进程内 LB 直连:少一跳、可做 P2C/EWMA;代价是 SDK 与语言绑定
服务端发现调用方只认 VIP/LB,由它转发:客户端零成本、跨语言;K8s 用 kube-proxy 在节点内核转发规避单点

③ CP 还是 AP(本 deck 最难的一问)

结论发现场景偏 AP:拿"稍旧的列表"远好于"拿不到列表",调用层本就有超时重试兜底
CP 代表etcd / ZK:Raft 多数派,分区时拒绝服务;适合选主、锁这类强一致元数据
AP 代表Eureka / Nacos 临时实例(Distro):分区时继续可读写,接受短暂不一致
K8s 的解法etcd 强一致存 + informer 弱一致读——兼取两家,是加分答案

④ 产品选型:按环境分流

K8s 内Service / EndpointSlice / DNS 原生即可,通常不需要外置注册中心
自建 IDC 多语言Nacos(要控制台、注册配置一体)或 etcd(要强一致 / 选主)
多数据中心Consul(多 DC 能力最强)
etcd 的坑写走多数派确认,不能当高频心跳用(用 lease KeepAlive,别用 Put 当心跳)
K8s 之外DB、老机房、三方服务不在 K8s 模型内 → 仍需外置注册中心

⑤ 优雅上下线(发布不 5xx)

上线依赖就绪(连接池/缓存预热)→ /readyz 才返回 200 → 进流量
下线SIGTERM → deregister(停新流量)→ drain(存量跑完) → 关连接退出
drain 时长公式≥ 摘除传播延迟 + 最长请求时长(K8s:preStop + terminationGracePeriodSeconds)
为什么不能只靠摘除列表缓存在每个调用方进程内,推送有秒级窗口 → 靠调用方重试兜底

⑥ 三个必答题的一句话答案

· 注册中心挂了还能调吗:能——列表缓存在调用方进程内,影响的只是拓扑变化的感知,不挡在调用路径上。
· 心跳和探测都要吗:都要——心跳证明"活着",就绪证明"能干活"(DB 挂了心跳照样正常)。
· 临时 vs 持久实例:生命周期跟进程走 → 临时(AP,自动摘除);跟数据走 → 持久(CP,只标记不删除)。
速查页:六块按答题顺序排(先机制、再模式、再 CP/AP、再产品选型、再优雅上下线、最后三个必答题的一句话答案)。机制回指第 6 页,模式回指第 5 页,CP/AP 回指第 7 页,选型回指第 9-11 页,上下线回指第 12 页。

Interview QA · 1/2

高频 QA(一):机制与取舍

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回上一页速查表。

Q1 注册中心挂了,服务之间还能调用吗?

本地缓存降级可用

能。实例列表缓存在每个调用方进程内,注册中心短暂不可用时现有拓扑照常调用;影响的是拓扑变化感知(新实例上线、坏实例摘除)。这正是发现系统"数据可缓存"的设计收益——注册中心不挡在调用路径上。

Q2 心跳和主动探测的区别?都要吗?

进程活性服务可用性

心跳证明进程活着(续约 lease),无法发现"活着但不可用"(DB 挂了/线程池打满);主动探测(readiness)验证服务能力,可纳入依赖健康。生产组合:SDK 心跳保活 + 平台就绪探针 + 调用失败被动摘除,三层兜底。

Q3 为什么服务发现多选 AP 而不是 CP?

旧列表可用拒绝服务更糟

实例列表是可过期数据:分区时拿"稍旧的列表"绝大多数实例仍可调,调用层有超时重试兜底;CP 系统分区时拒绝读会导致"注册中心故障放大成全站故障"。强一致需求(选主/锁)才值得 CP。K8s 用"etcd 强一致存 + informer 弱一致读"兼得。

Q4 etcd 的 watch 和 ZooKeeper 的 watcher 区别?

revision 续看一次性触发

etcd watch 按 revision 顺序推送事件、可从任意历史 revision 续看(MVCC 保留)、断线自动重连续传,不丢事件;ZK watcher 是一次性的(触发后需重设),重设间隙的事件要靠重新拉全量补齐,容易踩丢事件坑。

Q5 服务发布瞬间调用方还在打旧实例怎么办?

推送延迟重试兜底

实例列表在各调用方进程内缓存,摘除通知有秒级窗口。标准解法:下线先 deregister + drain(存量跑完再退),调用方对连接拒绝/快速失败做幂等重试到其他实例。两边配合,5xx 才能压到零。

Q6 K8s 里还需要 Nacos/etcd 做注册中心吗?

平台代管边界外仍需要

K8s 内服务:不需要——Service/EndpointSlice/DNS 由平台代管(探针验证健康),客户端 watch endpoint 即可。需要外置的场景:K8s 之外的实例(老机房/DB/三方服务)要进同一套发现;或跨集群/混合云统一寻址。常见架构:K8s 内原生 + 边界网关注册进外置中心。

上半场六题。Q1 的"本地缓存所以照常调用"是理解发现系统的分水岭;Q2 区分进程活性与服务可用性;Q3 AP 取舍要能说出"旧列表好过没列表";Q4 etcd/ZK watch 语义差异体现深度;Q5 推送延迟窗口用 deregister+drain+重试三方配合;Q6 云原生答案:K8s 内不需要,边界外仍需要。

Interview QA · 2/2

高频 QA(二):设计与实战

同样建议先自答。这一页的题都要"结论 + 一句代价/边界"才完整——只答结论容易被追问。

Q7 etcd 能直接当注册中心吗?要注意什么?

lease+watch 足够别放高频心跳

可以:key 前缀 + lease + watch 即是完整方案。注意三点:① 心跳用 lease KeepAlive(低频续约),不要用 Put 当心跳(写放大打爆 Raft);② 实例多时前缀 watch 事件量大,客户端要做快照+增量的防抖合并;③ 3 节点起步,做好 etcd 本身的监控与备份。

Q8 如何设计服务的优雅下线(含 K8s 细节)?

SIGTERMpreStopdrain

流程:SIGTERM → preStop(sleep 数秒 + readiness 失败,让 LB 摘除)→ deregister → gRPC GracefulStop/HTTP server Shutdown 等存量请求(受 terminationGracePeriodSeconds 限制)→ flush 日志/指标 → 退出。关键数字:drain 时长 ≥ 摘除传播延迟 + 最长请求时长。

Q9 海量实例(几万+)时注册中心怎么优化?

分片推送合并分级

① 推送合并:一次发布引发上万事件,按服务聚合增量推送(Nacos Distro 批量同步);② 分片:按服务名哈希分片到多节点(Distro 天然分片),避免单节点全量;③ 快照+增量:新客户端先拉快照再续增量;④ 心跳降频:拉长 TTL 换取服务端容量。

Q10 多机房/异地多活下服务发现怎么做?

机房内闭环元数据路由

原则:发现优先机房内闭环(同机房实例优先),跨机房只是兜底。手段:实例 metadata 打 region/az 标签,客户端按标签过滤+优先级排序;注册中心每机房独立部署、实例数据按需同步;单元化场景按用户分片把"接入-服务-数据"整链路钉在单元内。Nacos 同城双活/Consul 多 DC 都是这套思路。

Q11 注册中心本身的可用性怎么保证?

集群化不影响调用

① 部署:3/5 节点集群 + 跨可用区打散;② 降级设计:调用依赖本地缓存,注册中心全挂也不影响存量调用——把"注册中心故障"从 P0 降级成 P1(新实例无法上线而已);③ 监控:Raft 延迟、磁盘(etcd 对 fsync 敏感)、lease 数量。

Q12 临时实例和持久实例怎么选?

生命周期归属

看生命周期跟着谁走:跟客户端进程走(微服务 Pod,发布即消失)→ 临时实例(AP,自动摘除);跟数据走(DB、静态配置的依赖、第三方)→ 持久实例(CP,服务端探测,失败只标记)。Nacos 的双协议就是为这两种语义并存设计的。

下半场六题偏实战。Q7 强调"lease 续约不是 Put 心跳"的写放大陷阱;Q8 的 drain 公式值得背:摘除传播延迟 + 最长请求时长;Q9 海量实例四板斧:合并推送、分片、快照+增量、降频心跳;Q10 多活原则是机房内闭环+元数据路由;Q11 把注册中心故障降级成 P1 的设计体现;Q12 用生命周期归属一句话分清临时/持久。

Related & References

相关知识点与参考

本领域相关 deck

  • 负载均衡 —— 拿到实例列表之后,怎么挑一个:P2C/EWMA/一致性哈希
  • 服务间通信与 RPC —— 发现解析出的地址,最终由 RPC 框架建立连接与调用
  • 熔断降级容错 —— 调到"列表里的坏实例"时的兜底体系
  • 微服务总览 —— 注册发现在全景架构中的位置
  • 配置中心 —— 同为 etcd/Nacos 承载的另一类数据(配置 vs 拓扑)
  • 服务网格 —— 把客户端发现+负载均衡整体下沉到 Sidecar 的形态

跨领域相关 deck

参考链接(一手来源)

收尾链接:领域内主要是负载均衡(拿到列表后怎么挑)、RPC、容错(调到坏实例的兜底)、配置中心(同平台另一类数据)、Mesh(发现下沉);跨领域对照 Redis 哨兵的拓扑裁决与 Kafka 消费者组的成员发现。参考链接全部官方文档,etcd v3.7.1 与 Nacos 3.x 为 2026-09 核实版本。