Nacos 服务注册表雪崩导致全链路 502:我按这个 checklist 30 分钟把服务拉回来

发布时间:2026/7/17 16:01:01
Nacos 服务注册表雪崩导致全链路 502:我按这个 checklist 30 分钟把服务拉回来 Nacos 服务注册表雪崩导致全链路 502我按这个 checklist 30 分钟把服务拉回来上周五 14:23监控群里开始炸核心交易链路 P99 从 120ms 飙到 8s订单服务出现大量 502。翻开告警Nacos 集群三台节点 CPU 全被打满。我打开链路追踪发现服务调用方拿到的 provider 地址里有接近 40% 已经下线。15 分钟后我按下面的 checklist 把服务拉回来了。本文把这次排查和兜底方案整理出来。1. 现场现象注册中心为什么会“撒谎”Nacos 2.2.3三节点集群MySQL 8 做外置存储。出问题前一周为了做灰度我们临时把某个服务的实例数从 8 扩到 32。没想到这成了压垮注册表的最后一根稻草。最奇怪的点是Nacos 控制台看所有实例都是健康的但调用方本地缓存的地址列表里有很多已经下线的实例。服务 A 调服务 B 时Feign 负载均衡到一台已经挂掉的节点直接返回 502。我用三个命令快速确认了问题范围# 1. 看 Nacos 节点负载forhinnacos-{1..3};dossh$huptime;done# 2. 看 Nacos 服务端线程数jstack$(pgrep-fnacos)|grep-cnacos# 3. 看某客户端本地缓存cat~/.nacos/naming/cache/serviceName.json|jq.hosts[].ip发现三台 Nacos CPU 都接近 100%。服务端DistroFilter线程池排队严重。客户端缓存里有 12 个实例已经 dead但心跳更新时间停在 3 分钟前。这说明注册表本身没有挂但数据同步已经跟不上实例变化。更麻烦的是这种“半死不活”的状态比直接宕机更难发现因为进程还在、端口还在、健康检查还在通过。2. 根因定位为什么是“雪崩”而不是单点故障Nacos 2.x 默认使用长连接gRPC HTTP1.1 双栈。实例扩到 32 后连接数翻了 4 倍但每台 Nacos 的distro同步线程数还是默认配置。更坑的是我们的 K8s 探针配置的是 TCP 探活只检测了 8848 端口没检测 9848 gRPC 端口。结果是 Nacos 进程还在gRPC 连接已经半死不活客户端以为注册中心健康继续把请求往旧实例发。我把根因拆成三层连接放大实例数 × 客户端缓存数 × 双协议长连接数超过 Nacos 默认处理上限。nacos-server的 gRPC 工作线程默认只有 16 个32 个实例加上多客户端一瞬间就把线程池打满。同步瓶颈distro数据同步队列被心跳/注销事件打满旧实例状态推不下去。distro是 Nacos 自研的 AP 协议节点之间靠全量同步和增量同步保持注册表一致。一旦队列堆积一致性就滞后。探测缺失健康探针没覆盖 gRPCKubernetes 没把真正不可用的 Nacos 摘流。8848 端口能响应不代表 9848 长连接通道还能正常推送数据。还有一个容易被忽略的点是客户端本地缓存的默认刷新时间比较长。Spring Cloud Alibaba 里nacos的ServerListRefreshInterval默认是 30 秒在注册表雪崩时30 秒足够把下游服务打挂好几轮。3. 30 分钟止血 checklist下面是我当时按顺序执行的步骤基本每一步都能快速缩小故障范围。建议保存下来注册中心异常时按顺序执行。3.1 立即切客户端缓存1 分钟让调用方不再依赖本地缓存里的“僵尸地址”spring:cloud:nacos:discovery:ephemeral:falsemetadata:preserved:true更直接的办法是在网关层加一层动态刷新curl-XPOST http://gateway:8080/actuator/refresh实际生产中我改的是ribbon.ServerListRefreshInterval到 1 秒让客户端快速刷新实例列表ribbon:ServerListRefreshInterval:10003.2 扩容 Nacos 读节点5 分钟Nacos 集群当时 3 节点我立刻把临时节点扩到 5 节点并把注册/发现流量切到新增节点# nacos/conf/application.properties nacos.core.protocol.distro.sync_retry_delay500 nacos.core.protocol.raft.data_snapshot_interval1000注意临时扩节点不要改集群元数据只作为FOLLOWER加入避免触发 Raft 重新选举。Raft 选举期间写请求会短暂不可用所以扩容时只读不写是最安全的。3.3 杀掉半死 gRPC 连接3 分钟在服务端清掉那些ESTABLISHED但无数据流动的长连接ss-tnstate established( dport :9848 )|\awk{print $4}|sort|uniq-c|sort-rn|head# 对超过 180s 无收发的连接按客户端 IP 批量 rstforipin$(catstale_ips.txt);doss-Kdst$ipdport9848done这个操作有风险但当时我评估后认为客户端有自动重连杀掉 stale 连接可以逼客户端重新拉取实例列表。风险点在于如果批量 rst 太多会触发客户端重连风暴所以要分批执行每批间隔 10 秒。3.4 限制实例心跳频率2 分钟把心跳间隔从默认 5s 调到 10s减少服务端压力spring:cloud:nacos:discovery:heart-beat:10000heart-beat-timeout:30000这个调整不能从根本上解决问题但能在雪崩时给服务端喘息空间。如果集群本身已经处于高负载心跳包太多会进一步拖垮distro同步。3.5 开启 Nacos 过载保护长期# application.properties nacos.naming.data.warmuptrue nacos.naming.healthcheck.threshold0.7这能在服务端 CPU 超过阈值时自动丢弃部分非关键心跳保护核心同步链路。我们后来把阈值调成了 0.8配合 CPU 监控一起用。4. 验证效果502 是怎么降下来的做完前 3 步14:31 链路开始恢复。我用下面三个指标验证# 1. Nacos 服务端线程队列jstack pid|grep-cdistro# 2. 客户端缓存更新延迟curl-shttp://service:8080/actuator/nacos|jq.lastRefTime# 3. 业务侧 502 数量kubectl logs-lappgateway--tail500|grep 502 |wc-l结果502 从每分钟 1200 次降到 0 用了 6 分钟。P99 从 8s 回到 130ms。客户端缓存与 Nacos 控制台实例数差异从 12 个降到 0。验证过程中我发现distro_sync_delay这个指标是最敏感的。只要它还在涨就说明注册表还没完全恢复。等它降到 200ms 以内链路基本就稳定了。5. 事后加固别让它再来一次这次故障让我意识到注册中心不是“配置好就忘”的组件。我们补了 4 件事探针必须覆盖 gRPClivenessProbe:tcpSocket:port:9848initialDelaySeconds:30readinessProbe:tcpSocket:port:9848initialDelaySeconds:10注册中心按读写分离部署3 个节点负责写注册/下线2 个独立节点负责发现读客户端只连读节点降低写节点压力。写节点和读节点之间通过distro同步数据但读请求不再冲击写节点。实例数上限熔断单个服务实例数超过 20 时自动触发告警并切到静态地址列表兜底。这个策略是在发布平台里实现的超过阈值就禁止继续扩容必须先评估注册中心负载。Nacos 监控面板监控nacos_naming_subscriber数量监控distro_sync_delay直方图监控grpc_connect_count与slow_query次数。我们还在 Grafana 里加了告警当distro_sync_delay 1s持续 2 分钟直接 P1 告警到 on-call 手机。写在最后注册中心的问题最烦人的地方在于它看起来好好的但给出去的数据是错的。这次排查让我学到一件事不要只监控 Nacos 进程是否活着要监控“数据新鲜度”。客户端缓存和控制台实例数的差异、distro 同步延迟、长连接半死状态这些才是注册中心真正的健康指标。如果你也在用 Nacos 2.x建议把这个 checklist 存一份。出问题的时候按顺序执行能省不少时间。注册中心雪崩不是不能恢复关键是用对顺序、先止血再查根因别让二次故障把排查拖成小时级。