主题
服务注册与发现
一句话:注册中心维护「服务名 → 可用实例列表」的映射,让调用方在实例扩缩容、重启、故障时不必改配置;代价是调用方要接受一份可能短暂的旧视图。
适用版本:Nacos 2.x(客户端 2.x)、Spring Cloud Alibaba 2023.0.x。Eureka/Consul 的差异单列。
Nacos 自身的架构、集群一致性、容量规划与 AI Registry 详见 Nacos 专栏;本文只保留 Spring Cloud 使用视角。
为什么需要
没有注册中心时,调用方只能把 ip:port 写在配置里。实例换一台机器、扩一个副本、缩一个副本,都要改配置并重启调用方;故障实例也不会自动从列表里消失,流量继续打过去。
注册中心解决三件事:
- 提供方启动时登记自己。
- 注册中心判断它是否还活着。
- 消费方订阅列表,并在变更时收到通知。
核心机制
一次注册、发现、调用的完整流程
四方按时间顺序交互:提供方登记、注册中心判活、消费方订阅缓存、调用时直连。注意最后一条消息的箭头指向提供方自己,不指向 Nacos。
关键点:调用不经过注册中心。注册中心只负责「名单」,数据面是调用方直连;所以注册中心挂了,已启动的调用方仍能用本地缓存继续调用,只是看不到新实例。
Nacos 2.x 的推送为什么和 1.x 不一样
Nacos 1.x 的命名服务用 UDP 推送变更;UDP 不可靠,丢包后要靠客户端定时拉取兜底。Nacos 2.x 客户端与服务端建立 gRPC 长连接,变更通过这条连接推送:
- 默认端口是主端口 +1000/+1001:8848 → 9848(gRPC)、9849(gRPC TLS/集群)。
- 长连接同时承载注册、订阅、健康检查,不再依赖 UDP。
- 防火墙只放通 8848 是常见事故:HTTP API 能查,客户端却注册不上或收不到推送。
写「Nacos 实时 UDP 推送」时,要注明是 1.x;2.x 以 gRPC 长连接为准。
AP 与 CP:不是全局开关
Nacos 用两套协议处理两类数据:
| 实例类型 | 一致性协议 | 语义 | 适用 |
|---|---|---|---|
临时实例 ephemeral=true(默认) | Distro | AP:每个节点可写,异步复制,最终一致 | 普通微服务 |
永久实例 ephemeral=false | Raft | CP:多数派确认,强一致 | 不能丢的静态服务、数据库代理 |
Distro 的冲突解决是带时间戳的后写覆盖(Last-Write-Wins);所以 AP 模式下,某个节点可能短暂读到旧列表。对大多数无状态服务,这比「注册中心不可写」更容易接受。
健康检查的默认值与代价
Nacos 临时实例的默认节奏(2.x,以官方文档为准):
- 客户端每 5s 发一次心跳。
- 15s 收不到心跳 → 标记不健康。
- 30s 收不到 → 从列表剔除。
这三个值是在「误判」和「发现速度」之间取折中:心跳太密,注册中心 QPS 随实例数线性上涨;太稀,故障实例会在列表里停留更久。不要在没有压测的情况下改成 1s/3s/6s。
Nacos 还有健康保护阈值 protectThreshold(默认 0):当健康实例比例低于阈值时,返回全部实例(包括不健康的),避免「健康实例太少,流量把剩下的也打挂」。它不是 Eureka 的自我保护,但作用类似;说「Nacos 没有自我保护」是不准确的。
消费方的本地缓存
Spring Cloud LoadBalancer 的实例缓存默认 35s(spring.cloud.loadbalancer.cache.ttl)。这意味着:
- 新实例上线后,最坏 35s 才被部分调用方看到。
- 实例下线后,最坏 35s 仍可能收到流量。
- Nacos 推送会触发刷新,但缓存本身仍有 TTL。
所以「注册成功」不等于「立刻全量可调用」;发布和下线都要留排干窗口。
Eureka 与 Consul 的定位
| 维度 | Nacos 2.x | Eureka 1.x | Consul |
|---|---|---|---|
| CAP | 临时实例 AP、永久实例 CP | AP + 自我保护 | CP(Raft) |
| 配置中心 | 内置 | 无 | KV |
| 推送 | gRPC 长连接 | 客户端轮询 | DNS/HTTP + gossip |
| 多数据中心 | 需额外方案 | 无原生 | 原生 WAN Join |
| 状态 | 活跃 | 维护模式,Eureka 2.0 已停止开发 | 活跃 |
Eureka 是「维护模式/社区维护」,不是「已停止」;老系统可以继续用,但新项目默认选 Nacos 或 Consul。
实战场景
一、只放通 8848:注册成功,但调用方永远慢半拍
现象:安全组收敛后,order-service 扩容的实例在 Nacos 控制台能看到,但调用方要等约 35 秒才把流量打过去;滚动发布时新实例的预热时间被完全浪费。手工 curl instance/list 一切正常,看不出问题。
排查:HTTP API 与长连接是两条通道,能查不代表能推。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| gRPC 端口连通 | 在调用方机器 nc -vz <nacos> 9848;看客户端日志有无 GrpcSdkClient 连接失败 | 连不上 → 变更推送走不到客户端 |
| HTTP API 可用 | curl .../v1/ns/instance/list 能返回 | 能返回但推送不通,说明只有 8848 放通 |
| 列表更新方式 | 抓包看是否有周期性 subscribe 请求兜底 | 全靠轮询兜底 → 视图延迟等于 TTL |
| 集群侧 | 9849 在集群节点间是否互通 | 节点间不通 → Distro 同步异常,不同节点视图不一致 |
| 版本 | 客户端是 1.x 还是 2.x(nacos-client 版本) | 1.x 才有 UDP 推送语义,2.x 以 gRPC 为准 |
根因是网络策略:8848 放通、9848 被挡。客户端退回定时拉取,最坏要等一个 LoadBalancer 缓存周期(默认 35s)才能看到新实例。
对症做法:把端口清单当成上线前置条件,而不是等故障再补。
text
Nacos 2.x 必须放通的端口
8848 客户端 HTTP(OpenAPI、控制台、配置长轮询)
9848 客户端 gRPC = 主端口 +1000(注册、订阅、心跳、配置推送)
9849 服务端 gRPC = 主端口 +1001(集群节点间 Distro/Raft 通信)
3306 外置 MySQL(集群模式)bash
# 启动自检:连不上就直接失败,而不是静默降级
for p in 8848 9848; do nc -z -w2 "$NACOS_HOST" "$p" || { echo "nacos port $p unreachable"; exit 1; }; done客户端侧还要确认 spring.cloud.nacos.discovery.server-addr 指向的是 8848(不是 9848),端口偏移由客户端自动计算。
复盘要点:这类故障的隐蔽之处在于“所有主动查询都正常”,只有推送不通。判据应做成发布前门禁:客户端日志里出现 gRPC channel 连接失败即不允许启动;把端口清单写进部署模板与网络基线,避免每次新环境重踩。
二、注册上来的是容器内网 IP,跨网段调不通
现象:迁移到 K8s 后,同一集群内的调用正常;网关部署在另一个网段(或 ECS 侧),调用 order-service 时连接超时。instance/list 里能看到实例,IP 是 10.244.x.x 这类 Pod CIDR 地址。
排查:注册中心只保证“登记的是客户端自己看到的地址”,不保证这个地址对所有人可达。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 注册的 IP | curl .../instance/list 里的 ip 字段 | Pod IP 且调用方路由不到 → 跨网段必失败 |
| 本机网卡选择 | 客户端日志里 NetUtils 选中的网卡名 | 多网卡机器可能选到 docker0/flannel.1 |
| 调用方视角 | 在调用方机器 nc -vz <ip> <port> | 不通即路由问题,与应用无关 |
| Service 形态 | 是否应该注册 NodePort/LoadBalancer 而非 Pod IP | 期望与实际的注册模型不一致 |
| 端口映射 | spring.cloud.nacos.discovery.port 与容器实际暴露端口 | 注册 8080、映射 30080 → 连不上 |
对症做法:
yaml
# 1. 显式声明对外可达地址(跨网段互通时的直接修法)
spring:
cloud:
nacos:
discovery:
ip: ${ANNOUNCED_IP} # 由 downward API 注入宿主机/入口 IP
port: ${ANNOUNCED_PORT} # NodePort / LB 端口
# 2. 多网卡机器指定网卡,避免选到容器网桥
network-interface: eth0
# 3. K8s 原生场景优先不注册 Pod IP 到跨域注册中心:
# 用 Service + 外部 LB 承接跨域流量,注册中心只服务本域yaml
# downward API 注入宿主机 IP 的片段
env:
- name: ANNOUNCED_IP
valueFrom: { fieldRef: { fieldPath: status.hostIP } }
- name: ANNOUNCED_PORT
value: "30080"跨数据中心本身不是 Nacos 的原生能力:多 DC 场景要评估 Consul 的 WAN Join 或自建同步,不要指望注册中心替你做路由。
复盘要点:地址正确性要有断言:服务启动后由自检脚本调用一次 instance/list,比对返回 IP 是否在预期网段清单里;不符就让 readiness 失败。多网卡、容器网络、跨域调用三个变量同时出现时,靠默认选卡必然出错。
三、下线后仍有流量:两层延迟叠加成 65 秒
现象:每次发布约 30~60 秒内出现零星 5xx,客户端报 Connection refused;进程日志显示异常时间点正好在服务停止之后。低峰期也有,量小但一直存在。
排查:从“实例被摘掉”到“调用方不再打过来”中间有两段独立延迟。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 是否主动注销 | 服务停止日志里有没有 deregister 记录 | 只有 kill → 全靠 30s 超时剔除 |
| 调用方缓存 | spring.cloud.loadbalancer.cache.ttl(默认 35s) | 与 5xx 时间窗长度一致即可确认 |
| 关闭顺序 | 进程退出与端口关闭的相对时间 | 端口先关、注销后发 → 中间窗口全是失败 |
| 保护阈值 | protectThreshold 为 0 时健康实例全挂会返回空列表 | 空列表会让调用方直接失败,而不是降级 |
| 客户端重试 | 是否有重试与实例切换 | 无重试则任何一次命中都是用户可见错误 |
对症做法:
yaml
# 1. 优雅下线:先注销,再等一个缓存周期,最后关端口
management:
endpoint:
shutdown:
enabled: false # 不暴露 /actuator/shutdown,用自定义下线接口更安全java
// 2. 自定义下线钩子:摘除 → 排干 → 退出
@PreDestroy
void gracefulOffline() throws Exception {
namingService.deregister(instance); // ① 主动注销,不等心跳超时
Thread.sleep(40_000); // ② 覆盖 35s 缓存 TTL(经验值,需校准)
serverConnector.stopAccepting(); // ③ 停止接收新连接
inFlight.awaitCompletion(20, SECONDS); // ④ 等在途请求排干
}yaml
# 3. K8s 侧用 preStop 承担等待,探针同时置为失败
lifecycle:
preStop:
exec: { command: ["/bin/sh", "-c", "curl -s -XPOST localhost:8080/offline && sleep 40"] }
terminationGracePeriodSeconds: 90properties
# 4. 缓存可以更短,但不要用 0:全量拉取会把压力推给注册中心
spring.cloud.loadbalancer.cache.ttl=15s对“不能调错实例”的支付回调、交易路由,用永久实例(CP)或在业务层做幂等校验,而不是缩短心跳周期。protectThreshold 要在健康实例比例经常骤降的服务上评估:设 0 时全挂会返回空列表,调用方没有任何可试的实例。
复盘要点:这段窗口应该被度量成发布指标——发布窗口内客户端 5xx 率与“进程退出到最后一个请求结束”的时间差。判据是发布门禁(例如窗口内 5xx < 0.1%,经验值,需校准),而不是文档里写“等待 35 秒”。完整停机顺序与探针配置见 优雅上下线与启动检查。
最小上手
起 Nacos 2.x,先用 OpenAPI 注册一个临时实例,再查询:
bash
docker run -d --name nacos -p 8848:8848 -p 9848:9848 \
-e MODE=standalone nacos/nacos-server:v2.3.2
# 注册临时实例(ephemeral=true 需要客户端持续心跳,否则 30s 后被剔除)
curl -X POST "http://127.0.0.1:8848/nacos/v1/ns/instance" \
-d "serviceName=order-service" -d "ip=127.0.0.1" -d "port=8080" -d "ephemeral=true"
# 查询实例列表
curl -s "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=order-service"Spring 侧只需要 discovery 依赖和一行地址:
yaml
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: public启动后调用 curl 查询接口,hosts 中出现本机地址即成功;同时确认 9848 端口可连,否则看客户端日志里的 gRPC 连接错误。
生产动作
- 集群与存储:3 节点起步,外置 MySQL;
MODE=cluster,不要用 standalone 上生产。 - 命名空间与分组:
namespace隔离环境,group隔离业务线;不要用同一个 namespace 承载 dev/prod。 - 临时 vs 永久:普通服务用临时实例;没有心跳能力、必须保证列表存在的服务用永久实例,并接受 CP 的可用性代价。
- 下线排干:先调下线接口或
preShutdown钩子摘除实例,等超过 35s 再停进程。 - 保护阈值:健康实例比例经常骤降的服务,评估
protectThreshold;设为 0 时健康实例全挂会返回空列表。 - 监控:注册实例数、心跳超时数、gRPC 连接数、CP 服务 leader 切换次数;实例数突降通常先于调用失败出现。
排障路径:
- 调用方 404/无实例 → 查
instance/list是否有实例 → 没有则查提供方注册日志和 9848 连通性。 - 有实例但调用失败 → 查实例 IP 是否是容器内网 IP、端口是否映射 → 修
spring.cloud.nacos.discovery.ip/port。 - 实例下线后仍有流量 → 查 LoadBalancer 缓存 TTL 与排干时间 → 延长等待或主动摘除。
边界
- 注册中心不是配置数据库:不要用它存业务配置;高频写入会放大 Distro 同步压力。
- AP 会读到旧列表:对「不能调错实例」的场景(支付回调、交易路由),要么用永久实例/CP,要么在业务层做幂等和校验。
- CP 牺牲可用性:少数派节点不可写,恢复期间注册/发现可能失败;不要为了「一致」把所有服务切 CP。
- 跨数据中心:Nacos 没有原生多 DC 复制;多机房优先考虑 Consul 或自建同步方案。
- Eureka 不是不能用:维护模式意味着不再加新特性,已有系统可以保留,但升级路径要提前规划。
讲清楚
服务注册与发现解决的是「实例地址会变」:提供方登记,注册中心探活,消费方订阅并在本地缓存,调用时直连实例,不经过注册中心。Nacos 2.x 用 gRPC 长连接推送,1.x 才是 UDP;临时实例走 AP 的 Distro 协议,允许短暂旧视图,永久实例走 CP 的 Raft,强一致但牺牲可用性。心跳 5s、15s 不健康、30s 剔除是默认折中,消费方还有约 35s 的本地缓存,所以注册成功不等于立刻全量可见。它的边界是:注册中心只解决「找谁」,不解决「调用可靠」;超时、重试、熔断要在调用方做。