第 11 章 Linux 性能四板斧
性能问题不是猜出来的,是从系统整体逐层收敛到一行代码的证据链。本章把 CPU、内存、磁盘、网络四类指标读到能定案。
学习目标
读完本章,你应当能够:
- 按「系统整体 → 某类资源 → 具体进程 → 具体代码」组织排查,并说明为什么先看代码必然跑偏。
- 说清负载 = 可运行(R)+ 不可中断(D)的任务数与指数衰减平均口径,推出高负载低 CPU 的三种成因。
- 把
vmstat各列与top的八项 CPU 时间对上,判断wa高的两种相反解释、sy高与切换的关系、st的含义。 - 用
iostat -x的await/aqu-sz判断磁盘瓶颈并论证%util为什么不可靠;用sar/ss/netstat -s定位网络侧问题。 - 在
perf、off-CPU 火焰图、bpftrace、ftrace、/proc/[pid]/stack之间选出能回答「进程在等什么」的那一个,并走完「P99 从 50 ms 涨到 800 ms」的排查链。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
进程状态机、D 状态、cgroup 资源限制 | 见第 10 章 10.1 节、10.6 节 |
| CFS 调度与上下文切换开销的量级 | 见第 5 章 5.5 节 |
| 系统调用开销与中断路径 | 见第 9 章 9.5 节、9.7 节 |
| page cache 与虚拟内存的观测面 | 见第 7 章 7.6 节、第 8 章 8.3 节 |
11.1 排查方法论:收敛路径与 USE 法
排查难在搜索空间太大:一次慢请求可能源于应用锁、GC、磁盘队列、宿主机超卖,各层现象相同。新手最常做的动作是打开代码找可疑循环,然后花两小时读一段本来没问题的逻辑——这条路必然失败,因为代码层是搜索空间最大、证据密度最低的一层:一个服务几十万行,而系统指标只有几十个数字。正确做法是利用「上层指标异常必在该层留下可观测痕迹」逐层排除,每层把候选原因砍掉一个数量级:
| |
支撑这条路径的是 USE 方法(utilization / saturation / errors):利用率回答「忙不忙」,饱和度回答「排队多长」,错误回答「有没有丢东西」。饱和度对延迟最敏感——利用率 100% 只是「没空闲」,饱和度高意味着新请求要排队,延迟立刻恶化。
| 资源 | 利用率(忙不忙) | 饱和度(排队多长) | 错误(丢了吗) |
|---|---|---|---|
| CPU | 100 − id − wa − st | vmstat 的 r、/proc/loadavg、/proc/pressure/cpu | dmesg 的降频与 mce |
| 内存 | available / total(不是 free) | vmstat 的 si/so、/proc/pressure/memory | OOM kill、memory.events 的 oom_kill |
| 磁盘 | %util(须配并发度读) | aqu-sz、await、/proc/pressure/io | dmesg 的 I/O error |
| 网络 | sar -n DEV 的带宽占比 | listen overflow、ss -s 的发送队列 | retransseg、SyncookiesSent、softnet_stat 丢包列 |
重要
面试的分水岭在这一节:能背 vmstat 各列的人很多,能说清「先看饱和度还是先看利用率、以及为什么」的人很少。标准答法:利用率说明资源有没有闲着,饱和度说明请求有没有在排队,决定延迟的是后者。所以看到 %util = 100% 不要下结论,看到 aqu-sz 从 1 涨到 8 才是动手信号。
11.2 平均负载:统计的是任务数,不是 CPU 使用率
11.2.1 三个数字的真实口径
/proc/loadavg 与 uptime 的三个数字是 1、5、15 分钟的指数衰减移动平均(exponentially decaying moving average),不是窗口内的算术平均。内核用定点数(FSHIFT = 11,即 FIXED_1 = 2¹¹ = 2048)周期性做指数平滑:
| |
警告
高频错误:把 /proc/loadavg 说成「过去 1/5/15 分钟的平均进程数」。它既不是算术平均(是指数衰减平均),也不是「进程数」而是任务数——同一进程的多个线程各算一个。一个开 200 线程的服务与一个单线程服务,同一份代码的负载口径完全不同。
11.2.2 负载的两个组成部分:R 与 D
| 状态 | 内核含义 | 为什么算进负载 |
|---|---|---|
R(TASK_RUNNING) | 正在 CPU 上跑,或在运行队列等 CPU | 想要 CPU 而没拿到,是真正的 CPU 需求 |
D(TASK_UNINTERRUPTIBLE) | 不可中断睡眠:等磁盘 I/O、等驱动事件 | 它想要资源而没拿到。不可中断是因为该路径持有内核不变量,被信号打断会让结构半更新(见第 10 章 10.2 节) |
S 等可中断睡眠 | 等网络、等锁、epoll_wait、sleep | 不算:不消耗资源,不代表系统承压 |
D 态计入负载是负载指标最重要也最容易答错的一点。设计意图是:负载衡量「系统有多少活等着被干」,这个量不该只算 CPU。一块慢盘会把一批任务按在 D 态,此时 CPU 可能很闲,但系统确实忙不过来——这就是高负载低 CPU 的来源,也是 8 核机器上负载同为 8 却含义完全相反的原因。
小心
把 load average 说成「CPU 使用率」最容易把面试答崩。两者量纲不同:负载是任务个数(无上限,可到几千),CPU 使用率是时间百分比(0 ~ 100%)。64 核机器负载 64 而使用率 100%,与负载 64 而使用率 10%,是两个完全不同的问题。
11.2.3 数值口径与三种「高负载低 CPU」的原因
load ≤ 核数 说明队列基本不排队,≈ 核数 说明刚好跑满无余量,> 核数 说明开始排队,load / 核数 大致反映每个任务要等的倍率。但该判据只在负载由 CPU 贡献时成立:若负载主要来自 D 态,除以核数毫无意义,因为慢盘造成的 D 态任务数与核数无关。
三种成因的机制:其一,D 态 I/O 阻塞——任务在等磁盘、等网络文件系统、等块设备。其二,大量线程频繁唤醒——负载按 tick 采样瞬时队列长度而非累积 CPU 时间,若一批线程处于「睡极短 → 醒 → 跑极短 → 再睡」的循环(忙等、细粒度定时器、锁竞争重试、惊群),采样点容易撞上它们同时在队列里的瞬间,负载虚高而 us + sy 不高。其三,cgroup CPU 配额节流——任务确实处于 R 态,但 cpu.max(v2)/ cpu.cfs_quota_us(v1)在周期内已用完,被节流到下个周期:整机看 CPU 空闲,容器内看永远排队。
| 原因 | 负载来源 | CPU 使用率 | 定位命令 |
|---|---|---|---|
D 态 I/O 阻塞 | b 列 | 低,wa 可能高 | vmstat 的 b、iostat -x、/proc/PID/wchan |
| 线程频繁唤醒 | r 列瞬时抖动 | 低但 sy 偏高 | vmstat 的 cs/in、pidstat -w 1 |
| cgroup CPU 配额 | r 列持续非零(容器内视角) | 被硬顶在配额值 | /sys/fs/cgroup/cpu.stat 的 nr_throttled/throttled_usec 是否持续增长 |
| |
提示
记忆锚点:「高负载低 CPU」= 有人想要资源但拿不到,而这个资源不是 CPU。三个候选就是等盘(D)、等调度机会(唤醒风暴)、等配额(cgroup 限流)。按这个顺序答,比罗列现象有效得多。
11.3 CPU 统计:把 vmstat 的每一列对上内核计数器
11.3.1 列定义与第一次输出的陷阱
vmstat 的数据来自 /proc/stat、/proc/vmstat、/proc/meminfo,所有比率列都是「自上次采样的区间速率」,因此第一次输出是自开机以来的累计平均,必须丢弃,从第二行看。这是现场最容易踩的坑:vmstat 1 第一行显示 us=30,有人据此判断「CPU 30%」,实际那是开机至今的平均值。
| 列 | 含义 | 判据与常见误读 |
|---|---|---|
r | 运行队列中可运行的任务数(含正在运行的) | 持续 > 核数 说明 CPU 饱和;对应 load 的 R 部分 |
b | 处于不可中断睡眠(D)的任务数 | 非零且持续就是 I/O 阻塞;这就是负载里 D 的部分 |
si / so | 从 swap 换入 / 换出到 swap 的速率(KB/s) | 判断「是否真的在用 swap」的唯一可靠信号,见 11.5.2 |
bi / bo | 块设备读入 / 写出的块数(blocks/s,1 block = 1024 B) | 统计块设备层 I/O;单位是块不是 KB,见下 |
us / sy | 用户态消耗 / 内核态消耗(含系统调用与部分中断处理) | sy 高通常指向系统调用密集或切换密集,见 11.3.4 |
id | 空闲 | id 高不代表没压力:D 态任务不消耗 CPU |
wa / st | 等待 I/O 的时间 / 被宿主机偷走的时间 | 前者见 11.3.3;后者仅虚拟化下有,见 11.3.5 |
11.3.2 top 的八项 CPU 分解
top 顶部的 %Cpu(s) 与 vmstat 是同一批计数器,只是切分更细:us(用户态)、sy(内核态)、ni(nice,被调低优先级的进程所用)、id(真空闲)、wa(等 I/O)、hi(硬中断)、si(软中断)、st(被偷走)。八项在任何时刻相加恒为 100%,所以现场心算用 有效计算时间 = us + sy + ni + hi + si = 100 − id − wa − st。一个易混淆点:top 默认 Irix 模式下 us 已包含 guest 时间,按 Shift+i 切到 Solaris 模式才会分开。
| 字段 | 高的时候该怀疑什么 | 下一步命令 |
|---|---|---|
us | 应用计算密集:序列化、加密、正则回溯、GC | perf top -p PID 看热点函数 |
sy | 系统调用频繁、切换频繁、内核锁竞争、网络协议栈 | vmstat 1 看 cs;perf top -g 看内核函数 |
ni | 有进程被调过 nice 且占用可观 CPU | top 的 NI 列 |
id | 空闲。若同时负载高,必须转查 b 列与 cgroup 配额 | vmstat 的 b、cpu.stat |
wa | 见 11.3.3,两种相反解释 | iostat -x、pidstat -d |
hi / si | 硬中断过密(高频小包网卡)/ 软中断过密(NET_RX、BLOCK) | /proc/interrupts、/proc/softirqs |
st | 宿主机 CPU 超卖 | mpstat -P ALL 1,见 11.3.5 |
| |
警告
vmstat 的 bi/bo 单位是 blocks/s(1 block = 1024 B),而 iostat 的 rkB/s/wkB/s 是 KB/s。把 bo = 102400 当成 100 MB/s 会算错 1000 倍。这是报告事故时最常见的量纲错误之一。
11.3.3 wa 高不代表磁盘慢:两种相反解释
wa(iowait)对应 /proc/stat 的 iowait,内核只在很窄的条件下累加:该 CPU 空闲(没有可运行任务),同时至少有一个任务正在等本地块设备的 I/O 完成。每个限定词都重要:CPU 有活干时等待 I/O 的任务不产生 iowait,所以忙碌的机器上 wa 天然偏低,此时磁盘可能已经是瓶颈;等网络文件系统(NFS)或套接字的 I/O 不计入,所以存储全走 NFS 的服务磁盘很慢也可以 wa = 0;它是「CPU 视角的空闲归类」,iowait 期间发起 I/O 的 CPU 实际空闲,并没有被 I/O 占住。
| 现象 | 可能的真相 | 区分办法 |
|---|---|---|
wa 高 | 磁盘确实慢,任务在排队 | iostat -x 的 await 显著高于设备固有延迟、aqu-sz 上升 |
wa 高 | 磁盘其实空闲,只是该 CPU 恰好没事干而另一任务在等一次 I/O | %util 低、aqu-sz ≈ 0,说明设备没排队 |
wa 低但应用慢 | 磁盘是瓶颈,但 CPU 一直在忙(没机会累加 iowait) | pidstat -d 看 iodelay,它按进程归属且不受 CPU 是否空闲限制 |
重要
标准答法的形成逻辑:wa 是「CPU 空闲且有人在等本地块设备 I/O」的时长占比,归因给 CPU,不归因给磁盘。所以它既不是磁盘利用率的代理,也不能作为磁盘瓶颈的判据——判磁盘瓶颈请看 11.6 节的 await 与 aqu-sz。
11.3.4 sy 高与系统调用、上下文切换
sy 包含三部分:系统调用执行、中断与软中断处理(扣掉单列的 hi/si)、内核线程与内核内部工作。一次系统调用的成本分四块(见第 9 章 9.5、9.7 节):特权级切换(寄存器与栈保存、syscall/sysret 本身)、地址空间相关处理(SWAPGS、返回用户态时的 TLB 处理)、参数校验与权限检查、以及执行体本身可能触发上下文切换(如 read 遇数据未就绪就睡)。因此 sy 高的成因按排查顺序是:系统调用次数太多(不是单次太贵,典型是 read/write 小包循环、futex 风暴、epoll_wait 空转,用 strace -c -f -p PID 或 perf trace 看分布);上下文切换太多(每次切换都要执行调度与地址空间切换,还破坏缓存局部性,用 vmstat 的 cs 与 pidstat -w 定位);内核锁竞争(sy 高且 CPU 大量「忙等在内核」,用 perf top -g 看内核函数)。
11.3.5 st:被宿主机偷走的时间
st(steal time)是当前 vCPU 处于可运行状态,但宿主机物理 CPU 没有调度给它的时间。它是协作式指标,只有宿主机的虚拟化层(KVM 的 kvm_steal_time 等)主动报告时才有意义,裸机恒为 0;判据是持续超过 5% ~ 10% 就值得警惕。它的关键性质是在 Guest 内部任何应用优化都无效,典型症状是「同一份代码、同样 QPS,在容器或云主机上 P99 抖动明显更大」。
注意
st 与其他字段的边界:这段时间 vCPU「想跑没跑成」,所以不计入 id(不是空闲),也不计入 us/sy(没真的执行)。这正是八项之和恒为 100% 的原因,也是排查虚拟化性能时必须把 st 单列出来看的原因:它会把真实 CPU 需求掩盖成一个偏小的 us。它由宿主机写入 Guest 内核的 per-CPU 统计,容器共享同一内核,所以只能从宿主机或节点层监控读到。
11.4 上下文切换:cs 高但不忙的典型场景
11.4.1 cs 与 in:两个常被混为一谈的计数器
| |
自愿切换由任务自己发起(等 I/O、等锁、nanosleep、futex、读空管道),高本身正常,但与低 CPU 使用率组合说明大量时间花在等待上;非自愿切换由内核抢占发起(时间片耗尽仍想跑、被更高优先级抢占),说明 CPU 争抢激烈或优先级配置不当,该任务的 CPU 需求没被满足。
11.4.2 cs 高但不忙的三类场景
「cs 几十万/秒而 us + sy 只有 20%」信息量很大:它几乎排除了「算得太多」,指向「切换得太多」。其一,锁竞争。 无竞争的 futex 快路径在用户态一次 CAS 完成(约 20 ns);一旦有竞争,等待方要进内核 futex 等待队列睡眠,持锁方释放时再唤醒——一次竞争至少带来两次切换,且唤醒后未必马上抢到锁,可能反复「睡 → 醒 → 抢不到 → 再睡」。其二,频繁唤醒(唤醒风暴 / 惊群)。 事件源一次到达唤醒大量等待者,或线程池里每个任务都极短;唤醒目标若在不同 CPU 还会触发核间中断(IPI),in 跟着涨。其三,细粒度定时器与轮询退避。 大量任务以毫秒甚至更小间隔 nanosleep,或 select/poll 超时值极小导致空转。
| |
警告
常见错误回答:「上下文切换的开销就是保存寄存器,很快」。真实成本是四块:寄存器与栈的保存恢复(约 1~5 μs)、地址空间切换导致的 TLB 与缓存局部性损失(大头,可能让之后几千条指令全部缓存未命中,见第 1 章 1.4 节)、调度器决策、以及新任务预热。所以减少切换次数的收益远大于让切换更快。
11.5 内存指标:free 的每一列与 available
11.5.1 free 输出的逐列解释
| |
| 列的常见误读 | 为什么错 | 正确读法 |
|---|---|---|
「free 只剩 200 MB,赶紧扩容」 | page cache 会被优先用作缓存,可随时回收 | 看 available / total 与 /proc/pressure/memory |
「used 高就是内存泄漏」 | used 不含 buff/cache,但含不可回收 slab 与共享内存 | 按进程看 RSS/PSS:/proc/PID/smaps_rollup |
「buff/cache 是应用缓存,删掉能省内存」 | 它就是 page cache,删掉只会在下次读盘时重建 | drop_caches 只做实验,不做优化 |
「buff 与 cache 是两个独立可加的量」 | 2.4 起两者已统一到同一套页管理;shared 也已被计入 cache | 分开看,不做加法 |
available 由内核 si_mem_available() 估算(/proc/meminfo 的 MemAvailable):从 free 出发,加上可回收 page cache 的绝大部分、可回收 slab 的一部分,再减去 min_free_kbytes 这类保留量。它是估算值,所以判据要看比例与趋势,不要卡在几百 KB。真正内存不足的签名是三条同时出现:available 逼近 0、buff/cache 被压得很低(说明回收一直在工作)、si/so 开始动。
11.5.2 si / so:是否在用 swap 的唯一可靠信号
vmstat 的 si(从 swap 换入)与 so(换出到 swap)是唯一可靠的「是否真的在用 swap」信号。原因:free 的 Swap: used 只是「swap 里目前有多少页」,它只反映历史压力——一台机器可能在几分钟突发中换出 2 GB,之后内存宽裕了,这些页仍躺在 swap 里(换入是惰性的,被访问时才换回)。若 si/so 持续非零,确实不够,损失机制也明确:换页按页(4 KB)同步进行,一次换入可能是一次磁盘读;若换入的页之后又要换出别的页,就形成 swap 抖动(thrashing),CPU 大量时间耗在页替换上,吞吐断崖式下跌。此时 wa 未必高,因为换页等待发生在缺页处理路径上,属于进程自己的执行时间。反之 si/so 全为 0 而 Swap used 非零只说明历史上换过,现在不在动,不构成当前性能问题。
| |
重要
标准答法:「判断是否在用 swap,看 vmstat 的 si/so 是否持续非零,不要看 free 的 Swap used。」能补上「Swap used 是存量、只反映历史压力」与「vm.swappiness 的默认 60 决定的是换出倾向而不是『是否允许换出』」,就领先绝大多数候选人。注意 swappiness = 0 也不等于禁止换页,内存严重不足时内核仍会换出(见第 12 章)。
11.6 磁盘 I/O:await 与 aqu-sz,以及 %util 为什么不可靠
11.6.1 iostat -x 的关键列
iostat -x 的数据源是 /proc/diskstats,它默认打印所有设备(含分区),看分区行与整盘行容易重复计数,务必用 iostat -x 1 nvme0n1 限定设备。
列(iostat -x 常见名) | 旧名 | 含义 | 判据 |
|---|---|---|---|
r/s、w/s | tps | 每秒读/写请求次数(IOPS 的两个分量) | 与 await 一起决定吞吐上限 |
rkB/s、wkB/s | kB_read/s 等 | 每秒读/写的千字节数 | 除以 r/s 得平均请求大小;小请求 + 高 IOPS 即随机负载 |
r_await、w_await | — | 读/写请求平均完成延迟(ms),含排队 + 设备服务时间 | 与设备固有延迟比较,见 11.6.3 |
await | — | 读写延迟的加权平均 | 混合负载下会掩盖读或写的异常,读写要分开看 |
aqu-sz | avgqu-sz | 平均在途请求数(队列长度) | > 1 说明有排队;判断饱和度的首选 |
%util | — | 「设备至少有一个 I/O 在处理」的时间占比 | 见 11.6.2,不可靠 |
rareq-sz、wareq-sz | — | 读/写平均请求大小(KB) | ≥ 128 KB 偏顺序,4~16 KB 偏随机 |
%rrqm、%wrqm | — | 被合并的请求占比 | 合并率高说明上层 I/O 模式接近顺序 |
11.6.2 %util 为什么不可靠
%util 的定义是「采样区间内该设备有至少一个 I/O 在途的时间占比」。对单队列设备它近似等于「忙」的比例,但对现代设备这个类比彻底失效,因为一台设备可以同时处理多个 I/O:
| |
正确结论:%util 高 ≠ 磁盘是瓶颈,它只说明设备一直在处理 I/O;当在途并发远大于设备最佳并发度时 %util 必然 100%。反过来 %util 低也不代表磁盘没问题:等待可能发生在网络文件系统上,而它根本不计入块设备统计。
11.6.3 判断磁盘瓶颈的正确口径
一是看 await 是否显著高于设备固有延迟。数量级参照(4 KB 随机读,单次设备延迟):NVMe SSD 约 100 μs、SATA SSD 约 100200 μs、机械盘约 510 ms。若 NVMe 报出 w_await = 5 ms,那是排队而非设备慢。难点在于必须知道这台设备的基线,所以生产上看相对变化(与同集群同型号节点对比)而非绝对值。二是看 aqu-sz 与并发度的关系:在途请求数直接反映饱和度,由 Little 定律 aqu-sz ≈ 到达率 × await 可用它交叉验证报告数字是否自洽,若不自洽(虚拟化逻辑卷常见)本身就是重要线索;aqu-sz 接近或超过设备最佳队列深度时,再加负载只增延迟不增吞吐(IOPS = Q / L 的关系见第 9 章 9.9.1 节)。三是看 r_await 与 w_await 是否分离:混合负载下 await 会被平均掉,一次冷读的长延迟可能被大量写请求稀释,读写分开后常能发现「写在排长队,读很轻松」。
| |
小心
面试最容易答崩的一句是「%util 到 100% 说明磁盘到瓶颈了」。正确的反问式回答是:「%util 只说明设备一直在处理 I/O,它不告诉你还有多少余量;判断饱和要看 aqu-sz 与 await。在并发 I/O 或 RAID 场景下,%util = 100% 完全可以健康。」 反过来也要会答:%util 低同样不能证明磁盘没问题,因为等待可能发生在网络文件系统上。
11.7 网络指标:sar、ss、netstat -s 的排障含义
11.7.1 sar -n 子开关
| 命令 | 输出什么 | 关键列与判据 |
|---|---|---|
sar -n DEV 1 | 每网卡收发速率 | rxkB/s/txkB/s 与线速比较;rxpck/s 与 txpck/s 相除得平均包长,包长小说明小包密集(每包协议栈固定开销大,pps 才是瓶颈) |
sar -n TCP 1 | 连接层状态 | active/s、passive/s(新建连接速率)、iseg/s/oseg/s。active/s 远大于 passive/s 说明本机是客户端角色 |
sar -n SOCK 1 | Socket 总量 | totsck、tcp-tw(TIME_WAIT 数量)、tcpsck。tcp-tw 暴涨会耗尽本地端口(见第 16 章) |
sar -n EDEV 1 | 网卡错误与丢弃 | rxdrop/s、txdrop/s、rxerr/s。非零说明链路侧已在丢,这类丢弃常被应用层重传掩盖 |
11.7.2 ss -s、netstat -s 与它们的坑
ss -s 给的是存量快照:各类 socket 的当前数量、内核态内存占用、按状态的 TCP 连接计数,回答「现在有多少连接、卡在哪个状态」。netstat -s(或 nstat)给的是累计计数器,判据是看增量而不是总量——累计值只增不减,总量里的历史重传毫无意义。
计数器(netstat -s 中的行) | 含义 | 排障含义 |
|---|---|---|
segments retransmitted | 重传段数 | 除以 out segments 得重传率:局域网应 < 0.1%,广域网 < 1%。偏高指向丢包、链路错误或对端接收窗口不足 |
SYNs to LISTEN sockets dropped | SYN 队列溢出 | tcp_max_syn_backlog 太小或遭 SYN Flood,与 SyncookiesSent 对照 |
times the listen queue of a socket overflowed | accept 队列溢出 | 这是应用的锅:accept() 太慢,已完成三次握手的连接被丢弃,客户端超时重试。这是「服务端有空闲 CPU 却拒绝连接」的经典根因(见第 16 章) |
packets pruned from receive queue | 接收缓冲区不足 | 应用读得慢,内核丢包,触发重传与窗口收缩 |
SyncookiesSent / SyncookiesRecv | 启用了 SYN cookie | 说明半连接队列确实溢出过,是被动防护生效的证据,不是正常状态 |
TCPAbortOnMemory / TCPBacklogDrop | 内存压力下丢连接 | 需与连接数上限、tcp_mem 一起看 |
| |
11.8 高级工具的选择:谁在算 vs 谁在等
前七节回答「哪类资源饱和了」。最后一步要回答「具体是哪行代码、或者它在等什么」,工具选择的唯一依据是问题的形状:
| |
| 工具 | 最好回答的问题 | 主要代价 / 边界 |
|---|---|---|
perf top / perf record -g + 火焰图 | on-CPU 热点函数与调用链归属(谁调用了那个热点) | 只有 on-CPU 视角;受 perf_event_paranoid 限制;采样频率过低会漏长尾 |
| off-CPU 火焰图 | 进程不在 CPU 上的时间花在哪(等锁、等 I/O、被抢占) | 需 eBPF(bcc 的 offcputime)或 perf sched;用户态栈符号缺失则无法归因到业务代码 |
bpftrace / ftrace | 一行脚本回答特定问题(谁在 open、read 大小分布、谁在等 futex);ftrace 可看内核调用链耗时 | 需内核 4.x+ 与 CONFIG_BPF;脚本本身可能成为新的开销源;function_graph 在热路径开销大 |
strace -T -c | 系统调用次数与耗时分布 | ptrace 开销可达数量级,对高 QPS 服务不可用,会先把它拖死 |
/proc/PID/stack、wchan | 某个进程此刻卡在哪(内核栈 + 等待函数名) | 需 root;wchan 在 4.1+ 默认只给地址 |
| |
提示
面试话术锚点:「perf top 看到的是 CPU 在算什么,看不到进程在等什么。等锁、等 I/O、等网络的时间不在任何 CPU 热点里,只能靠 off-CPU 火焰图或调度事件追踪。」 这能立刻把「只会用 perf top」和「真的排查过线上问题」区分开。追问「off-CPU 的栈怎么来」——答 eBPF 挂 sched_switch,记录切出时刻与栈、切回时求差并按栈聚合(offcputime 就是这个思路)。
11.9 完整案例:接口 P99 从 50 ms 涨到 800 ms
设定:无代码变更、无发布,流量与前一天同一时段持平,但 P99 从 50 ms 涨到 800 ms,P50 只从 20 ms 涨到 35 ms。P50 几乎没动而 P99 涨了 16 倍这个形状本身排除了「整体资源不足」(那会同时抬高 P50),指向「部分请求被某个偶发事件阻塞」。
第一步看系统整体:vmstat 若给出 r=2, b=11, si=so=0, id=88, st=0,则负载高(r + b 超过 8 核)但 CPU 空闲(id = 88),且 b = 11——这正是 11.2.3 节「原因一:D 态 I/O 阻塞」的签名;si/so = 0 排除内存换页,st = 0 排除虚拟化超卖。第二步验证块设备侧是否真的排队:
| |
这里的关键是交叉验证:w_await 与 aqu-sz 同时恶化,两个独立指标指向同一结论,才是可靠证据——单看 %util = 99.8% 不足以下结论(11.6.2 节)。同时要排除一个常见误判:若 df 显示数据落在 NFS 上,这笔 I/O iostat 根本看不到(iowait 也不计),此时要换 nfsiostat、mountstats。第三步把写入归因到进程与线程,第四步才读代码——而第一件事不是读代码,是读内核栈:
| |
定位结论。 b = 11 与 w_await = 48.6 ms 说明块设备写队列已饱和;pidstat -d 把写入归因到某个进程;/proc/PID/stack 显示它卡在文件系统日志提交上。根因是写 I/O 饱和导致的 D 态阻塞:某进程开始大量写盘,同盘上的其他进程一起被拖慢,每次写请求排队约 50 ms,应用线程被按在 D 态,于是 P99 跳到数百毫秒。而 P50 几乎不动,是因为只有命中这条写路径的请求才被阻塞:读缓存命中的请求不受影响(占多数,P50 稳定),恰好需要写盘或读冷数据的请求才排队(少数,构成 P99)——这就是尾延迟放大的一般规律。
处置与验证(按收益/风险排序)。 一是先止损、不动代码:确认是不是临时批量任务(备份、日志轮转、数据搬迁)在占盘,错峰即可。二是减写入量:调低日志级别、合并小写、批量提交(把 N 次 fsync 合成一次,即组提交思路,见第 21 章 21.3 节)。三是隔离 I/O:日志/备份/数据库拆到不同块设备,用 cgroup v2 的 io.max 或 io.latency 给关键进程设保护(见第 10 章 10.6 节),注意 io.max 限带宽/IOPS 而 io.latency 限延迟,后者对尾延迟更对症。四是确认后再扩容:只有在减少无效写入并隔离之后仍饱和,才考虑加盘或换更高 IOPS 的设备。五是验证:vmstat 的 b 回到 0 附近、aqu-sz 落到 1 以下、w_await 回到设备基线、P99 回落,四项都要变好才算解决。
重要
这个案例的价值不在结论,而在于每层都有排除法的动作:第一步用 si/so 与 st 排除内存和虚拟化,第二步用 await + aqu-sz 双指标确认磁盘饱和(而不是只信 %util),第三步用 pidstat -d 归因到进程,第四步用 /proc/PID/stack 归因到等待对象。面试时把这条链讲完整,比背任何结论都有说服力。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 收敛路径与 USE | 每层只回答一个问题;利用率看忙不忙,饱和度看排队多长,延迟由饱和度决定 | 上来就读代码;只看利用率,看到 id 高就认为健康 |
| 平均负载 | 可运行(R)+ 不可中断(D)的任务数,1/5/15 分钟指数衰减平均,须除以核数 | 说成「CPU 使用率」或「算术平均的进程数」 |
vmstat / wa | 第一次输出是累计平均必须丢;bi/bo 单位是 blocks/s;wa 是「CPU 空闲且有人在等本地块设备 I/O」,归因给 CPU | 用第一行判断当前 CPU;认定 wa 高就是磁盘慢 |
sy / st | sy 指向系统调用次数、切换次数、内核锁竞争;st 是被宿主偷走的时间,容器内不可见 | 说「切换开销只是保存寄存器」;把云上抖动全归因到应用 |
| 内存与 swap | 看 available 而非 free;buff/cache 是合并列且 shared 已计入 cache;si/so 持续非零才是真换页 | free 小就扩内存;看到 Swap used 非零就断定内存不足 |
| 磁盘判据 | 用 await 对比设备基线、用 aqu-sz 看并发度;%util 只说明设备没闲着 | 说「%util 100% 就是磁盘瓶颈」 |
| 网络判据 | 累计计数器要取增量算比率;listen queue overflow 是应用 accept 慢 | 直接读 netstat -s 的绝对值下结论 |
| 工具选择 | perf top 看 on-CPU 在算什么;off-CPU 火焰图看它在等什么;/proc/PID/stack 看卡在哪 | 用 perf top 找阻塞问题,自然什么都找不到 |
自测题
Q1(判断 + 推导)某 8 核机器 uptime 显示 load average: 32.00, 30.50, 28.00,而 top 显示 5.0 us, 3.0 sy, 90.0 id, 0.0 wa, 0.0 st。请判断是否存在 CPU 瓶颈,并给出至少三种可能的解释与对应验证命令。
答案与解析
不存在 CPU 瓶颈。判据有两条:us + sy = 8%,CPU 大量空闲;而 load / 核数 = 4 看似「超载 4 倍」,但这个判据只在负载由 CPU 贡献时有效。负载 = R + D,id = 90% 说明运行队列不长,那 32 个任务必然主要来自 D 态。
D态 I/O 阻塞:vmstat 1看b是否约等于 32;ps -eo stat,pid,comm | awk '$1 ~ /^D/';iostat -x 1看await/aqu-sz;/proc/PID/stack看栈顶等待函数。- 线程频繁唤醒导致队列瞬时虚高:
vmstat的cs/in与负载同向飞涨;pidstat -w 1看谁的自愿切换率极高。此时wa ≈ 0,可与第一种区分。 - cgroup CPU 配额节流:
/sys/fs/cgroup/cpu.stat的nr_throttled/throttled_usec持续增长(仅「非零」不算,要看增长);cpu.max看配额。整机空闲与容器内排队可以同时成立。
错在哪:把 load / 核数 = 4 直接读成「CPU 严重不足」。面试官会追问「wa 是 0,你怎么还说是 I/O 问题?」——wa 只在 CPU 空闲时才累加,所以 wa = 0 在此恰好排除了「本地块设备等待」,把方向推向 cgroup 节流、唤醒风暴,或网络文件系统(NFS 等待不计入 iowait)。这一问考的就是 wa 的口径。
Q2(计算 + 辨析)某 NVMe SSD 的 iostat -x 为 w/s = 4000、w_await = 3.5(ms)、aqu-sz = 14.0、%util = 100.00。请计算写吞吐与每个请求的平均停留时间,判断是否已饱和,并说明 %util = 100% 能否作为瓶颈证据。
答案与解析
写 IOPS = w/s = 4000。由 Little 定律 aqu-sz ≈ 到达率 × 停留时间:Q = 4000 /s × 3.5 × 10⁻³ s = 14.0,与报告的 aqu-sz 一致——说明报告数字自洽可信,可以放心用它判断。设备服务时间(不含排队)无法从这些列直接得出,必须与基线比较:若同型号设备在 aqu-sz ≈ 1、w_await ≈ 0.1 ms 时也能达到 4000 IOPS,那么现在这 3.5 ms 里约 3.4 ms 是排队,设备已经饱和——它跑在吞吐上限附近,再加负载只会增加延迟而不再增加 IOPS。
%util = 100% 不能单独作为瓶颈证据。它的定义是「设备至少有一个 I/O 在处理的时长占比」,aqu-sz = 14 时设备当然每刻都有在途请求,所以 %util 必然 100%——这是定义上的必然,不携带「还有没有余量」的信息。一块能并发 64 个请求的 NVMe 在 aqu-sz = 14 时可能仍有余量,%util 依然是 100%。
错在哪:用 %util = 100% 直接下结论,答不出它的定义;以及忽略 aqu-sz 与 await 的自洽性检验(虚拟化逻辑卷上这两值常不自洽)。面试官会追问「你怎么知道设备上限」——用 fio 压出该型号的 IOPS-延迟曲线做基线(见第 13 章 13.5 节)。
Q3(推导)free -m 显示 total = 16384、used = 15800、free = 120、buff/cache = 464、available = 380。请判断是否内存不足,并说明为什么 available 与 free 差得这么小。
答案与解析
这是内存不足的信号。available = 380 MB 约占 total 的 2.3%;更要紧的是 buff/cache 只剩 464 MB——正常运行的 Linux 上 page cache 通常能占内存一半以上,cache 被压到这么低说明回收(reclaim)一直在工作,反复释放缓存去满足分配。
两者关系是 available ≈ free + 大部分可回收 page cache 与 slab − 内核保留的低水位。正常时 available 远大于 free(因为 cache 很大);这里只差 260 MB,说明可回收部分几乎被榨干,available 退化成「free 加一点点」。这个差值本身就是压力指标,比绝对值更早预警。还要补两件事:是否正在换页(vmstat 1 的 si/so 持续非零才是铁证,Swap used 非零不能证明什么);回收压力与 OOM 风险(/proc/pressure/memory 的 avg10、dmesg -T | grep -i oom、容器内 memory.events 的 oom_kill,见第 12 章 12.5 节)。错在哪:只答「free 小是正常的,要看 available」——通则本身对,但本例 available 也小,结论应是内存不足。这是把正确的通则套到不满足其前提的数据上。面试官会追问「available 怎么算」:内核 si_mem_available() 从 free 出发,加上可回收 page cache 的绝大部分与可回收 slab 的一部分,再减去 min_free_kbytes 等保留量;它是估算值,判据看趋势与量级。
Q4(设计)一个 Java 服务在容器中运行,容器内 CPU 稳定 200%(配额 2 核),吞吐比物理机低 40%,而宿主机 mpstat -P ALL 1 显示多核 %steal 在 8% ~ 15% 波动、%idle 仍有余量。请给出排查路径与处置建议,并说明为什么「优化 Java 代码」在这里无效。
答案与解析
先分清两个独立的限流来源。来源一:cgroup CPU 配额。 容器内 CPU 顶在 200%(正好是 cpu.max 的 2 核)。验证:cat /sys/fs/cgroup/cpu.max(v2 格式 quota period,如 200000 100000 即 2 核);cpu.stat 的 nr_throttled 与 throttled_usec 是否持续增长——只有持续增长才说明真被节流。此时容器内负载(R)很高而整机 id 仍有空闲,正是 11.2.3 节第三种成因。
来源二:宿主机 steal time。 %steal 8% ~ 15% 意味着 vCPU 可运行但物理 CPU 没分给它。这部分损失在容器内任何指标上都看不出来(表现为「时间变慢了」而非「被等待了」),只能从宿主机或云厂商监控读到;%idle 仍有余量说明不是物理 CPU 全满,而是虚拟机与其他租户争抢(超卖)。处置:先在容器内确认节流(cpu.stat、/proc/pressure/cpu);再在宿主机确认 steal 分布——集中在少数核指向亲和/超线程争抢,均匀分布指向整体超卖;配额不足就提高 cpu.max,超卖导致的偷取只能靠换宿主机、换规格、迁移独享型实例。
为什么优化代码无效:两个来源都在应用之外——配额是 cgroup 调度器在周期边界拒绝给 CPU,steal 是宿主机虚拟化层不给物理 CPU。代码优化得再快,%steal 那几十毫秒里它也执行不到指令。能优化的是「在给定算力下做更多事」(减少系统调用与切换、减少 GC 停顿),但「把被偷走的时间拿回来」超出应用权限边界。错在哪:看到 CPU 200% 就断言「CPU 打满了,加机器」;正确顺序是先分清「被自己算满」还是「被配额或宿主机限住」。面试官会追问「%steal 在容器里能看到吗」——不能:它由宿主机虚拟化层写入 Guest 内核的 per-CPU 统计,容器共享同一内核。
Q5(推导 + 设计)vmstat 1 持续显示 cs = 400000、in = 120000,而 us = 12%、sy = 18%;pidstat -w -t 显示某进程 cswch/s 高达 30 万/秒、nvcswch/s 只有几百。请判断属于哪一类问题,给出把范围收窄到「具体是哪把锁」的命令序列,并说明为什么这里不该优先调内核参数。
答案与解析
判断:自愿上下文切换占绝对多数(30 万 vs 几百),说明任务不是在抢 CPU(否则非自愿切换该高),而是在反复主动睡眠等待某个资源;CPU 合计仅 30%,排除了计算密集。为什么更可能是锁竞争而非普通 I/O 等待:in 也高而 wa 未升高,说明不是块设备等待;cs/cswch 的量级(几十万/秒)远超正常 I/O 等待频率。锁竞争的特征是同一把锁上形成「睡眠 → 唤醒 → 抢不到 → 再睡眠」循环,每次至少两次切换,切换率被放大到远超实际等待次数。
| |
为什么不该先调内核参数:症状是应用内部在锁上反复睡眠,属于并发设计问题(临界区过长、锁粒度过粗、用全局锁保护本可分段的状态、CAS 重试风暴),改 vm.max_map_count、vm.nr_hugepages 之类参数不会减少任何一次 futex_wait;正确方向是缩小临界区、按业务键分段加锁、改用分片或无锁结构、或用批处理摊薄唤醒次数。错在哪:只看 cs 绝对值就断言「系统忙着切换所以要减线程数」——线程数不是根因,每个线程为什么反复睡醒才是;以及混淆自愿与非自愿切换,把「等锁」误判成「抢 CPU」。面试官会追问「你怎么证明是同一把锁」——统计 futex_wait 的锁地址分布或 kstack/ustack 聚合结果。
| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应小节 |
|---|---|---|---|
| 平均负载是什么意思?那三个数字分别代表什么? | 负载口径 | 答「CPU 使用率」或「过去 1/5/15 分钟的算术平均进程数」直接扣分;必须说清「可运行 R + 不可中断 D 的任务数 + 指数衰减平均」,并主动补一句「要与核数相除才有意义」 | 11.2 |
| 负载很高但 CPU 使用率很低,你会怎么查? | 高负载低 CPU | 只答「有进程在死循环」——那会推高 CPU;正确答法给三种成因:D 态 I/O 阻塞、线程频繁唤醒、cgroup 配额节流,每种配一条验证命令 | 11.2.3 |
wa 很高是不是说明磁盘到瓶颈了? | iowait 口径 | 答「是」是最典型的错;wa 是「CPU 空闲且有人在等本地块设备 I/O」的时长,归因给 CPU 不归因给盘;还要能举出 NFS 等待不计入、CPU 忙时 wa 天然偏低这两个反例 | 11.3.3 |
%util 到 100% 了,是不是磁盘瓶颈? | %util 定义 | 答「是」会被立刻追问倒;说明它只是「设备至少有一个 I/O 在处理的时长占比」,不携带余量信息,判据应是 await 对比基线加 aqu-sz | 11.6.2 |
free 显示内存快用完了,是不是要扩内存? | 内存指标口径 | 答「free 小就危险」;正确答法是看 available 占 total 的比例,并解释 page cache 可回收、buff/cache 是合并列且 shared 已计入 cache | 11.5.1 |
| 你怎么判断系统是不是在用 swap? | swap 的唯一可靠信号 | 答「看 free 的 Swap used 是不是 0」是错的;正确答法是 vmstat 的 si/so 是否持续非零,并说明 Swap used 只是存量、反映历史压力 | 11.5.2 |
| 你的服务 P99 突然变高但 P50 正常,怎么排查? | 尾延迟归因 | 答「整体资源不足」——那会同时抬高 P50;正确思路是「只有部分请求命中某条慢路径」,用四板斧逐层收敛到具体阻塞点 | 11.9 |
perf top 能看到进程在等锁吗? | on-CPU 与 off-CPU | 答「能」是概念性错误;perf top 采样的是正在 CPU 上执行的栈,等锁/等 I/O 的时间不在任何 CPU 上,要用 off-CPU 火焰图或调度事件追踪 | 11.8 |
listen queue overflow 涨了,是网络问题还是应用问题? | accept 队列语义 | 归因为网络或内核参数;正确答法是「已完成三次握手的连接因 accept() 不及时被丢弃」,属于应用并发不足或 accept 循环被阻塞 | 11.7.2 |
延伸阅读
man 1 vmstat、man 1 iostat、man 1 pidstat、man 1 sar、man 1 mpstat(sysstat的官方列定义;bi/bo的单位与aqu-sz的改名说明都在这里)。man 5 proc(/proc/loadavg、/proc/stat、/proc/meminfo、/proc/PID/stack、wchan、schedstat的字段定义)、man 8 ss、man 8 netstat。- Linux 内核源码:
kernel/sched/loadavg.c(calc_load与exp[]常量、LOAD_FRAC_BITS)、mm/page_alloc.c的si_mem_available()(MemAvailable的估算口径)、block/blk-core.c(iostat各列的计数器来源)。 - Brendan Gregg, Systems Performance: Enterprise and the Cloud(第 2 版第 2 章 USE 方法;第 4~10 章分别对应本章四类资源)、BPF Performance Tools(off-CPU 分析与
bpftrace单行脚本目录)。 - Linux 内核文档:
Documentation/accounting/psi.rst(/proc/pressure/*的语义与阈值接口)、Documentation/admin-guide/cgroup-v2.rst(cpu.stat的nr_throttled/throttled_usec,io.max与io.latency的差别)、Documentation/trace/ftrace.rst。 fio官方 HOWTO(iodepth、bs、rw参数与 IOPS-延迟曲线的测法,与第 13 章 13.5 节配合阅读)。