资讯详情

资讯详情

Linux查看线程:从CPU飙高到锁定罪魁线程的完整排查指南

先说一个我经常碰到的场景告警群里突然刷出一条消息某台4核机器上的Java进程CPU吃到了接近400%。我登录上去第一反应是执行ps -ef | grep java结果只看到孤零零的一行Java进程没有线程也没有实时CPU列。进程明明忙得不行但我连里面哪条线程在忙都看不出来。Linux如何查看线程这个问题在书本上是个概念题在线上它就是一道生死题。这篇内容是我结合多次实际排查经历整理出来的。我会把最常用的几条查看线程命令、它们在输出上的关键区别、内核里线程到底是怎么表示的原理以及一套从CPU飙高到锁定罪魁线程的完整排查链路全部讲清楚。无论你是刚接触Linux的运维还是写后端写了几年的开发只要平时需要跟进程打交道这篇文章都值得认真看一遍。1. 为什么ps -ef里看不到线程先补上进程与线程的分界认知1.1 一次CPU告警引发的困惑先说回开头那个场景。同事执行ps -ef | grep java之后看到的是类似这样的输出root 2300 2299 0 14:00 ? 00:02:11 java -Xmx2g -jar order-service.jar只有一条记录PID是2300。ps -ef是静态快照它根本不展示实时CPU占用率更不会把进程内部的线程拆成多行显示。于是我们从知道进程有问题到看出是哪个线程有问题中间断了一截。很多人到这里就卡住了只能去重启或者让开发加日志然后反复试。这个困惑的本质不是命令不够多而是我们默认了进程就是可以被观察的最小单位。在Linux上这个默认是错误的。1.2 线程不是进程的子进程Linux内核对待进程和线程的态度和Windows差异很大。在Linux里线程实际上也对应一个任务task内核同样用一个task_struct结构体来管理它。线程是通过clone()系统调用创建的只不过创建时带了CLONE_THREAD、CLONE_VM、CLONE_FILES这些标志位表示要和创建它的那个任务共享地址空间、文件描述符表、信号处理函数等资源。因为共享了这么多东西我们才把这些内核任务叫做线程。而英文资料里更准确的说法是轻量级进程LWPLight Weight Process。所以线程在Linux内核眼里并不是进程的儿子而是和你平级、但共享家产的另一张身份证。主线程的身份证号恰好和进程号一样其他线程的身份证号则各不相同。用生活类比来说进程是一个办公室线程是办公室里各干各活的员工。ps -ef就像前台那张访客登记表只记录哪个办公室有人来过不会把办公室里每个人每天做了什么细节都登记上去。想看具体员工名单你得换一份更细的表格这就是后面几节要讲的命令。1.3 会查线程是解决性能问题的分水岭进程层面的信息太粗了。一个应用开了50条线程其中只有1条在死循环单核CPU就能被吃到100%。从进程层面看你只能得出这个进程很忙的结论完全看不出是50条线程都在忙还是1条线程在独奏。如果不具备查看线程的能力后续所有排查都只能靠猜。尤其像JVM这种多线程大户GC线程、线程池线程、定时器线程、网络IO线程混在一起动辄几十上百条。没有查看线程的能力CPU飙高时你只能盯着进程号发呆。会从进程走进线程是我认为Linux性能排查的第一个分水岭过了这个坎后面很多手段才能真正落地。2. 四条命令四种看法top -H、ps -eLf、htop与pidstat -t实测对比2.1 top -H最快看到进程内部的线程如果你已经定位到有问题的进程PID最快的下一步就是top -H -p 2300这里的-H让top进入线程模式。进入之后原来显示进程PID的那一列此刻显示的是每个线程的TIDThread ID每个线程单独占一行包括主线程。默认按CPU%从高到低排序所以最耗CPU的线程通常出现在最顶部。操作细节上要注意进入top后再按一下H键可以在进程视图和线程视图之间切换这个交互快捷键在有些终端环境下容易误触脚本或远程排查时更推荐直接带-H参数。如果想看某个进程内部的总线程数可以按f键进入字段管理把NLWP那一列选出来显示它表示该进程当前拥有的线程数量。需要注意的是top默认情况下单线程的CPU%在IRIX模式下可能显示超过100%按I键可以切换成Solaris模式单线程最大显示100%。实际排查时重点看哪些TID显著偏高相对值比绝对值更重要。2.2 ps -eLf把系统里所有线程摊开ps -eLf是另一个高频命令ps -eLf | grep 2300它把系统里所有进程的所有线程都铺开展示。输出里最关键的是三列PID进程ID也就是主线程所在的那个进程号。LWP线程ID轻量级进程ID这才是线程的真名。NLWP这个进程一共拥有的线程数量。还有一个容易被忽略的C列它是内核调度器维护的一个粗略CPU利用率不是top那种实时刷新值参考意义有限。想看实时CPU还是用top或者接下来要说的pidstat更靠谱。如果只想看某一个进程更精简的写法是ps -L -p 2300 -o pid,tid,pcpu,comm这里%CPU也是进程启动以来的平均CPU占用不是瞬时值。ps -T -p 2300的效果和ps -L -p 2300类似都属于指定进程线程列表的专用形态。用ps -eLf的好处是脚本友好输出可以直接用管道处理比如快速过滤某个进程的所有线程。2.3 htop带树的线程视图htop是top的增强版默认不一定安装需要根据发行版自行安装。它最大的优点是可视化按F5进入树形视图后同一个进程下面的线程会以子节点的形式展开层次关系一眼就能看清。进入htop后默认视图其实会混着显示进程和线程可以在设置里把线程展示调整为更舒服的模式。F4支持按关键字过滤F6可以选择排序字段。当线程数量特别多的时候htop的树形展示比top的一屏列表直观得多尤其在给同事或领导解释问题现场时一张htop截图比一段top文字输出更有说服力。不过htop这样的交互式工具不太适合自动化脚本更多是给人看的。2.4 pidstat -t适合持续采样的线程统计如果你需要长时间观察线程的CPU波动pidstat是更专业的选择。它来自sysstat包需要先安装。用法pidstat -t -p 2300 1输出中会多出TGID和TID两列。TGID是线程组ID其实就是进程PIDTID是线程ID。后面的%CPU是采样周期内该线程的实时CPU占用比ps的累计平均值更有参考价值。把间隔和次数加进命令就可以边采集边记录pidstat -t -p 2300 5 10 /tmp/thread_cpu.log这会在50秒内采样10次每次间隔5秒落盘后可以慢慢分析。2.5 四个命令到底怎么选命令适用场景输出粒度优势注意点top -H -p PID线上快速定位某进程内CPU最高的线程单进程内线程实时刷新、自带排序交互界面脚本化不便ps -eLf全量快照、脚本统计、排查线程总数系统全部线程静态完整方便管道处理不刷新CPU列非实时htop可视化查看线程树、向人解释现场系统全部进程/线程树形结构直观需要额外安装pidstat -t持续采样线程CPU、生成趋势数据指定进程内线程可脚本化适合留证据需要sysstat包我的选择逻辑很直接临时看现场用top -H -p留证据、做统计用ps -eLf需要在窗口里反复观察和解释用htop要采集线程CPU趋势数据就用pidstat -t。没有什么最强命令只有匹配场景的命令。3. 内核视角的线程档案TID、tgid还有/proc/PID/task这个宝藏目录3.1 每个线程都有一张独立的身份证在Linux内核里每个任务进程或线程的task_struct中都有两个容易混淆的字段pid和tgid。pid字段当前任务自己的ID。对线程来说这就是线程ID也就是TID。tgid字段线程组的ID也就是主线程的进程PID。对于主线程pid和tgid是相等的所以你会看到主线程的TID恰好等于进程PID。用户态调用getpid()返回的是tgid而gettid()返回的才是线程真正的pid字段。这就是为什么很多命令默认展示进程时你能看到的那一行其实是主线程非主线程因为身份证号和进程号不同默认就不展示。下面的例子能帮你建立直觉进程PID2300内部有4条线程它们的TID可能是2300、2301、2302、2303。主线程TID正好是2300其余三条线程的TID都是独立编号。理解了这一层再看ps -eLf里PID列和LWP列的关系就不会觉得奇怪了。3.2 /proc/PID/task文件系统里的线程目录Linux把大量内核状态暴露在 /proc 伪文件系统里。每个进程的线程信息就躺在/proc/PID/task/目录下ls /proc/2300/task你会看到像2300 2301 2302 2303这样的数字目录每一个都代表一条线程。进入某个线程目录你能看到它的完整状态cat /proc/2300/task/2301/comm cat /proc/2300/task/2301/statuscomm文件里是这个线程的名字status里则有这个线程的运行状态、内存信息等。想知道进程总共有多少线程更快的命令是grep Threads /proc/2300/status输出类似Threads: 4一目了然。3.3 一条脚本把线程ID和线程名对应起来/proc这种方式在写脚本时特别好用因为它不需要额外fork一个ps进程去解析文本。下面这条命令可以列出某个进程的所有线程ID和线程名for tid in /proc/2300/task/*; do echo $(basename $tid) $(cat $tid/comm) done如果进程线程非常多想按CPU时间排个序可以借助线程的stat文件。注意/proc/PID/task/TID/stat的第14个字段是utime即用户态累计CPU时间可用作粗略排序依据for tid in /proc/2300/task/*; do printf %s %s %s\n $(basename $tid) $(cat $tid/comm) $(awk {print $14} $tid/stat 2/dev/null) done | sort -k3 -nr | head -20这里有一个小坑如果线程名里含有空格awk取字段时会发生偏移。我的建议是把线程名都设置成不含空格的单词这本身也是好的命名习惯。提示/proc/PID/task下每个数字目录就是一条线程目录项数量减去特殊情况下的开销基本就等于进程线程总数。如果你发现实际线程数明显大于你配置的线程池数量那多半是有线程创建后没有被回收这就是线程泄漏的前兆。4. CPU飙高400%之后从PID到TID再到jstack的完整排查流程4.1 第一步top先锁定进程PID假设现场是4核机器执行top后看到Java进程PID 2300CPU占用约375%。此时先不要急着杀进程第一步是把PID记录下来。接下来所有操作都围绕这个PID展开。4.2 第二步top -H -p 定位线程TID执行top -H -p 2300输出里原本的PID列变成了TID列。我盯着看了一会儿发现TID 2347的CPU一直稳定在100%左右而其他线程基本都在个位数徘徊TIME列也在持续增长。到这个程度嫌疑线程基本就锁定在2347了。这里说一个经验当某个线程的CPU长期稳定在接近100%其他线程都相对安静时优先怀疑这条线程是死循环或者密集计算如果所有线程CPU都不高但进程总CPU高那有可能是线程频繁创建销毁或者有大量短生命周期任务在排队。4.3 第三步把TID转成十六进制很多人在这一步卡住明明在top里看到了TID是2347但Java线程转储文件jstack里全是nid0x92b这种十六进制值二者对不上。其实只需要一次进制转换printf %x\n 2347输出是92b。匹配的时候要在前面补上0x也就是jstack里的nid0x92b。这个步骤为什么必须做因为JVM的线程转储规范就是用十六进制表示线程ID的你不转换就永远无法在jstack里精准定位。4.4 第四步jstack转储按nid匹配线程栈执行线程转储并把结果保存下来jstack 2300 /tmp/tstack.log然后用grep抓取目标线程的栈段grep -A 20 nid0x92b /tmp/tstack.log这段输出会告诉你这个线程的线程名以及它此刻的Java调用栈。例如栈里如果反复出现业务代码里的某个类名和方法名那基本就是元凶所在如果栈停在类似网络IO的native方法上通常只是正常的等待线程CPU占用不太可能长期100%。对照栈帧回到代码里定位效率会非常高。4.5 Java之外的场景怎么办jstack是Java专属工具C/C、Go或者其他语言怎么办同样思路只是工具不同pstack 2300打印进程所有线程的调用栈比gdb attach更轻量。gdb -p 2300进入后执行thread apply all bt打印所有线程栈。注意gdb attach会短暂暂停进程线上环境要谨慎。perf record -t 2347 -g -- sleep 5按线程ID采样随后perf report查看热点函数调用链。strace -p 2347跟踪指定线程的系统调用但会让该线程明显变慢适合在低峰期或测试环境使用。不管什么语言方法论是一致的先通过top找到进程PID再用某种线程视图找到异常的TID最后用该语言生态对应的工具把线程栈、热点或者系统调用捞出来建立TID → 线程上下文 → 代码逻辑的映射。5. 线程名、线程数与持续采样容易踩的坑和拿来即用的脚本5.1 线程名到底怎么查很多人在top -H里看到的COMMAND列清一色是进程名比如全都是java这是不是bug不是。很多线程根本没有设置自己的线程名内核就会拿进程的可执行文件名或者进程名作为它的comm。想看真实的线程名两个办法cat /proc/2300/task/2347/comm在应用代码里也强烈建议主动设置线程名。C语言可以用pthread_setname_np()注意Linux下线程名最多15个字节Java在较新版本里可以通过Thread.ofPlatform().name(worker-1).start(...)这种方式来命名。线程名一旦设置好配合top -H或pidstat -t一眼就能看出是哪个业务线程在消耗CPU排查成本会直线下降。5.2 统计线程数时容易踩的两个坑统计系统线程总数时如果直接写ps -eLf | wc -l结果会多算1因为第一行是表头。想统计单个进程的线程数更稳的方式是ps -L -p 2300 --no-headers | wc -l或者直接看ps -L -p 2300 -o nlwp的NLWP列。还有一个细节/proc/PID/task目录里的数字子目录就是一条条线程用ls /proc/2300/task | wc -l统计也是可靠的。线程泄漏的排查思路也可以从这里切入定时采集进程的NLWP如果这个数字随时间单调上涨且不回落大概率是线程创建后没有正确关闭最终会打到操作系统的线程数上限导致新线程创建失败。想快速找到系统里线程大户ps -eLo pid,lwp,nlwp,comm --sort-nlwp | head -20这条命令按NLWP降序排列线程数最多的进程会排在最前面是排查线程泄漏时常用的第一筛。5.3 持续采样的正确姿势单次top -H只能看到当前瞬间如果要评估一个线程的CPU趋势要持续采集pidstat -t -p 2300 5 10 /tmp/thread_cpu_$(date %Y%m%d).log采样结束后可以用awk按TID维度汇总看看哪些线程反复出现在CPU消耗名单里。需要注意的是不同版本pidstat的输出列位置有差异写解析脚本前先跑一条看准列号再动手。这种来自线上真实数据的日志比事后的我感觉靠谱得多。5.4 一个小脚本替代反复截top除了前面按utime排序的脚本我还会用下面这条命令把某个进程所有线程的当前状态存成快照for tid in /proc/2300/task/*; do echo TID:$(basename $tid) NAME:$(cat $tid/comm 2/dev/null) STATE:$(awk {print $3} $tid/stat 2/dev/null) done这里的STATE是内核里的线程状态R表示运行S表示睡眠D表示不可中断睡眠等。把它存到文件里隔几秒再存一次对比状态变化比反复敲top然后人肉记忆要可靠得多。最后再说一点我个人的体会排查线程问题我的顺序永远是先想清楚我要看什么是看线程的CPU占用还是看线程在等什么锁还是看线程数有没有泄漏还是看线程名是什么目的不同选用的工具完全不同。top -H -p解决谁在抢CPUps -eLf解决全量快照和线程数/proc/PID/task解决脚本化细看每个线程jstack和perf解决这个线程到底在干什么。把这些串起来就形成了一条从进程到代码层面的完整链路。下次再遇到CPU飙高别再盯着进程层面发愁了线程就在那里等着你去查。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →