接到线上告警的那一刻,我通常不会先打开监控大盘,而是直接登服务器敲命令:top看一眼整体,vmstat盯两秒钟,再用pidstat定位到具体线程。这套“三连”动作,对于全栈工程师来说,应该像吃饭喝水一样自然。Linux 系统管理指令里和 CPU 相关的部分,正是这个系列第二章的核心内容。这篇内容适合所有需要自己排查服务性能问题的人——不管你是后端、运维还是“被迫全栈”的开发者,把 CPU 相关的指令练熟了,遇到机器卡顿、进程飙高、负载异常的时候,才不会像无头苍蝇一样乱抓。
我会从系统级观测讲起,逐步深入到 CPU 硬件识别、负载与上下文切换、进程级定位与优先级管理,最后用一次完整的 CPU 飙高排查过程收尾。全程都会带上真实可用的命令、参数解释和踩坑经验,尽量做到你看完就能直接照着用。
1. 先把系统级 CPU 状态一眼看清
1.1 uptime:三分钟看懂 Load Average
很多人以为 Load Average 就是 CPU 使用率,这是个被误解了很多年的概念。先看命令本身:
uptime输出大概是这样的:
14:22:33 up 20 days, 3:12, 2 users, load average: 1.20, 0.80, 0.60重点是最后三个数字:它们分别代表最近 1 分钟、5 分钟、15 分钟内的平均活跃任务数。“活跃任务”不仅包含正在跑在 CPU 上的进程,还包括不可中断睡眠状态(D state)的进程。D 状态通常和磁盘 I/O、网络文件系统等待绑定在一起,所以负载高并不必然等于 CPU 跑满了。
判断负载是否过高的标准要和 CPU 核数挂钩。单核 CPU 的 load 到 1.0 就接近饱和,4 核机器 load 到 4.0 才是同等程度。一个常见的解读套路是:
- 1 分钟值远大于 15 分钟值,说明系统近期有突发压力;
- 三个值持续走高,说明压力是逐步累积的;
- 持续高于核数,就要往下查,是 CPU 排队还是 D 状态进程堆积。
我习惯把 load average 类比成高速公路收费站的排队长度。CPU 核数是收费窗口,排队多不一定窗口不够,也可能是某个窗口前堵了一辆抛锚的车——这个“抛锚的车”,往往就是 D 状态进程。
1.2 top:CPU 监控的“瑞士军刀”
top是最常用的实时监控命令,大多数人只是用它看一眼就退出,其实里面的信息量很大。常用启动方式是:
top -d 2-d 2表示每 2 秒刷新一次。进入交互界面后,几个按键一定要记住:
P:按 CPU 使用率降序排列,默认就是这个;1:展开或折叠每个逻辑核心的独立使用率;H:切换线程视图模式,能看到进程内的线程;c:显示完整命令行路径;z:开启颜色高亮。
最值得仔细看的是%Cpu(s)那一行,里面藏着系统运行状态的核心信息:
%Cpu(s): 8.3 us, 2.1 sy, 0.0 ni, 89.3 id, 0.2 wa, 0.0 hi, 0.1 si, 0.0 st字段含义分别是用户态占用、内核态占用、优先级调整过的进程占用、空闲、I/O 等待、硬中断、软中断、被虚拟机管理程序偷走的时间。这行数据是判断性能瓶颈方向的起点:us高大概率是业务代码问题,sy高要怀疑系统调用或锁竞争,wa高就要看磁盘 I/O。
还有一个细节容易被忽略:top的%CPU列显示的是相对单个逻辑核的比例。也就是说,一个多线程进程如果占满 4 个核,这里可能显示 400%。看到超过 100% 的数值不要慌,这不是 bug,后面我会专门讲。
脚本或者自动化采集场景下,top要用批处理模式:
top -bn1 | head -20-b是批处理模式,-n1表示只输出一次。head -20截取前 20 行,可以避免刷新动画把输出搞乱。
1.3 htop:比 top 更像任务管理器
htop不是系统自带的,但绝大多数发行版都能装:
# CentOS/RHEL 系 yum install -y htop # Debian/Ubuntu 系 apt install -y htophtop的优势是交互体验:CPU 占用率用彩色条形图展示,按F5切换到树状视图,能直接看到父子进程关系;按F6可以选择排序字段;选中进程后按F9可以直接发送信号,省得另开一个终端敲kill。
不过我必须提醒一句:生产环境没有图形界面的情况下,htop可能没装;而top一定在。所以top的按键用法必须是肌肉记忆,htop只是锦上添花。
2. 认识机器的 CPU“底细”
2.1 lscpu:一条命令看全家福
排查性能问题之前,先搞清楚这台机器到底是什么配置。lscpu是最直观的方式:
lscpu重点看这几个字段:
Architecture: x86_64 CPU(s): 8 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Model name: Intel(R) Xeon(R) CPU E5-... NUMA node0 CPU(s): 0-7CPU(s)是逻辑核数,也就是操作系统看到的 CPU 数量。要算物理核心数:逻辑核数 / Thread(s) per core = 物理核数。像上面这个例子,8 个逻辑核除以每核心 2 线程,实际是 4 个物理核,开了超线程。
flags字段里也藏着信息:包含vmx或svm说明 CPU 支持硬件虚拟化;包含avx2、avx512说明支持高级向量指令,对某些计算密集型程序性能影响很大。
判断机器是不是“超线程虚胖”,一个简单办法是看Thread(s) per core。超线程对大多数业务来说是加分项,但对部分极端计算场景可能反而引发资源争抢,这个时候就需要配合绑核操作了。
2.2 /proc/cpuinfo 和 /proc/stat:数据源头
lscpu的输出本质上来自/proc/cpuinfo,但后者能提供一些更原始的信息。几个常用命令:
# 逻辑核数 grep -c "processor" /proc/cpuinfo # 物理 CPU 数量 grep "physical id" /proc/cpuinfo | sort -u | wc -l # 每颗物理 CPU 的核数 grep "cpu cores" /proc/cpuinfo | sort -u真正做 CPU 监控时,绕不开/proc/stat。它的第一行是总 CPU 时间片:
cpu 12345678 2345 345678 90123456 12345 6789 23456 0这 8 个数字依次对应:用户态时间(user)、低优先级用户态时间(nice)、内核态时间(system)、空闲时间(idle)、I/O 等待时间(iowait)、硬中断时间(irq)、软中断时间(softirq)、Steal 时间。注意top里展示的百分比,就是从这些累计值里采样计算出来的。
CPU 使用率的计算公式不复杂,关键在于“两次采样取差值”:
第一次采样: total1 = user1 + nice1 + system1 + idle1 + iowait1 + irq1 + softirq1 + steal1 第二次采样: total2 = user2 + nice2 + system2 + idle2 + iowait2 + irq2 + softirq2 + steal2 CPU 使用率 = ( (total2 - total1) - (idle2 - idle1) ) / (total2 - total1) * 100这里用idle的差值作为闲散时间,剩下的部分都算“忙”。实际脚本里很多人会把iowait单拎出来看,因为 I/O 等待占用高但 CPU 还在发指令,不能简单当成纯忙碌。
2.3 为什么全栈工程师必须知道“几核几线程”
部署服务时,这个信息直接决定配置参数的合理性。比如一个 Java 服务默认线程池大小、Nginx 的worker_processes、数据库的连接池配置,都和逻辑核数相关。盲目把线程池配成“越大越好”,往往会在 CPU 上下文切换上吃大亏。
容器环境下尤其明显。假设一台物理机 16 核,给某个容器--cpus=1,容器内lscpu可能仍然显示 16 核,但实际能用的 CPU 配额只有 1 核。这时候你在容器内部跑top,看到load average高达 10,其实不等于整机负载高,只说明容器被限制得厉害。所以排查容器 CPU 问题,一定要跳出容器看宿主机。
物理机上做绑核操作也经常用到:
# 将进程绑定到逻辑核 0 和 1 上 taskset -pc 0,1 PID # 查看进程当前绑定关系 taskset -pc PIDNUMA 架构下还可以用numastat查看内存访问是否跨节点。绑核这种操作一般不在业务代码里做,而是运维层和容量规划层的活,但全栈工程师至少要知道有这些命令存在,遇到延迟敏感型服务才不至于束手无策。
3. 负载、上下文切换与历史数据
3.1 vmstat:两秒钟判断瓶颈方向
vmstat是我最依赖的指令之一,它用极少的输出覆盖了 CPU、内存、I/O 三大维度:
vmstat 1 5每 1 秒采样一次,共输出 5 次。关注这几列:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa 3 0 128 20480 1024 65536 0 0 12 48 120 2400 8 2 89 1r:运行队列长度,等待 CPU 调度的任务数。持续大于 CPU 核数,说明 CPU 排队严重。b:不可中断睡眠进程数。这个值异常升高,通常意味着 I/O 出问题了。cs:每秒上下文切换次数。这个指标极其重要,后面单独展开。us/sy:用户态 / 内核态 CPU 占比。id:空闲占比。wa:I/O 等待占比。
判断思路可以整理成套路:
- CPU 瓶颈:
r高、us高、id低,不用犹豫,直接去挖用户态代码; - I/O 瓶颈:
wa高、b高、bi/bo有持续读写,这时盯磁盘iostat; - 上下文切换异常:
cs飙到几十万级别,同时sy也高,就要查线程数量和锁竞争。
3.2 mpstat:把视角切到每个核心
vmstat看的是全局平均值,有时候会掩盖“单核打满”的真实情况。比如 8 核机器整体空闲 80%,但某个核已经被干到 99%,这通常意味着应用是单线程或者被绑核了。这时候要用mpstat:
mpstat -P ALL 1 3输出会列出每个逻辑核的独立占用率:
12:00:01 AM CPU %usr %nice %sys %iowait %irq %soft %steal %idle 12:00:02 AM all 15.0 0.0 3.0 1.0 0.0 0.5 0.0 80.5 12:00:02 AM 0 99.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 12:00:02 AM 1 2.0 0.0 1.0 0.0 0.0 0.0 0.0 97.0all行是平均值,后面的0、1是逻辑核编号。看到某个核独占 99%,下一步就去确认这个进程是不是绑核了,还是 JVM 的 GC 线程集中在某个核上。
3.3 sar:把历史翻出来对比
线上问题最麻烦的一点是:你赶到现场的时候,现象可能已经消失了。这时候sar能帮你把过去某段时间的性能数据翻出来。用法也不复杂:
sar -u 1 3 # 当前 CPU 使用率采样 sar -q 1 3 # 负载和运行队列长度 sar -P ALL 1 1 # 每个核的使用情况sysstat工具包提供了数据采集能力,一般通过 cron 每分钟落盘一次,归档文件在/var/log/sysstat/目录下。我第一次用sar排查问题时,最直观的感受是:你能精确地说出“CPU 是从下午 2 点 10 分开始飙升的,持续了 25 分钟”,这比“下午业务高峰 CPU 比较高”这种模糊表述有用得多。
排查方向可以这样用:把问题时段和正常时段的sar -q输出对比,如果负载均值突然翻倍,再结合sar -u看看是用户态还是内核态驱动,定位效率会高很多。
3.4 上下文切换:为什么线程太多反而更慢
我们来深入讲讲vmstat里的cs列。上下文切换可以简单理解成 CPU 从一个任务切到另一个任务的“换挡”过程。换挡本身有开销:寄存器状态要保存、缓存要失效、调度器要做决策。任务切换得越频繁,真正干活的算力被打折越多。
生活化类比:一个咖啡师同时给 100 个顾客做咖啡,如果每个顾客做一口就换下一个,大部分时间都浪费在切换上。合理的做法是让每个人至少喝完一杯再换。操作系统也一样,线程数量远超核数时,每个线程分到的时间片变短,切换次数暴增,系统整体吞吐反而下降。
排查这个问题的命令组合是:
vmstat 1 10 # 看 cs 列的变化 pidstat -w 1 5 # 看每个进程的主动/被动切换数量pidstat -w会输出cswch/s(主动切换)和nvcswch/s(被动切换)。主动切换通常是线程自己让出 CPU,比如等锁、等 I/O;被动切换是被更高优先级任务抢占。两类数值都高时,重点检查线程池是不是开得太大了。
4. 进程级 CPU 管理与定位
4.1 ps:快速揪出 CPU 榜首
全局看完了,下一步就是找“谁在吃 CPU”。ps命令配合排序输出非常高效:
ps -eo pid,ppid,%cpu,%mem,user,stat,cmd --sort=-%cpu | head -20这条命令把系统中所有进程按 CPU 使用率降序排列,只取前 20 条。字段里%cpu和stat值得多说两句。
%cpu在ps中是一个很有意思的指标:它表示进程从启动到当前时刻累计消耗的 CPU 时间,按逻辑核数做了折算。所以一个长期存在的多线程进程,%cpu显示 800% 都不奇怪。这个值的弊端是不能反映“此刻”的占用,看瞬时一定要用top或pidstat。
stat是进程状态,常见的有R(运行中)、S(睡眠)、D(不可中断睡眠)、Z(僵尸进程)。如果你在一堆进程里看到大量D状态,CPU 使用率可能不高,但 load 已经爆了,这时候排查方向应该是 I/O 而不是 CPU。
4.2 pidstat:线程级 CPU 视图
定位到高 CPU 进程之后,90% 的场景还需要定位到“哪个线程”在捣乱,尤其是 Java、Go 这类多线程运行时。pidstat是最直接的工具:
pidstat -t -p 12345 1 5-t表示显示线程维度,-p指定进程号,输出里会多出TID列。假设看到TID 4567的%CPU长期 100%,这个线程就是绕不开的对象了。
如果排查的是 Java 应用,还需要把线程号转成十六进制,去线程栈里找对应关系:
printf "%x\n" 4567输出的十六进制值,比如11d7,然后配合线程 dump 文件查找:
jstack 12345 | grep -A 20 "0x11d7"这一步能把“CPU 高的线程”和“业务代码里哪一行”关联起来。虽然这个过程听起来繁琐,但它在生产环境救过我太多次。非 Java 应用也可以用gdb附加进程查看线程栈,不过生产环境附加gdb需要谨慎评估,性能影响不小。
4.3 nice / renice:给进程排优先级
Linux 的调度器给每个进程分配了一个优先级指标——nice 值,范围从 -20 到 19。数值越小优先级越高,默认是 0。普通用户只能调高 nice 值(让进程更谦让),只有 root 能调低。
启动时指定优先级:
# 以 nice=10(较低优先级)启动一个批量任务 nice -n 10 ./batch_task.sh对已经在跑的进程做调整:
renice -n 5 -p 12345典型场景是:业务高峰期你想跑一个不那么重要的日志压缩任务,又不想它抢占在线服务的 CPU。把任务 nice 到 10 以上再启动,调度器会自动让它在系统空闲时获得更多时间片。
nice不是万能的,它只能影响同优先级竞争下的调度比例。如果机器 CPU 本身已经满载,nice 值的调节效果有限。极端情况下要限制进程 CPU 占用比例,可以借助cpulimit这类工具,它会通过暂停/恢复进程的方式把 CPU 占用压到目标值以下。这类工具通常用于“没法改代码又要立刻止血”的场景。
5. 一次 CPU 飙高问题的完整排查实录
5.1 从告警到结论的六个步骤
我把自己在实际项目里沉淀出的一套排查流程固定成六步,遇到 CPU 异常基本照做:
- 登服务器先跑
top,找到 CPU 占用 TOP 的进程,记录 PID; - 用
top -H -p PID或pidstat -t -p PID 1 5,定位到具体线程 TID; - 确认这个线程的 CPU 占用是持续还是瞬时,至少采样 3 次以上;
- 根据应用类型打开线程栈,Java 用
jstack,C/C++ 用gdb或perf; - 回到
vmstat看us/sy比例,判断问题出在用户态还是内核态; - 汇总现象和结论,确定修复方向并验证。
这套流程的顺序是有讲究的:先现象、后细节、再根因,每一步的输入来自上一步的输出。很多人一上来就strace、perf,只会把现场越搞越乱。
5.2 用户态高还是内核态高,处理方向完全不同
在vmstat或者top的 CPU 行里,us和sy的比例决定了排查路径。
us高最常见的原因有三类:业务代码死循环、算法复杂度退化、GC 频繁或内存分配压力大。这类问题基本要在代码层解决,压测复现后拿 profiler 找热点函数。
sy高则指向内核态工作:系统调用频繁(比如大量网络读写、文件读写)、锁竞争激烈、软中断风暴。我曾经遇到过一个案例,某个服务每秒发生几十万次系统调用,sy占比超过 70%,最后通过批量处理请求和连接复用解决了问题。
两者同时高也很常见,不能机械地只盯一个方向。我的经验是:先把strace -c -p PID统计一下进程的系统调用分布,再决定往哪边深挖。
5.3 一个模拟案例:某服务的 CPU 飙高定位
为了把流程讲清楚,我模拟一次完整的排查过程。假设有个应用叫“模拟项目 X”,是 Java 写的在线服务,某天监控报警:CPU 使用率 85% 持续 5 分钟。
第一步,登服务器:
top -bn1 | head -20看到进程PID 32145,Java 进程,%CPU接近 700%(说明至少占用了 7 个逻辑核),状态R。
第二步:
top -H -p 32145 -bn1线程视图里看到TID 32189占 99%。为了确认它是持续高,再跑:
pidstat -t -p 32145 1 5五次采样中TID 32189的%CPU都接近 100%,基本可以判断这个线程在疯狂干活。
第三步:
printf "%x\n" 32189输出7dbd。接着拿线程栈:
jstack 32145 | grep -A 30 "0x7dbd"栈信息里能看到这个线程卡在一个 JSON 解析逻辑的循环里,最终定位到是配置下发后某个字段的解析出现了无限循环。修复代码并发布后,CPU 使用率回落到 3%。
这个过程中有一个很重要的小技巧:jstack只抓某一瞬间的栈,可能抓到的是无关紧要的瞬态。一定要多抓几次,至少间隔 3 秒抓 3 份栈,交叉对比后共同的调用点才值得深入。
6. 常见问题与避坑速查
6.1 为什么 top 和监控平台的数值对不上
这是最常被问到的问题。监控平台的数据一般通过 agent 定期采集/proc/stat并计算差值,采样周期通常是 15 秒到 1 分钟;而top默认是 3 秒刷新。两个工具的采样窗口不同,算出来的“平均 CPU 使用率”自然有偏差。所以不要在告警时纠结“平台显示 90%,top 显示 60%”,先确认两边采样周期是否一致,再以短周期采样为准。
6.2 多线程进程 CPU 超过 100% 正常吗
正常。top和ps显示的 CPU 百分比是相对单个逻辑核的。一个进程开了 8 个线程,每个线程满负荷跑,%CPU就可能显示 800%。判断是否异常,不是看这个数字是否超过 100%,而是看整体占用是否超出了这台机器的逻辑核数总额。
6.3 iowait 高但磁盘看起来不忙是怎么回事
wa高说明 CPU 在等待 I/O 完成,但这个 I/O 不一定来自本地磁盘。可能是 NFS 网络文件系统卡顿,也可能是内存大量 swap 导致页面换入换出。排查时把iostat -x 1和sar -W一起看,如果磁盘%util不高但await很大,就优先查网络存储和 swap。
6.4 nice 值调整后没效果
遇到过几次,原因是 cgroup 的 CPU 限制优先级高于 nice 值。也就是说,如果一个进程已经被容器或 systemd 的CPUQuota限制住了,你再怎么调 nice 也没用,配额墙比调度优先级更硬。确认进程是否在容器或受限 cgroup 里:
cat /proc/PID/cgroup如果路径里有明显的一切限制标志,那 CPU 问题要从配额角度解决,而不是 nice。
6.5 排查时别急着 kill -9
CPU 飙高确实让人着急,但kill -9只能让进程消失,问题很快会以另一种形式回来。我见过有人把 GC 线程满血的 Java 服务kill -9之后,进程由守护脚本再次拉起,又被打满,循环往复一整天。正确做法是先抓现场,拿到线程栈之后再决定是重启止损还是修复上线。除非服务完全无响应且严重影响用户,否则“先取证再动手”始终是更稳妥的选择。
我把这类典型问题整理成一张速查表,方便你直接对着排查:
| 现象 | 可能原因 | 优先排查命令 |
|---|---|---|
| top 与监控平台数值不一致 | 采样周期不同 | 对比两边刷新间隔 |
| load 很高但 CPU 空闲 | D 状态进程、I/O 阻塞 | vmstat 1、ps -eo stat |
| us 高 | 业务代码热点、死循环、GC | jstack、perf top |
| sy 高 | 系统调用过多、锁竞争 | strace -c、pidstat -w |
| 单核打满其他核空闲 | 单线程应用、绑核 | mpstat -P ALL 1 |
| wa 高但磁盘空闲 | NFS 卡顿、swap 频繁 | iostat -x 1、sar -W |
| 调 nice 无效 | cgroup CPU 配额限制 | cat /proc/PID/cgroup |
个人体会写在后头
CPU 相关的指令真的不难,难的是知道什么时候看哪个指标、看到指标之后怎么往下走。我的体会是:把uptime、top、vmstat、pidstat这四兄弟用熟,能解决 80% 的 CPU 排查问题;lscpu、sar、mpstat属于进阶补充,遇到硬件识别和历史回溯时再拿出来也不迟。实操中第一次定位到具体线程、找到代码根因的成就感,比看十篇教程都来得扎实。这个系列后面还会继续写内存、磁盘和网络,先把 CPU 这关过了,后面很多思路都是相通的。