
线上Nacos突然把线程池打爆这种事遇到一次就够写几个月复盘了。整整三天我都在跟这一堆不涨负载、不涨内存、偏偏线程数像坐火箭一样的线程搏斗从监控报警到最后定位到代码里一个不起眼的监听器过程相当曲折。今天把这次完整的排查过程、取证手段和最终修复方案整理出来尤其想给正在用Nacos的同学提个醒线程数飙升很多时候不是Nacos本身的问题而是你接入Nacos的方式在特定条件下把线程资源一点点耗干的。先说结论。这个问题在常见的Nacos客户端使用场景里完全有机会复现尤其是那些在配置监听器里做了外部调用、数据库操作或者其它慢动作的业务系统更容易踩中。如果你也遇到过线程数持续上涨、重启以后恢复、过几天又涨上去的现象而且jstack里能看到大量Nacos客户端相关线程这篇文章就是写给你的。1. 现象与第一反应1.1 报警是怎么响起来的那天下午监控系统突然弹出一堆线程数告警业务实例的线程总数从平时稳定的200到300之间直接飙升到2800多而且还在继续上涨。第一反应是流量进来了先翻了一遍QPS、RT、CPU、内存和GC情况结果是QPS没有任何异常波动CPU和内存也很平稳Full GC频率没有明显变化唯一不对劲的就是线程数。当时我的第一判断是某段代码被人改了引入了无限创建线程的逻辑或者连接池配置有误把线程泄漏在循环里了。先连续抓了两轮线程快照对比希望找到到底是哪一类线程在暴增。1.2 快速排除不是业务线程而是Nacos内部线程抓线程快照是有讲究的不能单抓一次就下结论一定要在同一时刻附近抓两到三份间隔10秒左右才能看出线程是怎么变化的。当时用jstack连续抓了三份文件都保存下来后用简单的文本统计命令按线程名做了归类结果非常明确nacos-grpc-client-*这类线程占了大头数量从最初的几十个涨到了近2000个业务相关的线程Tomcat、业务线程池、定时任务数量基本没动还有一批nacos-client-*和配置相关的内部线程也在跟着涨。看到这个结果我差点直接把锅甩给Nacos。但冷静下来想Nacos客户端版本用的是2.x稳定版网上也没有大规模反馈过这种线程泄漏案例大概率还是自己用歪了。于是开始着手把Nacos线程为什么会疯狂增长这个问题拆开。2. 一把jstack把现场钉死2.1 抓线程快照的正确姿势这里分享一下当时操作的完整过程排查线程问题时这套动作是可以直接复用的。第一步用top -Hp PID找到当前进程内CPU占用比较高的线程注意这次CPU并不高但依然执行了一遍确保没有发现隐藏的高CPU线程。第二步用jstack -l PID /tmp/jstack_1.txt抓第一份快照隔10秒后再抓一份这两份快照是后面找根因的关键依据。第三步用jstat -gcutil PID 1000 5观察GC情况排除GC导致的线程阻塞。这里有一个经验不要先把服务重启重启会把现场全部毁掉。线程数飙升不是传统意义上的崩溃在没有搞清楚原因之前重启等于把取证机会白白扔掉。宁可先把流量摘掉也要多保留一会儿现场。2.2 线程栈里读出来的异常信号把三份线程快照打开后重点看线程的状态分布。那批nacos-grpc-client-*线程绝大多数状态是WAITING或TIMED_WAITING少部分是RUNNABLE但看不到明显的同步锁冲突。进一步看堆栈细节大量线程最终都卡在了同一个位置at java.base/java.net.SocketInputStream.socketRead0(Native Method) at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140) ... at com.alibaba.nacos.client.config.utils....它们都不是死锁而是全部阻塞在读网络响应上。换句话说Nacos客户端线程池里大量线程在等待外部请求返回而请求迟迟不返回导致线程资源被长期占住新请求进来后客户端只能不断创建新线程来补充。这里必须解释一下Nacos客户端2.x的线程模型客户端会维护用于配置变更、服务发现的内部线程池正常情况这些线程是复用的数量很稳定。但如果某个任务的执行时间异常拉长活动线程数量激增线程池就会按需扩容如果拉长的任务一直不释放扩容出来的线程也不会回收最终就会看到线程数只增不减。2.3 为什么线程数只涨不降很多人遇到这种场景都会有一个疑问既然任务池有最大线程数限制为什么还能涨到2800这就要看Nacos客户端的部分内部线程池是否使用了无界队列和比较大的最大线程数配置。任务不断塞入队列同时线程池不断尝试创建新线程来处理堆积任务最大线程数足够大时线程数就会持续上涨。另外还有一个推波助澜的因素配置变更事件是频繁发生的。每次有配置发布事件过来客户端内部会触发监听器执行如果这些事件本身频繁触发而且每个事件处理都要几秒甚至几十秒那么堆积速度会远远超过线程释放速度线程数就会呈现出一条陡峭的上升曲线。3. 根因定位监听器里的慢HTTP才是真凶3.1 顺着线程栈往里翻光有线程名称还不足以定位到业务代码因为Nacos客户端线程池是通用的它执行的是注册进来的监听器回调而真正的问题代码就藏在回调里。当时顺着WAITING线程的完整堆栈往上追发现在Nacos客户端内部回调链路下方赫然出现了我们自己业务代码的类名里面有一段RestTemplate调用而且这个调用完全没有设置连接超时和读取超时。再结合当天的一个变更记录有人在某个配置监听器里加了一个逻辑配置变更时要去外部系统拉取一份白名单数据用于刷新本地缓存。看起来是一个很常见的需求但实现方式埋了三个雷第一监听器内部直接用同步HTTP请求调用外部接口没有设置超时时间第二这个外部接口当天恰好不稳定响应时间从原来的几十毫秒涨到了几十秒第三外部系统还做了熔断限流触发后直接挂起连接等待超时而这个超时时间设置得极长。于是Nacos客户端监听到配置变更把该监听器提交到线程池里执行结果所有线程都卡在了外部HTTP调用上。一次配置变更可能只产生一个任务但由于线程池里的线程被慢性阻塞后续配置变更事件又不断产生新任务客户端内部线程池为了尽量处理任务就不断新建线程最终线程数直接被打爆。3.2 复现实验一个监听器卡死整条池子遭殃为了验证这个推断我在测试环境做了一个最小复现使用Nacos 2.2.x客户端注册一个配置监听器监听器里写入一个Thread.sleep(30000)模拟慢调用连续触发十次配置变更观察客户端线程池线程数变化。结果和线上表现高度一致配置变更只要连续触发Nacos客户端线程池线程数就会阶梯式上涨且任务执行越慢、触发越频繁上涨速度越快。等到把Thread.sleep去掉之后线程数就不再新增但已创建的线程依然会保留一段时间。这个现象和线上完全吻合。3.3 数据估算一个慢监听器能吃多少线程假设每次配置变更产生一个任务每个任务阻塞30秒线程池最大线程数设置为200以上那么连续触发7次配置变更就足以让线程池内活动线程数卡在200左右。如果外部接口持续不稳定配置变更又因为其他原因频繁触发比如每次启动时拉取全量配置、定时重连、命名空间配置刷新等那么线程数就能在几小时内从200涨到2000甚至更多。这里有一个容易被忽略的放大因素Nacos配置中心很多团队会做配置合并、热更新和动态开关发布频率往往比想象中高。只要发布方有人误触或自动化脚本重复发布相同配置监听器就会被反复触发慢任务的堆积速度就会成倍增加。4. 整改方案线程池治理三板斧4.1 代码层面监听器内部禁止做耗时操作根因既然找到了第一件事就是把问题代码修掉。我们的做法有三步第一步监听器内部收件箱化配置变更只负责更新一个内存标记或最新版本号立即返回。第二步真正的白名单加载逻辑挪到独立的异步任务队列里由专用的线程池处理这个线程池单独设置超时时间、队列大小和拒绝策略。第三步所有外部HTTP调用必须显式设置连接超时和读取超时我们统一规定连接超时不超过2秒读取超时不超过3秒。这是典型的“同步阻塞操作混入事件回调”引发的线程池问题。事件回调里只做轻量操作耗时任务必须丢到独立的线程池或者消息队列里做削峰这条规则不光适用于Nacos也适用于所有中间件监听器、消息消费者和定时任务。另外如果你的业务确实需要在监听器里做外部调用至少要做到两点设置合理的超时时间用独立线程池隔离避免拖垮中间件客户端内部的线程池。4.2 参数层面给Nacos客户端和JVM上保险代码修复之后我们还从参数层面做了一层防御。Nacos客户端方面升级到最新的稳定版2.x并显式配置了连接和请求的底层超时参数避免出现无限等待。JVM层面给关键服务加上了线程数监控配合动态线程池管理工具对核心业务线程池做了数量上限控制防止再出现类似场景时线程数一路狂飙。这里要提醒一下网上讨论比较多的nacos.client.grpc.timeout这类参数不同版本支持情况不一样配置前一定先确认自己用的版本是否支持不要照搬其他项目的配置。经过调整再次连续发布配置触发监听器时线程数能保持稳定不会再出现阶梯式上涨。这说明问题不是出在Nacos客户端本身而是监听器实现方式不正确。Nacos客户端内部线程池的设计是足够健壮的只是它扛不住外部慢调用长期占用线程。4.3 监控层面把线程数和线程池指标纳入告警过去我们的监控只覆盖了CPU、内存、磁盘、连接数这些常规指标对线程数的关注不够。这次事件之后我把线程数、活跃线程数、线程池队列积压量都加到了核心服务的告警规则里并对Nacos客户端内部线程数量设置了单独的阈值告警。具体做法是用Spring Boot Actuator暴露线程池指标再接入Prometheus和AlertManager。当线程数超过基线的两倍并持续五分钟时触发告警避免等到线程数冲到几千才发现问题。这里分享一个实用的指标正常情况下一个Java服务中Nacos客户端相关线程数应该是一个极小且稳定的数字。如果你发现这个数字在持续增长哪怕很缓慢也说明一定有问题早晚会爆发。这也是最值得监控的信号。5. 复盘我把这些排查经验固化成了清单5.1 遇到线程数飙升我会按这个顺序排查这次踩坑之后我给自己定了一份排查清单遇到类似问题直接照着执行先确认线程数增长曲线和业务流量、GC、CPU、连接数的时间对应关系排除流量波动和GC异常连续抓三份jstack间隔10秒按线程名分类统计量化不同线程池的增长速度优先排查中间件客户端线程它们的线程数量通常稳定一旦异常就是使用方式有问题顺着线程栈定位业务代码重点看回调、监听器、异步任务里的外部调用检查外部调用是否有超时控制、是否合理使用了独立线程池、是否有熔断降级机制修复后用压测和持续配置变更的方式验证确认线程数回归稳定补上线程池指标和告警做到下次在早期就能发现问题。5.2 几个反直觉但很重要的经验第一线程数飙升不一定代表高并发。很多新手看到线程数暴涨就以为是流量问题实际上大量的线程可能只是在同一个地方等待而这个等待会掩盖更深层的故障。第二中间件的客户端线程池看似离业务很远其实是最容易被慢业务拖垮的环节。一旦中间件线程被业务慢调用占满影响范围就不仅是那段业务代码而是整个客户端实例。第三配置监听器这类“低频”回调最容易出问题因为低频让人放松警惕但它触发一次可能影响一堆线程。我还想强调一个容易被忽略的细节排查线程问题时线上抓到的jstack文件一定要保留好不要排查完就删。很多问题在当时看起来是偶发但过几天复盘对比历史线程栈时反而能发现更深层的规律。这次我们能这么快定位就是因为留了完整的快照记录可以反复对比。如果你现在也在用Nacos建议立刻检查一遍所有配置监听器里的代码逻辑看看有没有外部调用、数据库访问、锁等待这类耗时操作。不要等到线程数报警了才开始查排查过程虽然有趣但生产事故对业务的影响真不是开玩笑的。