Skip to content

现在我让你把单体服务拆成多个微服务,这些服务之间如何自动发现彼此?具体原理是什么? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

服务自动发现问题,我会先讲清注册中心的工作机制(注册、心跳、发现、推送四个环节),再落到架构师更关心的选型取舍和发布安全——因为生产上"服务发现"真正的事故大多不在原理,在上下线过程。

机制原理

1. 服务注册:服务启动时把自己的地址(IP:Port + 元数据)写入注册中心(Nacos/Consul/etcd),创建临时节点或带租约的记录。

2. 健康检查:服务定期心跳续约(默认 5 秒);注册中心检测失败或租约过期,将该实例标记为不健康并摘除。

3. 订阅与发现:消费方启动时拉取全量地址列表,并建立长连接订阅变更——Nacos 用 gRPC 推送,etcd/ZK 用 watch 机制监听节点变化,实例上下线秒级通知到所有订阅者。

4. 本地缓存:客户端在内存缓存地址列表(并落盘快照),注册中心全挂时用缓存兜底继续通信——这是可用性的最后防线。

选型的架构取舍

  • CP 还是 AP:ZK/etcd 偏 CP(保证一致,选举期间不可用);Nacos/Eureka 支持 AP(分区时宁可返回旧地址也不拒绝服务)。服务发现场景我倾向 AP——旧地址最多调用失败重试,注册中心不可用会让全集群瘫痪,代价不对等
  • 推送 vs 轮询:长连接推送变更秒级生效,是现代注册中心的标配

架构师的重点提醒:优雅上下线

  • 上线:服务注册后不能立刻接流量——要先预热(JIT 编译、缓存加载),Nacos 可配权重从 0 缓慢调大
  • 下线:先从注册中心摘除 → 等存量流量排空(消费方刷新 + 在途请求完成,约 10 秒)→ 再停进程。直接 kill -9 造成发布瞬间的批量 5xx,是"发布事故"的头号来源,治理它靠发布平台统一收口,不能指望每个开发者手写

🏗 系统架构师 ​

服务发现要解决三件事:注册、发现、感知变化,我以 Nacos/Consul 讲原理,再对比模式。

核心流程:

  1. 注册:服务启动时把 服务名 + IP + 端口 + 权重 + 元数据(版本/机房) 写入注册中心,通常是临时节点(CP 系统如 ZooKeeper 用临时 znode;Nacos 用客户端心跳维持,默认 5s 心跳、15s 未达标记不健康、30s 剔除)
  2. 发现:消费方不直接配置对端 IP,而是订阅服务名。客户端 SDK 通常本地缓存服务列表,调用时从本地列表按负载均衡策略选一个实例,每次调用并不实时查注册中心
  3. 变更感知:推拉结合——Nacos 用 UDP/gRPC 长连接推变更通知,客户端定期拉全量对账;ZooKeeper 用 watcher 事件。双保险防止推送丢失

为什么客户端要本地缓存:注册中心短暂不可用时,消费方靠本地快照继续调用(AP 优先),这是"注册中心挂了服务不挂"的关键设计。

选型对比(必讲权衡):

  • ZooKeeper(CP):强一致,主写选举期间不可用,可能牺牲可用性;临时会话断开即批量剔除,网络抖动会误伤
  • Nacos/Eureka(AP):最终一致,分区时各自可读写,配合健康检查兜底——服务发现场景我选 AP,因为短暂的数据不一致只是个别调用失败,注册中心不可用则全站瘫痪
  • K8s 原生:Service/DNS + Endpoints,云原生环境首选,配合 Istio 做服务网格时发现下沉到 sidecar

细节:实例下线要做优雅停机(先反注册、睡眠几秒排空在途请求再关闭),否则变更传播窗口内仍会有流量打到已死实例。

💻 全栈开发专家 ​

服务发现 = 注册中心(存)+ 心跳(保活)+ 推拉通知(同步)+ 客户端缓存(容错)。以我用得最多的 Nacos 为例。

原理四步

  1. 服务注册:实例启动时向 Nacos 发 POST /nacos/v1/ns/instance(服务名 + IP + 端口 + 集群/权重/元数据),注册表存内存 + 落盘持久化。
  2. 心跳保活:客户端每 5s 发一次心跳;15s 没收到标记不健康,30s 没收到直接剔除。永久实例(DB 存储型)只标不健康不剔除。
  3. 发现与同步:
    • 订阅方每 10s 定时拉取全量服务列表兜底;
    • Nacos 2.x 用 gRPC 长连接推送,服务列表变更毫秒级推到订阅方(1.x 是 UDP 推 + 拉);
    • 集群内节点间用 Distro 协议同步各自负责的服务数据。
  4. 客户端本地缓存:订阅方把服务列表快照落本地磁盘,注册中心全挂时读快照启动,这叫 failover,也是"注册中心挂了服务还能调"的关键。

调用链路

java
// OpenFeign + LoadBalancer:从本地缓存的实例列表里负载均衡选一个
List<ServiceInstance> instances = discoveryClient.getInstances("order-service");

踩过的坑

  • K8s 里用 Nacos 要处理优雅上下线:实例先从注册中心摘除(或 weight=0)再 kill Pod,配合 terminationGracePeriodSeconds,否则滚动发布时打到正在关闭的实例上报 502。我们加了 preStop 钩子先反注册再等待 10s。