本文大纲

《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。

image2021-3-25_17-21-3.png

  • 分析JVM内存

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

image2021-3-25_17-25-15.png

  • 分析线程状态

    使用jstack -l 25568 | grep 'java.lang.Thread.State' | wc命令发现有380个线程,远超预期。

    image2021-3-25_17-25-46.png

    jstack -l  25568  | grep ‘java.lang.Thread.State’

    进一步检查发现大量线程处于WAITING状态。发现大量 WAITING状态的线程,这是不对的。

    image2021-3-25_17-27-13.png

  1. Dump线程信息

    执行jstack -l 25568 >> thread.log并将日志文件下载到本地分析。

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

image2021-3-25_17-33-5.png

结论与建议

  1. 线程耗尽原因

    由于文件上传下载功能在调用阿里云OSS时,HTTP通信偶尔失败且无超时设置,导致线程长时间阻塞,最终耗尽Tomcat线程池。

  2. 预防措施

    • 设置连接超时:在进行外部服务调用时,务必设置合理的连接超时和读取超时,避免线程长时间阻塞。
    • 监控与报警:加强对线程池状态的监控,设置报警阈值,及时发现和处理线程耗尽问题。

通过本案例,我们深刻认识到外部服务调用的风险,并学会了通过分析线程状态来定位和解决线程耗尽问题。希望这些经验和教训能为读者在类似问题的排查和预防中提供帮助。

使用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的锁,你看上面的截图。

image2021-3-25_17-37-53.png

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

image2021-3-25_17-40-4.png

希望使用thread –state WAITING    –all   ,查看指定状态的线程

结果 :发现最多的是  http-nio-8080-exec-xxx  线程,这是tomcat线程池中的NIO线程。

和上使用   jstack -l  pid  命令分析到的是一样的结果, 这就是问题的所在。

image2021-3-25_17-40-47.png

线程WAITING状态 –教程

waiting有两种

第一种:waiting for monitor entry

image2021-3-25_17-48-14.png

第二种:waiting on condition

WAITING(parking):表示一直等待那个条件发生

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