Nacos 注册中心推送延迟飙到 5 秒,我用一致性哈希把服务发现耗时压回 50ms Nacos 注册中心推送延迟飙到 5 秒我用一致性哈希把服务发现耗时压回 50ms说实话我一开始真没把服务注册发现当成性能瓶颈。直到上周监控告警突然炸了——Nacos 推送延迟飙到 5 秒整个微服务集群的服务发现跟瘫痪了一样。这篇文章复盘一下排查过程核心方案是一致性哈希负载均衡最后把耗时压回 50ms 左右。背景问题是怎么暴露的我们的微服务架构用了 Nacos 2.x 作为注册中心和配置中心大概 200 多个服务实例日调用量几千万级。平时服务发现耗时稳定在 20-30ms上周开始突然飙升。监控面板上看到的症状服务发现耗时 P99 从 30ms 涨到 5sNacos 服务端 CPU 飙到 90%客户端频繁报com.alibaba.nacos.api.exception.NacosException: failed to req API超时部分服务实例心跳注册失败被误判为下线最坑的是这个问题不是慢慢积累的而是某天下午突然爆发的跟代码发布没关系。我的第一反应是是不是 Nacos 集群本身出了问题排查过程到底是谁在拖后腿第一步确认服务端负载先登录 Nacos 控制台看了下集群状态。三个节点Leader 节点的 CPU 和内存都爆表了另外两个 Follower 相对正常。这让我怀疑是不是请求都打到 Leader 上了。查了下 Nacos 的架构Nacos 2.x 的服务发现走的是长连接gRPC但配置推送和心跳注册还是需要走 HTTP。问题可能出在服务注册请求的负载均衡上。看了下客户端配置发现用的是默认的 Nacos 客户端负载均衡策略轮询RoundRobin。// 客户端配置问题版本spring.cloud.nacos.discovery.server-addr192.168.1.10:8848,192.168.1.11:8848,192.168.1.12:8848客户端默认会把请求轮询分发到三个节点。但 Nacos 2.x 的服务注册有个特性写请求注册、心跳、下线必须走 Leader 节点读请求服务发现、订阅可以走任意节点。轮询策略的问题在于它把写请求均匀分到三个节点但两个 Follower 节点收到写请求后还要转发给 Leader这就造成了大量的内部转发开销。第二步抓包验证用 tcpdump 在 Leader 节点上抓了一段时间的包发现大量的 HTTP POST /nacos/v1/ns/instance 请求来自两个 Follower 节点的 IP。这证实了上面的猜测客户端轮询把写请求发到 FollowerFollower 再转发给 LeaderLeader 的负载被放大了 3 倍。# 抓包命令tcpdump-ieth0host192.168.1.10 and port8848-wnacos.pcap# 用 wireshark 分析过滤 http.request.method POST更坑的是Nacos 的 Follower 转发写请求是同步阻塞的如果 Leader 处理不过来Follower 也会被拖住进而影响读请求。这就解释了为什么整个集群的服务发现都变慢了。第三步定位根因根因其实有两层第一层负载均衡策略不对。轮询不适合 Nacos 的读写分离架构写请求应该尽量直接打到 Leader减少转发。第二层服务实例数量增长后心跳上报频率成了瓶颈。默认心跳间隔 5 秒200 个实例 × 心跳 服务注册 配置推送QPS 虽然不高但每个请求都涉及 Raft 状态机同步Leader 的 CPU 被打满了。解决方案一致性哈希 定向路由方案一直接指定 Leader 节点不推荐最简单的办法是只配置 Leader 节点的地址让客户端直接连接 Leader。但这有两个问题Leader 故障时需要手动切换高可用性差所有压力集中在一个节点Leader 会更快被打爆这个方案我只在测试环境验证了一下确认确实能消除转发开销但生产环境肯定不能这么干。方案二客户端自定义负载均衡最终方案我们用的是 Spring Cloud Alibaba客户端底层用的是 Nacos 的ServerListManager管理服务端节点列表。我自定义了一个负载均衡策略核心思路是一致性哈希环把服务实例名映射到哈希环上同一个服务的请求总是路由到同一个节点读写分离写请求注册、心跳、下线优先路由到 Leader读请求发现、订阅通过一致性哈希分散到所有节点动态感知监听 Nacos 集群状态变化Leader 切换时自动重新路由ComponentpublicclassNacosConsistentHashLoadBalancerimplementsLoadBalancer{privatefinalTreeMapLong,StringhashRingnewTreeMap();privatefinalintVIRTUAL_NODES150;publicNacosConsistentHashLoadBalancer(ListStringservers){for(Stringserver:servers){for(inti0;iVIRTUAL_NODES;i){longhashhash(server#i);hashRing.put(hash,server);}}}publicStringselectServer(StringserviceName,booleanisWriteRequest){if(isWriteRequest){// 写请求直接路由到 Leader通过 /nacos/v1/core/cluster/nodes 动态获取returngetCurrentLeader();}// 读请求用一致性哈希longhashhash(serviceName);Map.EntryLong,StringentryhashRing.ceilingEntry(hash);returnentry!null?entry.getValue():hashRing.firstEntry().getValue();}privatelonghash(Stringkey){// FNV-1a hashfinallongFNV_64_INIT0xcbf29ce484222325L;finallongFNV_64_PRIME0x100000001b3L;longhashFNV_64_INIT;for(byteb:key.getBytes()){hash^(b0xff);hash*FNV_64_PRIME;}returnhash;}}关键配置spring:cloud:nacos:discovery:server-addr:192.168.1.10:8848,192.168.1.11:8848,192.168.1.12:8848load-balancer-type:consistent-hash# 自定义策略heartbeat-interval:3000# 心跳间隔从 5s 调到 3s更快感知故障ip-delete-timeout:9000# 超时剔除时间相应调整# 关键开启服务订阅的本地缓存减少对 Nacos 的读请求压力naming-cache-enabled:true方案三服务端优化配合客户端优化了路由策略服务端也做了些配合调整# Nacos 服务端 application.properties 调整nacos.naming.empty-service.auto-cleantrue nacos.naming.empty-service.clean.initial-delay-ms5000 nacos.naming.empty-service.clean.period-ms30000# 增大 Raft 快照频率减少状态机同步压力nacos.core.protocol.raft.data.snapshot_interval_secs3600# 调整 gRPC 线程池nacos.remote.server.grpc.thread.num32效果对比上线后观察了一周数据对比很直观指标优化前优化后服务发现耗时 P995.2s45ms服务发现耗时 P952.8s28msNacos Leader CPU93%35%Nacos Follower CPU78%22%心跳注册失败率12%0%服务发现读请求 QPS分散到 3 节点分散到 3 节点一致性哈希写请求转发次数约 66% 请求被转发0直接命中 Leader核心收益来自两点写请求直达 Leader消除了 Follower 到 Leader 的转发开销读请求通过一致性哈希分散同一个服务的请求总是命中同一个节点缓存命中率更高踩坑记录血泪教训这 4 条坑让我多折腾了两天坑 1一致性哈希的虚拟节点数没调好一开始虚拟节点设了 50 个结果服务分布不均匀有个节点还是承担了 60% 的读请求。后来调到 150 个分布才均衡。虚拟节点数太少哈希环的连续性不够。坑 2Leader 切换时客户端没及时感知Nacos Leader 切换后我的自定义负载均衡器还在往旧 Leader 发写请求导致 30 秒内注册失败。后来加了定时轮询/nacos/v1/core/cluster/nodes的机制5 秒刷新一次 Leader 状态。Scheduled(fixedRate5000)publicvoidrefreshLeader(){// 轮询集群状态更新 Leader 缓存this.currentLeaderfetchLeaderFromCluster();}坑 3心跳间隔调小后网络抖动导致频繁误判下线心跳间隔从 5s 调到 3s 后有几次网络抖动导致心跳超时实例被误判下线。后来把ip-delete-timeout从 15s 调到 9s心跳间隔的 3 倍给网络抖动留了点容错空间。坑 4Spring Cloud Gateway 的 Nacos 负载均衡和注册中心负载均衡搞混了Spring Cloud Gateway 有自己的服务发现负载均衡如 Ribbon/Spring Cloud LoadBalancer这个是在服务调用层面做负载均衡。而我上面改的是 Nacos 客户端连接服务端时的负载均衡两层逻辑不一样。一开始我改错了地方折腾了半天发现改的是 Gateway 的负载均衡策略不是 Nacos 客户端的。写在最后这次排查让我对服务注册发现的性能边界有了新认知。Nacos 2.x 用 gRPC 长连接优化了订阅推送但服务注册和心跳这部分还是走 HTTP而且写请求的 Leader 转发机制很容易成为瓶颈。一致性哈希在这里的作用不是传统意义上的均匀分布而是读写隔离缓存亲和性——同一个服务的读请求总落到同一个节点本地缓存效果更好写请求则直接命中 Leader避免转发。如果你也在用 Nacos建议检查两点客户端的负载均衡策略是否考虑了 Nacos 的读写分离特性服务端心跳间隔和超时配置是否匹配你的网络环境和实例规模有问题欢迎评论区交流。