服务注册与发现:原理与客户端缓存
单体时代调用方把地址写在配置文件里即可;微服务实例会弹性伸缩、随时上下线,硬编码地址立刻失效。服务注册与发现就是把"实例地址簿"从每个调用方手里收走,交给一个独立组件统一维护。本章讲清原理,第 03 章用 Nacos 落地。
三个角色
注册与发现体系里有三个角色:服务提供者(Provider)启动时向注册中心上报自己的地址、端口、权重等元数据;服务消费者(Consumer)调用前向注册中心查询目标服务的实例列表;注册中心(Registry)负责存储、健康检查与变更通知。一个服务往往同时是别人的提供者、又是别人的消费者。
注册与注销
提供者启动后发注册请求,注册中心保存实例并开始健康检查;正常停机时发注销请求,把实例从列表删除。注意"注销"与"失联"是两回事:优雅停机才走注销,进程被杀或网络隔离只能靠健康检查超时把实例标记为不健康再剔除,因此下线要尽可能走优雅停机流程。
心跳与健康检查
注册中心定期确认实例存活:一类是客户端主动上报心跳(续约),如 Nacos 临时实例;一类是注册中心主动探测(如 Consul 的 HTTP 健康检查)。只要有一段时间没收到心跳,实例就被判定不健康并摘除。心跳间隔、超时时间都是可配置的,调太短会误杀慢实例,调太长会让故障恢复变慢,需按业务取舍。
发现与客户端缓存(本章重点)
消费者拉取实例列表后,真正执行调用前通常要经过三层机制,理解它们才能解释各种"怪现象":
- 全量拉取 + 增量订阅:首次拉全量列表,之后订阅变更通知,收到推送再增量更新本地缓存;
- 本地缓存:消费者内存里始终保留一份服务列表快照,即使注册中心短暂不可用,仍能用缓存里的地址继续调用,这是高可用的关键设计;
- 定时兜底刷新:为防止推送丢失或漏消息,客户端还会周期性地重新拉取或校验缓存,多数注册中心(含 Nacos)都有这一机制。
正因存在缓存,注册中心侧"看到实例已下线"与消费者侧"仍能调到该实例"之间必然有短暂延迟,不可能做到零延迟一致。刚下线的实例地址还会被选中并触发调用失败,这属于预期现象,由调用侧的超时重试兜底(第 06 章)。
变更通知:从"拉"到"推"
消费者要拿到最新实例列表,有两条路径:
- 拉取(Poll):按固定间隔向注册中心查询列表,实现简单,但延迟与流量开销成正比,适合实例变化不频繁的场景;
- 推送通知(Notify):注册中心在实例变更时主动通知已订阅的客户端,客户端收到通知后再拉取增量数据落地。Nacos 2.x 起客户端与 Server 之间维持长连接,由服务端推送变更事件,时效明显优于纯轮询。
生产实现通常是"通知 + 拉取兜底"的组合:推送保证时效,周期拉取兜底防止漏通知。因此"变更后多久全局可见"由推送延迟、客户端处理节奏与健康检查超时叠加决定,端到端收敛时间通常以秒计,设计验收标准时不要按毫秒预期。
健康检查的粒度:进程活着不等于服务可用
心跳与探活大多只能证明"进程还活着",证明不了"业务可用"——数据库连接池耗尽、线程池打满时,进程依然在报心跳。若希望注册中心只把"真正能接流量"的实例暴露给调用方,需要把业务健康探针接入健康检查,例如让检查落到应用的 actuator health 端点,或实现自定义探活逻辑。探针粒度要平衡误判代价:太敏感会把短暂抖动放大成批量摘除,太粗糙则会让坏实例继续留在列表里被调用方命中。
CAP 取舍:AP 还是 CP
注册中心本质是分布式系统,要在一致性与可用性之间做取舍。Eureka 偏向 AP,强调"注册表始终可读",接受短暂读到过期数据;Consul、etcd 偏向 CP,强调数据一致,分区时可能拒绝读写。Nacos 支持临时实例(AP)与持久实例(CP)两种模式,可切换。理解这一点,就能解释为什么客户端普遍要保留本地缓存:注册中心即使抖动,也不能让整个调用链路不可用。
引入 Spring Cloud 后发生了什么
工程引入 spring-cloud-starter-alibaba-nacos-discovery(或 consul-discovery、eureka-client)后,Spring Cloud 的自动装配会注册相关 Bean:应用启动时自动上报实例、定期心跳、维护服务列表缓存,并把列表交给 LoadBalancer 使用。只要应用名(spring.application.name)与注册中心地址配置正确,无需额外注解即可完成注册,这是新版 Spring Cloud 的默认行为。
实例元数据:注册的不只是地址
实例上报给注册中心的除了 IP 与端口,通常还包含版本、权重、地域、自定义标签等元数据。元数据是上层策略的原料:
- 权重:控制流量分配比例(如 Nacos 控制台调权重即可改变分流),调整无需重启调用方;
- 版本/标签:配合网关或负载均衡按标签路由,是灰度发布的基础(本书后半部分有专题);
- 地域/机房信息:支撑就近调用与容灾切换。
另外,注册中心普遍提供命名空间、分组这类逻辑隔离手段,让多环境、多团队共用一个注册中心时互不干扰;Nacos 的相关用法在配置中心章节(第 11 章)中会再次出现。
网络分区时的自我保护
注册中心自身也会面临网络故障:集群发生分区时,如果仍按"心跳超时"大量摘除实例,可能把大量存活节点误判下线,反而放大故障。因此主流注册中心普遍设计了保护机制——分区期间宁可保留部分可能已过期的数据,也不大规模摘除实例,这与客户端本地缓存的思路一脉相承:可用性优先于强一致。理解了自我保护,就能解释"实例明明停了怎么还能查到"这类现象,也能理解为什么下线流程要走优雅停机而不是直接 kill 进程。
排查思路小结
遇到"明明注册了却调不通",先分清从哪个视角看问题,再按顺序排查:
- 注册侧视角:控制台服务列表里实例是否在列、健康状态与最近心跳时间是否正常;
- 消费侧视角:从调用方日志或监控看它实际请求的实例地址,判断是不是还在打旧实例;
- 缓存视角:若注册中心已摘除实例而调用方仍能调到,属预期延迟,重点核对摘除时机与调用方刷新间隔;
- 版本视角:注册中心与客户端大版本差距过大可能导致注册成功但元数据异常,核对官方兼容说明。
小结
注册与发现的核心不是"注册"这个动作,而是围绕实例生命周期的一整套机制:注册、心跳、健康检查、注销,以及消费者侧的缓存与订阅。记住"客户端缓存带来最终一致",后续读第 03、05、06 章时就能串联起完整调用链。