尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Tomcat假死故障排查:从CLOSE_WAIT到内存泄漏的实战指南

Tomcat假死故障排查:从CLOSE_WAIT到内存泄漏的实战指南 1. 项目概述从一次线上事故说起那天下午监控大屏上突然弹出一连串告警某个核心服务的响应时间曲线像坐了火箭一样直线飙升最终彻底变成一条平坦的直线——服务无响应了。登录服务器一看Tomcat进程还在ps命令显示它“活”得好好的CPU和内存占用也不高但就是没有任何请求能被处理前端用户看到的是无尽的加载圈。这就是典型的“Tomcat假死”一个让无数运维和开发同学头皮发麻的问题。它不像进程崩溃那样干脆利落会留下堆栈日志也不像资源耗尽那样特征明显监控指标可能一切“正常”。它更像是一种“植物人”状态进程存在但灵魂处理能力已经出窍。这个问题之所以棘手是因为它的诱因往往隐藏在应用代码、操作系统配置、网络交互甚至第三方库的深处。从热词中我们可以看到CLOSE_WAIT、HashMap、内存泄漏等都是高频关联词这已经为我们指明了排查的大致方向。本文将结合我处理多次类似故障的经验为你梳理出一套从表象到根源层层递进的排查方法论。无论你是刚接手线上服务的开发还是负责保障稳定的运维这套流程都能帮你快速定位问题恢复服务。2. 核心排查思路与整体设计面对Tomcat假死切忌无头苍蝇般地乱试。一个系统化的排查思路能让你事半功倍。我的核心思路是由外及内先全局后局部先现象后根源。具体可以分解为以下四个层次现象确认与影响评估首先明确“假死”的具体表现。是所有接口都超时还是部分是彻底无响应还是响应极慢同时快速评估影响范围决定是否需要立即重启恢复服务。系统与容器层健康检查检查Tomcat进程所在的宿主服务器或容器如Docker/Podman的整体健康状况。这能排除掉非Tomcat本身导致的问题。Tomcat自身状态诊断深入Tomcat内部检查其线程、连接、内存等核心组件的状态。应用层代码深度剖析这是最复杂的一环需要结合日志、代码和监控定位可能存在的死锁、死循环、资源泄漏等问题。整个排查过程可以借助一个简单的决策树来引导服务无响应 - 检查服务器负载CPU、内存、磁盘I/O、网络- 如果正常 - 检查Tomcat线程状态 - 如果线程池满/死锁 - 分析线程堆栈 - 定位代码问题。如果线程状态看似正常 - 检查网络连接状态特别是CLOSE_WAIT- 如果过多 - 检查应用代码中的连接是否未正确关闭。如果以上都正常 - 检查JVM内存使用情况堆内存、元空间- 如果存在持续增长而不回收 - 怀疑内存泄漏 - 生成堆转储文件分析。这个思路确保了排查的全面性和效率避免遗漏关键线索。接下来我们将深入每个环节的实操细节。2.1 排查工具箱准备工欲善其事必先利其器。在问题发生前就应该准备好这些工具而不是临时抱佛脚。命令行工具top/htop查看系统整体负载和进程资源占用。vmstat/iostat查看CPU、内存、磁盘I/O的详细情况。netstat/ss查看网络连接状态ss命令更高效推荐使用。jps列出当前用户下的所有Java进程ID。jstack获取Java进程的线程堆栈快照分析死锁和线程状态的神器。jmap生成Java堆转储文件用于分析内存泄漏。jstat查看JVM内存、垃圾回收等统计信息。JDK内置图形工具可选用于深度分析jvisualvm功能强大的图形化监控和故障分析工具。jconsole简单的JMX监控控制台。Tomcat自身管理界面如果配置了manager应用可以通过其查看连接数、会话数等。应用日志这是最重要的线索来源。确保你的应用日志级别合理如DEBUG用于排查INFO用于运行并且日志格式包含了线程名、时间戳等信息。注意生产环境执行jstack和jmap可能会引起短暂的停顿尤其是jmap -dump生成堆转储时。请在业务低峰期操作或先通过jmap -histo:live查看对象直方图进行初步判断。3. 分层排查实操详解3.1 第一步系统与容器层健康检查当接到告警第一步不是直接登录Tomcat而是先看它的“生存环境”。1. 检查系统资源瓶颈# 1. 使用top命令查看整体负载 top重点关注%Cpu(s)行的waIO等待值。如果wa值持续很高例如超过20%说明磁盘IO可能是瓶颈可能是日志写入过于频繁或磁盘本身有问题。同时看load average如果长期超过CPU核数的2-3倍说明系统负载过重。# 2. 检查内存和交换分区 free -h确保available内存充足并且swap交换分区使用率不高。频繁使用swap会导致性能急剧下降。2. 检查网络连接数限制Tomcat需要打开大量文件描述符每个Socket连接都是一个文件描述符。如果系统限制太小可能导致无法建立新连接。# 查看当前用户和系统的文件描述符限制 ulimit -n # 查看系统全局限制 cat /proc/sys/fs/file-max如果ulimit -n输出值较小比如默认的1024对于高并发应用是远远不够的。需要修改/etc/security/limits.conf文件进行调整。3. 检查容器状态如果使用Docker/Podman正如热词中提到的podman ps 检查状态 为 up 18minutes ago 但不能访问容器状态显示“Up”并不代表内部应用健康。# 进入容器内部执行命令 podman exec -it container_id /bin/bash # 或直接执行命令 podman exec container_id top podman exec container_id ss -tlnp需要进入容器内部重复上述系统检查步骤因为容器有独立的PID、网络和文件系统命名空间。3.2 第二步Tomcat进程与JVM状态诊断确认系统层无异常后我们将焦点转移到Tomcat进程本身。1. 获取Tomcat进程信息# 使用jps找到Tomcat的进程ID jps -l # 输出类似12345 /path/to/tomcat/bin/bootstrap.jar记下进程IDPID例如12345。2. 检查JVM内存与GC情况使用jstat观察垃圾回收是否正常。# 每2秒采样一次共采样10次查看堆内存和GC情况 jstat -gcutil 12345 2000 10关键列S0,S1: Survivor区使用率。E: Eden区使用率。如果每次GC后E区都能下降说明年轻代GC正常。O: 老年代使用率。如果O区使用率持续缓慢上升且Full GC后下降不明显是内存泄漏的强烈信号。YGC,YGCT: 年轻代GC次数和时间。FGC,FGCT: Full GC次数和时间。如果FGC在短时间内频繁增加且FGCT很长说明堆内存不足或存在内存泄漏频繁的Full GC会“Stop The World”导致应用长时间停顿从外部看就是假死。3. 检查线程状态——这是排查假死的核心使用jstack获取线程快照。# 生成线程堆栈到文件 jstack -l 12345 /tmp/jstack_12345.log分析jstack输出文件搜索关键字deadlock如果存在死锁jstack会在文件开头明确提示。统计线程状态重点关注RUNNABLE、BLOCKED、WAITING、TIMED_WAITING状态的线程。如果大量线程处于BLOCKED状态说明它们在等待锁可能存在锁竞争激烈或死锁。如果大量业务线程如http-nio-8080-exec-*处于WAITING或TIMED_WAITING状态这可能是正常的在等待任务但需要结合线程池队列情况判断。最危险的情况所有或大部分业务线程都处于RUNNABLE状态并且堆栈显示它们卡在同一个方法或同一个循环里。这很可能是一个无限循环或阻塞式IO操作如同步等待数据库响应而数据库挂了。查看线程池队列在堆栈中搜索java.util.concurrent.ThreadPoolExecutor查看其workQueue的大小。如果队列积压非常严重说明任务生产速度远大于消费速度。实操心得一次假死排查中jstack显示所有http-nio线程都处于RUNNABLE堆栈指向一个第三方JSON解析库的某个方法。最终发现是解析一个深度嵌套且畸形的JSON时该库陷入了死循环。解决方法是为解析操作设置超时时间或改用更健壮的库。3.3 第三步网络连接与CLOSE_WAIT陷阱网络连接问题特别是CLOSE_WAIT状态连接过多是导致Tomcat可用的文件描述符耗尽进而无法接受新连接的常见原因。1. 检查Tomcat连接状态# 使用ss命令更高效。查看所有TCP连接并过滤出Tomcat进程 ss -tlnp | grep 12345 # 或者查看指定端口如8080的连接 ss -tan | grep :80802. 理解CLOSE_WAITCLOSE_WAIT是TCP四次挥手过程中的一个状态。简单来说当对方客户端主动关闭连接后你的服务器Tomcat收到了FIN包应该回应ACK并进入CLOSE_WAIT状态然后你的应用程序应该调用socket.close()来发送最后的FIN包使连接进入LAST_ACK然后关闭。 如果大量连接停留在CLOSE_WAIT状态根本原因是你的应用程序没有正确关闭Socket连接。可能是代码中打开了数据库连接、HTTP连接、Redis连接等但在异常处理路径中忘记关闭或者关闭连接的代码没有被执行到。3. 排查与解决# 统计各种状态的连接数 ss -tan | awk {print $1} | sort | uniq -c如果发现成千上万的CLOSE_WAIT基本可以确定是连接泄漏。排查代码检查所有使用网络连接的地方HTTP客户端、数据库连接池、Redis客户端等确保在finally块中或使用try-with-resources语法Java 7进行关闭。检查连接池配置对于数据库连接池如HikariCP, Druid检查maximumPoolSize是否设置过大以及是否有连接泄漏检测配置如leakDetectionThreshold。使用工具监控可以通过APM工具如SkyWalking, Pinpoint监控连接创建和关闭的情况。3.4 第四步应用层代码深度剖析如果以上层面都未发现明显问题那么根因很可能在应用代码本身。1. 死锁分析jstack通常能直接检测到死锁。死锁的根本原因是多个线程以不同的顺序争夺多个锁。解决方法包括锁顺序化确保所有线程以相同的全局顺序获取锁。使用超时使用tryLock(long time, TimeUnit unit)尝试获取锁超时则失败并回滚。减少锁粒度使用更细粒度的锁或使用并发集合如ConcurrentHashMap代替synchronized。2. 内存泄漏排查内存泄漏的症状是jstat显示老年代使用率O持续增长频繁Full GC也无法回收。生成堆转储文件# 生成堆转储文件文件会比较大 jmap -dump:live,formatb,file/tmp/heap_dump.hprof 12345使用分析工具将生成的heap_dump.hprof文件下载到本地使用jvisualvm或专业的Eclipse MAT进行分析。常见泄漏点静态集合类如static Map或static List不断添加对象且无移除逻辑。这与热词中的HashMap相关特别是如果使用了非线程安全的HashMap在多线程下遍历并修改还可能引发ConcurrentModificationException或其他未定义行为。缓存使用不当本地缓存如Guava Cache, Caffeine没有设置合理的过期时间或大小限制。线程局部变量ThreadLocal如果使用了线程池ThreadLocal变量在线程被复用后不会自动清除可能导致关联的类无法被回收。必须在任务结束时调用ThreadLocal.remove()。监听器与回调注册了监听器但未注销。类加载器泄漏在Web应用热部署或使用OSGi框架时常见。3. 同步阻塞与慢查询数据库慢查询检查应用日志中SQL执行时间或监控数据库慢查询日志。一条没有索引的大表全扫描SQL就足以拖垮整个线程池。同步调用外部服务HTTP调用、RPC调用如果没有设置合理的超时时间当外部服务响应缓慢或挂起时调用线程会被无限期阻塞。文件IO或本地锁同步读写大文件或者竞争同一个文件锁。4. 常见问题场景与速查表为了方便快速定位我将常见假死现象、可能原因和排查命令整理成下表现象描述可能原因优先排查命令/方向CPU占用率极高接近100%1. 代码中存在死循环2. 频繁的GC特别是Full GC3. 加密/解密、序列化等CPU密集型操作暴增top- 找到高CPU线程PID -jstack中转换PID为16进制查找对应线程堆栈。jstat -gcutil查看GC频率。内存占用持续增长不释放内存泄漏jstat -gcutil观察O区趋势。jmap -histo:live查看对象实例数排名。最终用jmap -dump分析。线程数打满无空闲线程1. 线程池配置过小2. 所有线程被慢任务/阻塞操作占用如慢SQL、无超时的外部调用3. 线程死锁jstack查看线程状态和堆栈。检查应用配置的线程池参数如maxThreads。检查数据库和外部服务监控。新建连接失败Connection refused1. 文件描述符耗尽CLOSE_WAIT过多2. Tomcat连接器Connector配置的acceptCount队列已满ss -tan | grep CLOSE_WAIT | wc -lulimit -n检查Tomcat配置server.xml中的acceptCount和maxConnections。请求响应时间变长最终超时1. 数据库慢查询2. 外部服务响应慢3. Full GC频繁导致长时间停顿查看应用和数据库慢查询日志。jstat -gcutil查看FGC和FGCT。使用链路追踪工具如SkyWalking定位慢链路。日志停止打印1. 应用进程所有业务线程死锁或进入无限等待2. 日志框架锁死较少见jstack检查线程状态。尝试发送kill -3 PID生成线程转储会输出到标准输出/日志文件。5. 防御性设计与监控建议排查是事后补救更重要的是事前预防和事中监控。1. 合理的Tomcat与JVM参数配置线程池根据实际压测结果设置maxThreads默认200可能不够和acceptCount。JVM堆内存设置明确的-Xms和-Xmx避免动态调整带来的性能波动。合理设置新生代与老年代比例-XX:NewRatio。GC日志开启GC日志便于回溯分析。-Xloggc:/path/to/gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps2. 应用代码最佳实践资源关闭使用try-with-resources确保连接、流等资源被关闭。超时机制为所有远程调用数据库、HTTP、RPC设置连接超时和读取超时。线程池隔离将不同的业务类型如CPU密集型、IO密集型或重要级别不同的任务分配到不同的线程池避免相互影响。限流与熔断使用Resilience4j、Sentinel等组件防止下游故障拖垮上游。3. 建立完善的监控体系基础监控服务器CPU、内存、磁盘、网络。JVM监控堆内存使用率、各分区使用率、GC次数与时间、线程池活跃线程数/队列大小。应用监控接口响应时间、QPS、错误率。使用APM工具监控关键链路的调用关系和耗时。业务日志规范日志格式通过ELK等平台集中收集和告警。4. 定期的健康检查与压测为应用提供/health端点集成Spring Boot Actuator等检查数据库连接、缓存连接等关键依赖。定期进行压力测试了解系统的瓶颈和容量边界。最后处理线上假死问题保持冷静和清晰的思路至关重要。按照本文提供的分层排查法从系统到容器从JVM到代码一步步缩小范围你就能像侦探一样从各种线索中揪出导致Tomcat“假死”的真凶。每一次成功的排查都是对系统认知的一次深化。
返回列表