跳到正文

服务注册与发现 ​

一句话:注册中心维护「服务名 → 可用实例列表」的映射,让调用方在实例扩缩容、重启、故障时不必改配置;代价是调用方要接受一份可能短暂的旧视图。

适用版本:Nacos 2.x(客户端 2.x)、Spring Cloud Alibaba 2023.0.x。Eureka/Consul 的差异单列。

Nacos 自身的架构、集群一致性、容量规划与 AI Registry 详见 Nacos 专栏;本文只保留 Spring Cloud 使用视角。

为什么需要 ​

没有注册中心时,调用方只能把 ip:port 写在配置里。实例换一台机器、扩一个副本、缩一个副本,都要改配置并重启调用方;故障实例也不会自动从列表里消失,流量继续打过去。

注册中心解决三件事:

  1. 提供方启动时登记自己。
  2. 注册中心判断它是否还活着。
  3. 消费方订阅列表,并在变更时收到通知。

核心机制 ​

一次注册、发现、调用的完整流程 ​

四方按时间顺序交互:提供方登记、注册中心判活、消费方订阅缓存、调用时直连。注意最后一条消息的箭头指向提供方自己,不指向 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(默认)DistroAP:每个节点可写,异步复制,最终一致普通微服务
永久实例 ephemeral=falseRaftCP:多数派确认,强一致不能丢的静态服务、数据库代理

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.xEureka 1.xConsul
CAP临时实例 AP、永久实例 CPAP + 自我保护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 地址。

排查:注册中心只保证“登记的是客户端自己看到的地址”,不保证这个地址对所有人可达。

定位层取证据判定
注册的 IPcurl .../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: 90
properties
# 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 的本地缓存,所以注册成功不等于立刻全量可见。它的边界是:注册中心只解决「找谁」,不解决「调用可靠」;超时、重试、熔断要在调用方做。

把一门技术讲透到既懂也会用