尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux高CPU线程定位:从top -Hp到jstack/perf完整套路

Linux高CPU线程定位:从top -Hp到jstack/perf完整套路 做服务器维护和后台开发的人几乎都遇到过这种场景一台Linux机器负载突然飙到十几业务方火急火燎地说接口超时你登上服务器敲了一个top看到某个进程的CPU占用已经200%多然后呢然后大家就只能盯着进程名反复猜。说实话光知道进程名在绝大多数情况下一点用都没有。CPU是进程里的线程消耗的不看到线程级别你连方向都找不准。这篇就记录一下我平时定位Linux高CPU线程的完整套路从一条命令快速锁定线程ID到配合jstack、perf这些工具把问题钉死在代码行希望能帮刚开始接触Linux性能排查的朋友少走点弯路。1. 排查前先理清两个基本概念1.1 进程和线程在Linux里是怎么表示的Linux内核不像Windows那样把进程和线程完全分开管理它统一用task_struct表示调度单位。线程本质上是clone()创建出来的特殊进程只不过和同组的进程共享了地址空间、文件描述符、信号处理等资源。所以你会发现ps命令里既能看PID也能看TID这两个数字在同一个进程里经常不一样。用一个生活化的比喻进程是公司线程是员工。公司对外只有一个统一名称但公司内部真正在干的活是由一个个员工完成的。CPU调度器去分配时间片盯的是“员工”而不是“公司”。top默认状态下把所有员工打包成一个公司来显示所以你知道某个进程CPU高但不知道是哪个员工在摸鱼只有切到线程模式才能把内部的员工挨个抓出来。具体到内核参数线程组有一个tgid概念进程的主线程的TID等于进程PID后续创建的线程会得到新的TID。这也是为什么排查线程时千万不要拿线程ID直接当进程ID用。理解了这一点后面所有命令都不会绕晕。1.2 先分清CPU时间到底花在哪儿在动手抓线程之前建议先看一眼top头部那行统计。us、sy、ni、id、wa、hi、si、st这串缩写看起来很难记其实只需要重点关注前两个顺手扫一眼wa和st。指标含义常见方向us用户态CPU占用业务代码死循环、计算密集、GC频繁sy内核态CPU占用系统调用过多、锁竞争、内核模块异常wa等待I/O的CPU占比磁盘慢、存储抖动优先查I/O而非线程st被虚拟机偷走的时间云主机/容器宿主机超卖先排查宿主机us高说明CPU在跑用户态代码九成是业务逻辑问题比如循环没退出、反序列化过重、线程池被错误使用。sy高说明CPU花在内核态常见的是系统调用频繁、futex锁竞争、网络中断打满这时候光去抓业务线程栈是不够的反而要辅助strace或者perf去看内核路径。wa高则是CPU在等I/O通常先查磁盘读写和存储负载而不是急着找线程。st高说明你的虚拟机在排队等宿主机调度这种情况单看云服务器内部意义不大。先定性再往下钻能省下大量时间。2. 最快锁死线程ID用top -Hp和ps组合2.1 用top -Hp直接看线程CPU占用最直接的方式就两步。第一步先执行top -bn1或者直接top找出出问题的进程PID按P键让进程按CPU占用排序。第二步拿到PID之后执行下面这条命令top -Hp 12345-H表示Threads模式-p指定进程。执行完你会发现画面变了标题行从Tasks变成了Threads列表里每一行都是一个线程。这时候再按一下P键让线程按CPU使用率从大到小排第一眼看到的那一行就是嫌疑最大的线程。注意一个特别容易踩的坑在top -Hp的输出里第一列的标题仍然是PID但它实际上已经是TID了。比如你看到某个线程的PID是23456这个23456并不是一个真实存在的进程拿ps -p 23456去查大概率什么都查不到别拿着这个号到处找进程。另外COMMAND列默认只显示线程名一些Java线程名字很长按c键可以切换显示完整命令行。如果不想交互式操作也可以直接用批处理模式top -H -b -n 1 -p 12345 | head -25这个用法非常适合写脚本一次性采样输出一行行线程数据。2.2 用ps做线程级快照top是交互式的在脚本里解析它要多费不少功夫。这时候用ps更稳定格式也更容易控制。我最常用的两条命令如下ps -eLo pid,tid,pcpu,comm --sort-pcpu | head -20 ps -p 12345 -L -o tid,pcpu,comm --sort-pcpu | head -20第一条看的是全系统里所有线程的CPU占用排序适合一开始只知道机器整体CPU高、但不知道是哪个进程出了问题的情况。第二条针对指定进程列出其所有线程并按CPU占用排序。pcpu字段就是线程的CPU使用百分比脚本里直接拿这一列排序就行。要注意不同版本的ps对字段名的支持略有差异个别老发行版上如果报pcpu不可用改成%cpu试试。另外ps默认输出的COMMAND字段也可能被截断可以先加上-c参数强制展示进程可执行名或者干脆用comm字段简单场景下足够用了。2.3 把线程ID转成十六进制给Java栈用很多人排查Java进程卡在最后一步就是拿着十进制的TID去grep jstack半天查不到因为jstack输出里的nid是十六进制。转换非常简单printf 0x%x\n 23456比如23456转出来是0x5ba0。拿到这个值后面jstack里直接grep nid0x5ba0就可以了。如果你习惯用Python也可以执行python3 -c print(hex(23456))。这个动作虽然基础但确实是排查Java线程CPU问题时最高频的桥接步骤顺手记下来能节省很多时间。3. 从线程ID到代码栈三种场景的进阶打法3.1 Java应用jstack一把梭Java进程是最常碰到CPU飙升问题的场景。常规流程是先用top -Hp抓出占用最高的TID转成十六进制然后执行jstack把线程栈dump下来分析jstack -l 12345 /tmp/jstack_12345_$(date %s).txt grep -A 30 nid0x5ba0 /tmp/jstack_12345_*.txt如果jstack报错Unable to open socket file大概率是进程身份或JDK版本问题。竞争JDK9以后更推荐用jcmdjcmd 12345 Thread.print -l /tmp/thread_12345.txt注意jstack执行时会让JVM短暂停顿线上大压力下要快速执行不要反复折腾。dump下来之后重点看nid对应的线程当前栈顶和线程状态。同一个线程多次dump栈顶都落在某个用户方法上基本就是循环或计算热点栈顶在native方法里就要继续上升到perf层面看是哪个本地函数。如果发现线程名是G1 Young RemSet Sampling Thread或者CMS的并发标记线程CPU高不一定就是问题可能只是GC设置不合理。我之前遇到过一台服务CPU飙到接近300%定位下来是G1的Remember Set线程在疯狂执行最后通过调大堆内Region大小和调整GC参数解决。这类问题要结合GC日志别一上来就拍脑袋调堆。3.2 C/C及原生线程读/proc和用pstack/gdb非Java进程线程名一般可以直接从/proc下读cat /proc/12345/task/23456/comm cat /proc/12345/task/23456/statusstatus里的Name字段就是线程名。很多服务会把业务线程设置成有意义的名字比如nginx的worker线程、MySQL的page cleaner线程一看名字就能猜到大概职责。如果线程名看不出问题就得抓调用栈。轻量一点的方式是用pstack但pstack显示的是进程内所有线程的调用栈输出量很大需要配合grep去捞目标线程附近的内容。更可靠的是gdbgdb -p 12345 -batch -ex thread apply all bt /tmp/gdb_stack.txtgdb输出里每个线程会带LWP号这个LWP就是TID可以直接和top -Hp里的数字对照。但gdb attach一个正扛大流量的进程会让进程短暂停顿甚至触发超时报警生产环境使用前一定要评估风险。还有一种低侵入的方式读/proc/PID/task/TID/wchan和syscall文件能快速看到线程当前在内核里等什么适合判断线程是不是卡在某个系统调用上。3.3 热点函数perf和strace辅助定位如果栈顶在native方法或者C程序里线程名和调用栈都有了还是不清楚CPU到底消耗在哪我会请出perf。最常用的是实时热点perf top -p 12345这个命令能直接看到进程内CPU占比最高的函数符号。如果提示权限不足多半是kernel.perf_event_paranoid设置得比较严格需要root或者临时调小。如果要留证据用perf record加perf reportperf record -p 12345 -g -- sleep 30 perf report-g参数会记录调用关系非常适合分析C/C复杂项目里CPU热点到底是从哪个函数一路调过来的。另一个工具strace主要跟踪系统调用strace -p 23456 -c -f-c参数是在结束时打印系统调用统计-f表示跟踪子线程。不过strace会明显拖慢目标进程我一般只在进程CPU高且伴随大量系统调用时才用而且最多跑几十秒。遇到sy高的情况strace能很快告诉你它到底是在read、write、epoll_wait还是futex上折腾方向一下就出来了。4. 把定位动作脚本化防止现场一闪而过4.1 一屏看全一行命令打出TOP线程CPU问题往往不是稳定复现的等你在终端慢悠悠敲命令现场可能已经变了。我习惯把定位动作固化成脚本随手一敲就出结果。一个简化版脚本是这样#!/bin/bash PID$1 if [ -z $PID ]; then echo Usage: $0 pid exit 1 fi echo top threads by CPU top -H -b -n 1 -p $PID | tail -n 8 | awk {print $1, $9, $NF} | sort -k2 -nr | head -20 echo ps detail ps -p $PID -L -o tid,pcpu,comm --sort-pcpu | head -20top的批处理模式-b一次采样就退出-n 1控制只执行一次。tail -n 8是为了跳过头部那些统计信息。不同Linux发行版的top头部行数可能不一样如果脚本里输出对不齐建议直接信任ps那一行结果。实际排查中ps的pcpu排序已经足够用了。4.2 循环记录和定时保留现场脚本只解决单次定位不解决持续追踪。遇到CPU反复飙高的情况把采样放到循环里每几秒记一次while true; do echo $(date %F %T) top -H -b -n 1 -p 12345 | tail -n 8 | sort -k9 -rn | head -15 sleep 5 done /tmp/cpu_thread_12345.log这里有两点要提醒。采样间隔别太短1秒一次持续跑会给本来就不宽裕的机器增加额外负担。日志文件别一直写不滚动最好配合logrotate或者自己定期清理。如果你已经定位到目标TID还应该在同一时间戳下把线程栈也dump一份比如每10秒执行一次jstack。采样日志和栈文件对应上之后分析时才能还原出CPU高和代码路径之间的因果关系而不是靠猜。5. 躲过这些坑你就是排查老手5.1 线程ID和进程ID分不清白忙一场最常见的问题就是把top -Hp输出的PID当成进程ID去处理。这里再次强调线程模式下第一列是TID不是PID。另外top默认显示的进程CPU占用是把进程内所有线程CPU时间加总后的结果所以一个进程可能显示300%但没有一个单线程能超过100%。反过来说如果只看进程总CPU在机器核数很多时负载高不一定代表某一个线程异常也可能是整体流量涨了。排查时先和监控历史曲线对一下确认CPU是持续升高还是突发尖峰再决定要不要深入线程。5.2 线程跑得太快抓不到换个姿势采样CPU占用是时间片采样出来的一个线程可能一会儿99%、一会儿0%恰好在你敲top那一刻落在低点上就被排到后面去了。这种情况我改用pidstat做持续统计pidstat -t -p 12345 1 10这个命令每秒输出一次连续10次并显示每个TID的用户态CPU和内核态CPU占用。看累计值比看单次快照可靠得多。如果系统没有pidstat装一下sysstat包即可CentOS和Ubuntu都能直接用yum或者apt安装。还有个偏方把top的刷新间隔调大一点比如top -d 3 -Hp让它每3秒刷新一次多盯几轮线程的规律也会慢慢浮出来。5.3 容器场景和权限问题容器里排查比宿主机麻烦不少。首先容器内执行top看到的是容器自己的PID namespace和宿主机看到的PID可能完全不同。所以要在容器内用容器内PID执行jstack如果容器里没有JDK工具就要回到宿主机找到容器进程在宿主机里的PID再结合nsenter或者docker top去处理。其次attach操作经常会遇到权限限制比如kernel.yama.ptrace_scope设为1时普通用户不能attach其他进程执行jstack或gdb会直接报Permission denied。临时放开需要root但改内核参数属于高危操作最好只在一次性诊断时使用用完立刻改回。5.4 定位完成之后的几个处理方向找到元凶线程后别急着kill。线程不是一个独立进程kill线程的办法通常是把整个进程杀掉线上这么干基本就是事故。正确的做法是区分问题类型。如果是业务线程死循环或者异常重试保留dump让代码owner去修临时处理可以平移流量后重启服务。如果是GC线程调GC参数前先看GC日志确认是对象分配压力大还是内存碎片问题。如果是线程池里的活跃线程被下游依赖拖住下一步应该去查线程池大小、队列积压和下游超时配置。定位到这一步排查工作才算真正结束剩下的交给修复和验证。最后说一点我自己实操累积下来的体会。我现在的排查习惯是先用top看整体再用top -Hp锁定嫌疑TID同时起一个pidstat持续记录现场最后根据语言栈选择jstack或perf收尾。这套组合动作在绝大多数CPU尖峰场景里都能扛住。还有个小技巧抓到TID后先存一条带时间和CPU数值的记录别急着分析节奏太快多采几轮。CPU问题很会变脸手头有完整的数据链比什么都强。
返回列表