环境:3 节点 Nacos 集群,Docker Compose 部署,
network_mode: host,外部 MySQL。集群 3.1.1 ,节点间偶发 gRPC 超时,曾伴随服务调用失败和健康节点列表抖动。本文记录当时的证据、排查过程及处理结果。
现象
某节点反复连接172.23.3.5:9849失败,8848HTTP 端口仍可访问。另一次排查中,nacos.log一天记录了两次成员上报超时,分别发生在14:15:08和14:22:38:
ERROR Send request fail, request = MemberReportRequest{...}, retryTimes = 0, errorMessage = java.util.concurrent.TimeoutException: Waited 3000 millisecondsMemberReportRequest是 Nacos Server 之间的成员状态上报请求。请求超过 3 秒没有得到响应,会影响节点之间对成员健康状态的判断。如果持续发生,还可能伴随服务数据同步失败和健康节点列表变化。单次超时本身不能证明业务一定失败,需要结合nacos-cluster.log、naming-server.log、Distro 日志和客户端报错判断。
几个端口容易混淆:
| 端口 | 用途 |
|---|---|
8848 | HTTP API,以及部分插件请求 |
9848 | Java 等客户端连接 Nacos Server 的 gRPC 端口 |
9849 | Nacos Server 节点之间的 gRPC 端口 |
7848 | 节点间 JRaft 通信端口 |
8080 | Nacos 3.x 控制台端口 |
因此,8848可访问不能说明9849正常。Java 客户端即使切换到另一个健康的 Nacos 节点,也无法代替集群内部向指定节点发送的成员上报请求。Nacos 官方端口说明
先排除重启和长时间 GC
检查容器状态:
dockerinspect-f'OOM={{.State.OOMKilled}} Restart={{.RestartCount}} Started={{.State.StartedAt}}'nacos2dockerstats --no-stream nacos2现场结果:
OOM=false Restart=0 MEM USAGE ≈ 1.86 GiB PIDS=5986容器没有 OOM 或重启。GC 日志使用 UTC 时间;北京时间14:15:08对应 UTC06:15:08。附近的一次 Young GC 发生在 UTC06:15:25,暂停约28 ms,比超时晚约 17 秒,不能解释这次 3 秒等待。另一处报错时间附近也未找到长时间 GC。宿主机内核日志中没有发现 OOM、网卡掉线或 conntrack 溢出的明确记录,但这不能完全排除瞬时网络重传。
此时最突出的指标是PIDS=5986。Docker 的 PIDS 统计包含线程,所以继续检查 Java 进程:
dockerexecnacos2sh-c'grep -E "^(Name|Threads|VmRSS|VmSize):" /proc/1/status'Name: java VmRSS: 1960344 kB Threads: 5985容器的近 6000 个 PIDS,几乎全是 Java 线程。官方镜像里没有jcmd,所以直接通过/proc统计线程名称:
dockerexecnacos2sh-c'for f in /proc/1/task/*/comm; do cat "$f"; done'\|sort|uniq-c|sort-nr|head-20关键结果如下。Linux 的comm最多显示 15 个字符,因此名称被截断:
2048 nacos-grpc-exec 1098 nacos-cluster-g 512 nacos-grpc-clie 260 nacos-http-asyn 128 JRaft-Rpc-Closu 128 JRaft-Group-Def 128 grpc-default-wo 128 DistroExecuteTa三台节点的线程数都达到几千。多个线程池恰好呈现128的倍数,强烈提示 JVM 当时按大量可用 CPU 来确定线程池规模;128是根据线程数量作出的推断,现场没有用 JVM 命令直接确认有效 CPU 数。Nacos 社区也记录过高核机器上的 gRPC 线程池随处理器数量扩张的情况:相关 Issue。
为什么会发生
原来的 Compose 没有设置 CPU 和内存限制。docker stats显示的内存上限约为宿主机的1007 GiB,Nacos 进程可能看到了宿主机的大量处理器。Nacos、gRPC、JRaft、Distro 的多个线程池会据此配置并发规模,最终出现近 6000 个 Java 线程。
大量线程会增加调度和上下文切换开销,也会消耗线程栈等非堆内存。它能够解释“端口平时可达,但内部 gRPC 偶尔在 3 秒内没有响应”的现象。不过,三台都有几千线程而故障总指向.5,仍可能存在.5的连接负载或链路差异;仅凭线程数不能证明每一次超时都由线程调度直接造成。
处理方式与结果
给 Nacos 容器设置明确的 CPU 和内存边界。下面的4C8G是本次讨论采用的配置示例,生产环境应按自身峰值负载调整:
services:nacos:image:nacos/nacos-server:v3.1.1container_name:nacos2network_mode:hostcpus:"4.0"mem_limit:8gvolumes:-/data/nacos/logs:/home/nacos/logs-/data/nacos/application.properties:/home/nacos/conf/application.propertiesenv_file:-/data/nacos/nacos-ip.envrestart:on-failureJava 17 默认支持识别 Linux 容器的 CPU 和内存限制。cpus控制实际 CPU 配额,同时通常能使 JVM 按较小的有效处理器数量配置线程池;mem_limit提供内存边界,本身不会直接减少线程。如果 JVM 未正确识别 CPU 配额,可以再评估-XX:ActiveProcessorCount=4。该参数只改变 JVM 计算线程池与 GC 并行度时看到的处理器数,不限制容器实际能使用的 CPU。Java 17 参数文档
配置按节点滚动调整,单次只重建一个节点,确认它重新加入集群后再处理下一个。调整后,现场反馈此前的偶发超时没有再出现。这支持“资源边界缺失导致线程数过大,是本次问题的重要因素”的判断;由于没有保留长期对照监测和故障时刻的 TCP 抓包,不能据此断言网络因素已被彻底排除。
后续可用以下命令持续核查:
dockerstats --no-stream nacos2dockerexecnacos2sh-c'grep -E "^(Threads|VmRSS):" /proc/1/status'dockerinspect-f'OOM={{.State.OOMKilled}} Restart={{.RestartCount}}'nacos2同时观察MemberReportRequest、Server check fail、healthy server list changed和Sync data change failed是否仍在同一时段出现。如果限制资源后依然只在.5复现,应继续比较三台的客户端连接数、CPU 峰值,以及指向.5:9849的 TCP 重传。
两个运维补充
第一,MySQL 保存主要业务数据,但不等于容器内/home/nacos/data可以随意丢弃。该目录包含节点本地的协议与 JRaft 状态。Nacos 还会在这里写入jraft-auth-enforced.state;官方明确提示不要通过删除该文件尝试恢复旧版本兼容性。计划重建容器时,应检查该目录并为每个节点单独备份或持久化。
第二,network_mode: host省去了 Docker 端口映射,但不会排除宿主机网络问题。三台节点仍需保证彼此的9849、7848通信稳定。排查时先看容器线程、GC、CPU和成员日志,再结合 TCP 重传与网络抓包定性,避免仅凭一次TimeoutException就认定是 Nacos 程序 Bug 或网络故障。
小结
这次排查的转折点不是8848能否访问,而是PIDS=5986和 Java 进程Threads=5985。多个内部线程池随高核宿主机放大;为容器设置资源边界后,用户反馈故障不再复现。Nacos 集群出现9849偶发超时时,应同时检查节点间通信和 JVM 的实际线程规模。