Linux服务器CPU 100%排查实战:从top到jstack定位Java线程死循环

Linux服务器CPU 100%排查实战:从top到jstack定位Java线程死循环 1. 问题引入当服务器CPU飙红时我们该做什么做运维或者后端开发的朋友对下面这个场景肯定不陌生监控大屏突然告警钉钉/企业微信响个不停点开一看——“服务器CPU使用率持续高于95%”。心里“咯噔”一下脑子里瞬间闪过一堆念头是不是被攻击了哪个傻X写的代码又出循环了还是硬件真撑不住了要扩容干着急是没用的重启大法虽然能暂时缓解但治标不治本问题很快又会卷土重来。今天我就结合自己多年踩坑填坑的经验系统性地梳理一遍当Linux服务器CPU占用率冲到100%时一套从现象到根因再到解决的完整排查思路。这不是一篇简单的命令罗列我会带你走完一个真实的排查闭环理解每个步骤背后的“为什么”并分享那些只有踩过坑才知道的“骚操作”和注意事项。CPU高负载本质上是一种“症状”就像发烧一样原因可能千差万别。可能是应用程序的“业务逻辑感冒”如无限循环、低效算法也可能是系统资源的“细菌感染”如大量上下文切换、内存泄漏导致频繁交换。我们的目标就是扮演好“系统医生”的角色通过一系列“体检”手段快速定位到病灶所在。本文将围绕一个核心案例展开一个数据平台服务器的CPU使用率莫名持续在70%以上甚至瞬时触顶98%。我们将一步步抽丝剥茧最终定位到一个看似不起眼的时间工具类引发的性能风暴。文末还会附上经过我实战优化和注释的排查脚本让你拿来即用。2. 整体排查思路与核心原则面对CPU高企切忌无头苍蝇般乱试。一个清晰、高效的排查思路能帮你节省大量时间尤其是在分秒必争的线上故障期间。我的排查哲学可以概括为“先宏观后微观先整体后局部先用户态后内核态”。2.1 排查路径总览一个标准的CPU问题排查通常遵循以下路径我把它画成了一个决策流你可以保存在心里确认症状与影响范围首先通过监控系统或快速登录确认是高了一个核心还是所有核心是持续性的还是间歇性的同时影响的是一台机器还是一个集群这决定了问题的严重性和紧急程度。定位资源消耗主体使用top,htop,atop等工具快速找出是哪个或哪几个进程吃掉了大部分CPU。这是最关键的一步将问题从“系统有问题”缩小到“某个进程有问题”。剖析进程内部状态如果这个进程是Java/Python/Go等应用进程我们需要进一步深入其内部看看是哪些线程在疯狂工作。这里会用到top -Hp,jstack,pstack,perf等工具。关联代码与业务逻辑获得高负载线程的堆栈信息后将其与你的源代码进行关联定位到具体的函数、方法甚至代码行。这一步需要你对业务代码有一定的了解。分析根因并制定方案理解为什么这段代码会导致高CPU。是算法复杂度问题死循环还是外部依赖慢导致的阻塞自旋根据根因制定优化、修复或回滚方案。验证与复盘实施解决方案后持续观察CPU指标是否恢复正常。最后务必进行复盘如何避免同类问题监控是否可以更前置代码审查流程是否需要加强2.2 核心原则与心态在开始具体操作前有几点心态和原则必须明确注意永远不要第一时间想着重启或扩容。重启会丢失现场让你永远不知道问题根源盲目扩容则掩盖了代码或架构的缺陷成本高昂且问题可能在其他地方再次爆发。我们的首要目标是“保留现场定位根因”。原则一监控告警是起点不是终点。告警告诉你“系统不舒服了”但具体哪里疼、为什么疼需要你亲手去诊断。成熟的团队会有更细粒度的监控比如应用层的QPS、接口耗时、错误率JVM内的GC次数、线程池状态等这些信息能极大加速定位。原则二理解工具的输出比记住命令更重要。很多人会背top命令但未必真正理解load average,us,sy,ni,wa,hi,si,st这些字段的含义。理解它们你才能判断问题是出在用户程序、系统调用、IO等待还是虚拟化层面。原则三保持好奇心追问“为什么”。找到高CPU线程后不要满足于“哦是这个函数”。要问这个函数为什么会被频繁调用调用参数是什么是不是有逻辑错误数据量是否异常多问几个为什么往往就能触达真正的根因。3. 第一步宏观定位——哪个进程在“搞鬼”登录服务器我们首先需要一张系统的“全景图”。这里首推top命令它是Linux系统性能分析的“瑞士军刀”。3.1 解读top命令的关键信息直接输入top你会看到类似下面的动态刷新界面。我们重点关注上半部分的汇总信息和进程列表。top - 14:30:01 up 60 days, 20:15, 2 users, load average: 8.42, 7.85, 6.93 Tasks: 231 total, 1 running, 230 sleeping, 0 stopped, 0 zombie %Cpu(s): 98.7 us, 1.0 sy, 0.0 ni, 0.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 32067.2 total, 1520.1 free, ... MiB Swap: 0.0 total, 0.0 free, ...第一行系统概况load average: 8.42, 7.85, 6.93这是平均负载分别代表过去1分钟、5分钟、15分钟的系统平均负载。对于这个8核的服务器1分钟负载达到8.42意味着平均每个核都有一进程在运行并且还有进程在排队0.42系统明显过载。关键点平均负载高不一定代表CPU忙也可能是IO繁忙进程在等待IO。需要结合下面的CPU行判断。第三行CPU状态98.7 us用户空间CPU占用率高达98.7%。这是最直接的信号问题极大概率出在我们自己部署的应用程序上而不是内核或虚拟化层。1.0 sy内核空间占用率1%正常。0.3 idCPU空闲率仅0.3%几乎满负荷运转。0.0 waIO等待为0%说明当前瓶颈不在磁盘或网络IO纯粹是计算密集型问题。如果wa很高则排查方向要转向IO。进程列表默认按CPU使用率降序排列。一眼就能看到排在第一的进程PID比如682CPU使用率可能高达90%以上。记下这个PID。实操技巧在top界面中按1可以展开显示所有CPU核心的单独使用情况方便看是否是单个核心被打满。按P大写可以强制按CPU使用率排序默认就是。按M可以按内存使用率排序有时高CPU伴随高内存可能是频繁GC的Java应用。按c可以显示进程的完整命令行这对于识别具体是哪个应用非常有用。3.2 使用htop获得更友好的视图如果你觉得top的交互不够直观可以安装htop。它提供了彩色显示、树状视图、鼠标支持等对新手更友好。使用sudo apt install htop或sudo yum install htop安装后直接运行htop。高亮的进程条能让你更快定位问题进程。我的经验在紧急情况下我通常先用top快速抓取PID和关键指标因为它是所有Linux发行版的标准配置。如果需要更长时间的分析或向他人展示htop的界面更胜一筹。4. 第二步微观剖析——进程内部的“元凶”线程找到罪魁祸首进程假设是PID 682后我们需要钻进这个进程内部看看是哪些线程在疯狂消耗CPU。这对于Java、Go等多线程应用尤为重要。4.1 定位进程内的繁忙线程继续使用top但这次带上-H参数并指定进程PIDtop -Hp 682这个命令会显示进程682内所有线程的资源使用情况同样按CPU排序。你会看到一系列线程在top中称为任务TID即线程ID。找到那个CPU占用最高的线程记下它的TID例如1073。为什么是线程现代应用都是多线程的。一个进程CPU高可能是其中一个线程陷入了死循环也可能是多个线程都在执行高计算量的任务。定位到线程就离问题代码更近了一步。4.2 为Java应用量身定制的神技jstack如果目标进程是一个Java进程从top命令的COMMAND列通常能看到java那么jstack是分析线程堆栈的终极利器。它可以将线程ID十进制与其正在执行的Java方法堆栈关联起来。但是jstack需要线程ID的十六进制格式。所以我们需要转换printf \0x%x\\n\ 1073 # 输出0x431现在我们获取该Java进程的完整线程堆栈并从中过滤出我们关心的线程jstack 682 | grep -A 20 \nid0x431\jstack 682输出进程682的所有线程堆栈。grep -A 20 \nid0x431\查找包含nid0x431即我们找到的线程的行并打印该行及之后的20行。nid在jstack输出中就是本地线程IDNative Thread ID对应操作系统的线程IDTID。输出示例\http-nio-8080-exec-5\ #32 daemon prio5 os_prio0 tid0x00007f8b3820e800 nid0x431 runnable [0x00007f8b1f7f9000] java.lang.Thread.State: RUNNABLE at java.text.SimpleDateFormat.format(SimpleDateFormat.java:956) at java.text.SimpleDateFormat.format(SimpleDateFormat.java:929) at java.text.DateFormat.format(DateFormat.java:337) at com.example.util.DateUtils.formatTimestamp(DateUtils.java:45) --- 关键行 at com.example.service.ReportService.generateRealTimeReport(ReportService.java:123)看我们成功定位到了问题代码com.example.util.DateUtils.formatTimestamp方法。线程状态是RUNNABLE意味着它正在执行这符合CPU高的特征。踩坑记录jstack在生产环境使用时可能会因为进程繁忙而卡住或者因为权限问题进程用户与当前用户不同而失败。一个更稳健的做法是使用jstack -F强制模式或直接使用sudo -u 进程用户 jstack pid。另外在高并发场景下一次jstack可能不足以捕捉到瞬间状态可以考虑多次执行或结合其他工具。4.3 一站式脚本show-busy-java-threads手动执行top -Hp,printf,jstack | grep这一套组合拳在紧急的线上故障中还是太慢了。阿里巴巴的资深技术专家oldratlee李鼎将这一流程封装成了一个极其好用的Shell脚本——show-busy-java-threads.sh。它的核心价值在于一键化和自动化。脚本核心逻辑自动找出当前系统内CPU消耗最高的几个Java线程。自动获取这些线程的堆栈信息。将堆栈信息清晰地打印出来直接告诉你哪个Java方法最忙。基础用法# 下载脚本 wget https://raw.githubusercontent.com/oldratlee/useful-scripts/master/show-busy-java-threads.sh chmod x show-busy-java-threads.sh # 最简单用法找出最忙的5个线程默认 ./show-busy-java-threads.sh # 找出指定Java进程PID682中最忙的3个线程 ./show-busy-java-threads.sh -p 682 3 # 连续采样10次每次间隔1秒 ./show-busy-java-threads.sh -c 10 -i 1脚本优势省心无需记忆命令序列和ID转换。安全脚本内部处理了用户权限问题尝试用sudo执行jstack。灵活支持指定进程、采样次数、间隔适合分析间歇性问题。清晰输出格式友好直接高亮显示问题线程和堆栈。在我遇到的案例中正是通过这个脚本瞬间就定位到了那个“时间工具类方法”。脚本输出直接指向SimpleDateFormat.format的频繁调用排查效率提升了十倍不止。5. 第三步根因分析与解决方案设计通过线程堆栈我们定位到问题方法是DateUtils.formatTimestamp。但这还不够我们需要理解为什么这个方法的调用会如此频繁以至于打满CPU。这就需要结合业务代码进行分析了。5.1 代码逻辑复盘与问题推演回顾问题案例中的代码逻辑异常方法formatTimestamp。功能是将一个时间戳秒级转换为格式化的日期时间字符串如 “2023-10-27 10:30:00”。内部使用了SimpleDateFormat。上层调用在一个报表查询服务中需要计算从当天凌晨00:00:00到当前时间例如上午10点之间每一秒对应的格式化字符串并将这所有字符串放入一个HashSet中返回。调用规模单次查询如果当前是上午10点那么就需要循环调用formatTimestamp方法10 * 60 * 60 36,000次。如果查询逻辑中还有嵌套循环n次那么总调用次数就是36,000 * n次。业务背景这是一个实时数据平台实时报表和报警模块会以很高的频率例如每秒或每几秒发起这类查询。性能问题根因计算量爆炸单次查询的计算次数随时间线性增长。从凌晨到中午计算量增长数万倍。这本身就是一个O(n)且n很大的操作。对象创建开销虽然代码中没有体现但SimpleDateFormat的format方法内部涉及大量的字符串操作和临时对象创建在超高频率调用下会给GC带来巨大压力进一步加剧CPU消耗。资源浪费最关键的一点上层业务逻辑仅仅使用了返回的Set集合的size()即只关心从凌晨到当前有多少秒而根本不需要那几万个格式化的字符串本身5.2 解决方案从“治标”到“治本”找到了“资源浪费”这个关键点解决方案就呼之欲出了。方案一紧急优化治标既然只需要秒数那么完全不需要进行昂贵的日期格式化操作。计算从凌晨到当前的秒数是一个简单的数学问题// 优化后的方法 public static int getSecondsSinceMidnight() { long now System.currentTimeMillis() / 1000; // 当前秒级时间戳 long todayMidnight now - (now % 86400); // 计算当天凌晨的秒级时间戳 return (int)(now - todayMidnight); }将上层调用中复杂的循环格式化逻辑替换为对这个简单方法的调用。计算复杂度从 O(n) 降为 O(1)CPU消耗立竿见影地下降。方案二深入优化治本缓存与复用如果其他地方确实需要格式化字符串考虑缓存SimpleDateFormat对象注意线程安全可以用ThreadLocal避免频繁创建。或者使用性能更好的时间库如java.time包下的DateTimeFormatter线程安全。算法优化审视业务逻辑是否存在类似的不必要的循环或重复计算能否用更高效的算法或数据结构替代架构审视实时报表是否需要精确到秒级的全量数据能否采用滑动窗口、增量计算或预聚合的方式高频率的实时查询是否对数据库造成了压力方案三防御性措施限流与降级在服务层面对高消耗的查询接口实施限流防止异常流量打满资源。监控告警前置不仅监控系统级CPU更要在应用层监控关键方法的调用频率和耗时。当formatTimestamp方法的QPS异常升高时就应该触发告警而不是等到CPU打满。代码审查将性能意识纳入代码审查标准。对于循环内调用复杂方法、频繁创建重量级对象如SimpleDateFormat、存在明显浪费的代码在合并前就应提出质疑。在我们的案例中采用了方案一进行紧急修复上线后服务器CPU使用率从峰值98%骤降至3%左右效果极其显著。6. 进阶排查当问题不在Java进程时上述流程完美解决了用户态Java应用的问题。但CPU高的原因远不止于此。下面罗列其他常见场景及排查工具。6.1 系统调用sy过高如果top显示sy系统CPU使用率很高说明内核态很忙。常见原因频繁的系统调用例如网络服务中大量的epoll_wait或文件服务中大量的read/write。进程/线程频繁创建销毁例如不合理的“每个请求一个线程”模式。内存分配频繁例如Java应用虽然us高但频繁的GC会导致sy也升高。排查工具perf top实时查看系统中哪些内核函数或用户函数消耗CPU最多。这是最强大的性能剖析工具之一。strace -cp pid统计指定进程发起的系统调用类型和次数。可以快速发现异常频繁的调用。vmstat 1查看上下文切换cs列频率。如果每秒上下文切换次数过高例如上万次说明系统在进程/线程调度上花费了太多时间。6.2 IO等待wa过高如果wa很高说明CPU在等待磁盘或网络IO。瓶颈在IO子系统。磁盘IO数据库慢查询、大量日志写入、备份任务等。网络IO远程调用超时、网络拥堵、遭受流量攻击等。排查工具iostat -x 1查看各磁盘的利用率%util、响应时间await、读写速率。%util接近100%说明磁盘饱和。iotop类似top但是针对磁盘IO可以看到哪个进程读写最频繁。sar -n DEV 1查看网络接口的吞吐量rxkB/s,txkB/s和错误包数量。6.3 软中断si或硬中断hi过高这通常与网络处理有关。网络数据包到达后会触发硬中断然后由软中断如ksoftirqd内核线程进行后续处理。如果网络流量巨大软中断处理会消耗大量CPU。排查工具watch -n 1 \cat /proc/softirqs\观察各种软中断类型的计数变化。NET_RX和NET_TX的快速增长指向网络问题。sar -n DEV 1结合ifconfig查看是否有某个网卡流量异常。nethogs查看每个进程的网络带宽占用情况。6.4 僵尸进程与孤儿进程top中如果看到zombie数量不为0说明有僵尸进程。僵尸进程本身不消耗资源但过多可能表明父进程没有正确回收子进程需要关注。ps aux | grep defunct或ps -ef | grep \defunct\查看僵尸进程。解决方法是找到其父进程PIDPPID并重启或修复父进程。7. 实战脚本详解与自定义增强工欲善其事必先利其器。show-busy-java-threads.sh脚本是排查Java应用CPU问题的利器理解其原理并能根据自身环境定制会让你如虎添翼。7.1 脚本核心逻辑拆解脚本的主体逻辑清晰主要分为几个函数check_java_command检查jstack命令是否可用如果不在PATH中则尝试使用JAVA_HOME环境变量。printStackOfThreads核心函数。它接收一个经过排序的线程列表包含PID、LWP线程ID、用户、CPU占用率然后为每个繁忙线程获取并打印其Java堆栈。它巧妙地为每个Java进程只执行一次jstack将结果缓存到临时文件/tmp/${uuid}_${pid}然后从这个缓存文件中用sed提取特定线程的堆栈。这避免了为每个线程重复执行jstack的开销。它处理了运行用户与当前用户不同的情况尝试使用sudo来执行jstack并在失败时给出清晰的提示。主流程使用ps -Leo pid,lwp,user,comm,pcpu命令列出所有线程过滤出Java进程的线程按CPU使用率排序取前N个交给printStackOfThreads处理。7.2 根据自身环境定制脚本原脚本已经非常强大但在实际使用中我通常会做以下增强1. 适配容器化环境 现在很多应用跑在Docker或K8s里。你可以在宿主机上运行这个脚本但需要先找到容器的PID在宿主机上的对应PID即容器的1号进程在宿主机的PID。# 找到某个Java容器的宿主机PID docker inspect --format \{{.State.Pid}}\ container_name_or_id # 然后使用 -p 参数指定该PID ./show-busy-java-threads.sh -p host_pid更进阶的做法是将脚本拷贝到容器内执行但需要注意容器内可能没有perl等依赖原脚本用bash和awk一般都有。2. 集成到监控系统 可以修改脚本使其不直接输出到终端而是将分析结果如Top 5繁忙线程的堆栈摘要格式化成JSON或特定格式发送到监控系统如PrometheusGrafana, ELK或告警平台如钉钉、飞书机器人实现自动化的异常检测。3. 增加历史采样与对比 对于间歇性、难以捕捉的CPU毛刺可以写一个包装脚本定时如每10秒执行一次show-busy-java-threads.sh -c 5并将输出按时间戳保存到文件。当CPU告警触发时可以回头查看历史采样文件分析毛刺时刻的线程状态。4. 支持非Java进程 脚本核心是jstack所以只适用于Java。我们可以借鉴其思路为其他语言编写类似工具。例如对于Python的py-spyNode.js的0x或clinicGolang的pprof。思路都是top -Hp找线程 - 用语言特定的工具转储堆栈 - 分析。8. 总结与心法从救火到防火排查CPU 100%的问题技术上有清晰的路径和工具。但比技术更重要的是建立一套预防和快速响应的体系。心法一建立可观测性Observability。不要只满足于基础的系统监控CPU、内存、磁盘。要建设应用性能监控APM能够追踪每一次请求的完整调用链看到每个方法的耗时、数据库查询的慢SQL、外部调用的性能。当CPU高时APM能直接告诉你哪个服务、哪个接口、哪个方法出了问题将排查时间从小时级降到分钟级。心法二性能测试与容量规划。在上线前对核心接口和场景进行压力测试了解系统的性能拐点和资源消耗模型。基于此进行合理的容量规划设置安全水位线如CPU超过70%告警避免资源耗尽才被发现。心法三代码文化与复盘机制。将性能优化意识融入团队文化。代码审查时多问一句“这段代码在高压下会不会有问题” 每次线上故障解决后必须进行正式的复盘Blameless Postmortem不仅找出直接原因更要挖掘流程上的改进点是监控缺失、测试不足还是设计缺陷心法四熟练使用你的工具箱。本文提到的top,htop,jstack,perf,strace,iostat,vmstat以及show-busy-java-threads.sh这样的集成脚本都是你工具箱里的宝贝。平时多练习在测试环境模拟各种故障场景如注入一个死循环真正遇到问题时才能沉着应对。CPU高负载排查是一场与时间的赛跑也是一次对系统认知深度的考验。从宏观指标到微观代码从紧急止血到根因治理每一步都考验着工程师的综合能力。希望这篇长文提供的不仅是“鱼”具体的命令和脚本更是“渔”系统的排查思路和心法。下次当告警再次响起希望你能从容不迫精准定位快速解决。