本文大纲
《JVM剖析及性能跟踪》
案例3--Tomca线程被耗尽
环境描述
本案例发生在阿里云ECS服务器上,服务器架构包括Nginx、Tomcat和MySQL(RDS)。在上线部署时,已经进行了一些优化,如设置Tomcat内存参数:-Xms2048m -Xmx4096m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m,并且Tomcat默认使用NIO模式。此外,Linux系统的最大打开文件数也已设置完成。
问题描述
网站无法打开,浏览器显示加载中但无响应。检查系统资源发现CPU使用率和内存使用率均正常,Tomcat日志无异常,Nginx日志显示有少量499和503状态码,而MySQL(RDS)响应迅速。尽管一切看似正常,但系统仍处于无响应状态。
一切都这么安静,太可怕了,系统死的安安静静,没有任何迹象。之前的案例都用不上了。
解决方案
为了快速恢复业务,重启了两台服务器中的一台,保留另一台用于进一步分析。
分析步骤
-
查找Tomcat进程
使用
jps -v命令找到Tomcat的PID为25568。

-
分析JVM内存
通过
jstat -gcutil 25568 1000 10和jmap -heap 25568命令检查JVM内存状态,发现一切正常,没有内存泄漏或GC问题。

-
分析线程状态
使用
jstack -l 25568 | grep 'java.lang.Thread.State' | wc命令发现有380个线程,远超预期。
jstack -l 25568 | grep ‘java.lang.Thread.State’
进一步检查发现大量线程处于WAITING状态。发现大量 WAITING状态的线程,这是不对的。

-
Dump线程信息
执行
jstack -l 25568 >> thread.log并将日志文件下载到本地分析。发现问题是由于文件上传下载功能在调用阿里云OSS时,HTTP通信阶段偶尔出现失败,且没有设置超时时间,导致线程长时间等待,最终耗尽Tomcat线程。
到这一步,就已经成功的定位到了代码行。 是我自己开发的文件上传下载功能,要调用阿里云的OSS,大多数时候的调用是成功的,有少数几次是在http网络通信阶段有失败,由于没有超时时间,就一直等返回结果,越积累越多,直到占光tomcat所有线程。

结论与建议
-
线程耗尽原因
由于文件上传下载功能在调用阿里云OSS时,HTTP通信偶尔失败且无超时设置,导致线程长时间阻塞,最终耗尽Tomcat线程池。
-
预防措施
- 设置连接超时:在进行外部服务调用时,务必设置合理的连接超时和读取超时,避免线程长时间阻塞。
- 监控与报警:加强对线程池状态的监控,设置报警阈值,及时发现和处理线程耗尽问题。
通过本案例,我们深刻认识到外部服务调用的风险,并学会了通过分析线程状态来定位和解决线程耗尽问题。希望这些经验和教训能为读者在类似问题的排查和预防中提供帮助。
使用Arthas 再检查一次
使用方法:JVM剖析工具–Arthas
线程是的的常用命令
- thread -b 找出当前阻塞其他线程的线程。注意, 目前只支持找出synchronized关键字阻塞住的线程, 如果是
java.util.concurrent.Lock, 目前还不支持。 - thread 查看当前线程列表,只显示第一页
- thread –all 查看当前线程列表,显示所有
thread -i 1000: 统计最近1000ms内的线程CPU时间。thread -n 3 -i 1000: 列出1000ms内最忙的3个线程栈- thread –state WAITING –all ,查看指定状态的线程
- thread –state BLOCKED –all ,查看指定状态的线程
启动一 java -jar arthas-boot.jar
希望使用thread -b 找出死锁,结果 没有找出。因为: 目前只支持找出synchronized关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持。 本案例就是java.util.concurrent.Lock的锁,你看上面的截图。

希望使用thread –state BLOCKED –all ,查看指定状态的线程。 结果:没有

希望使用thread –state WAITING –all ,查看指定状态的线程
结果 :发现最多的是 http-nio-8080-exec-xxx 线程,这是tomcat线程池中的NIO线程。
和上使用 jstack -l pid 命令分析到的是一样的结果, 这就是问题的所在。

线程WAITING状态 –教程
waiting有两种
第一种:waiting for monitor entry

第二种:waiting on condition
WAITING(parking):表示一直等待那个条件发生

说明
- 线程状态为“waiting for monitor entry”:
意味着它 在等待进入一个临界区 ,所以它在”Entry Set“队列中等待。此时线程状态一般都是 Blocked:
java.lang.Thread.State: BLOCKED (on object monitor) - 线程状态为“waiting on condition”:
说明它在等待另一个条件的发生,来把自己唤醒,或者干脆它是调用了 sleep(N)。此时线程状态大致为以下几种:
java.lang.Thread.State: WAITING (parking):一直等那个条件发生(本文案例即为此种场景);
java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时的,那个条件不到来,也将定时唤醒自己。 - 如果大量线程在“waiting for monitor entry”:可能是一个全局锁阻塞住了大量线程。如果短时间内打印的thread dump 文件反映,随着时间流逝,waiting for monitor entry 的线程越来越多,没有减少的趋势,可能意味着某些线程在临界区里呆的时间太长了,以至于越来越多新线程迟迟无法进入临界区。
- 如果大量线程在“waiting on condition”:可能是它们又跑去获取第三方资源,尤其是第三方网络资源,迟迟获取不到Response,导致大量线程进入等待状态。所以如果你发现有大量的线程都处在 Wait on condition,从线程堆栈看,正等待网络读写,这可能是一个网络瓶颈的征兆,因为网络阻塞导致线程无法执行。也可能是如本文所提到的,由于程序编写不当所致。