Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

0.14 性能工具与命令行工程化

本章回答

CPU 高、内存涨、磁盘慢、网络抖,分别先看哪个指标?load average 高但 CPU 空闲说明什么?iostat 的 %util、await、aqu-sz 哪个才是瓶颈证据?怎么把一次诊断固化成可重复的脚本?

读完你会

用 USE 方法给四类资源各选一个指标;用 vmstat/pidstat/iostat/ss -ti 建立证据链;用 perf/strace/bpftrace 追到函数和系统调用;用 lsof 找被占用的文件与已删除但仍占空间的文件;写一个带时间戳、元数据和对比功能的采集脚本。

0.13 解决「通不通」,这一章解决「慢不慢」。性能问题的第一个陷阱是先选工具再找问题:一上来 perf top,看到一堆内核符号,却不知道是 CPU、内存、磁盘还是网络。第二个陷阱是把「资源利用率高」当成「资源不够」:%util 100% 可能只是队列里有请求,延迟并不高;free 只剩 500 MB 可能全是可回收的页缓存。

这一章用 USE 方法(Utilization、Saturation、Errors)组织:每类资源看三个问题——用得多不多、排队严不严重、有没有报错。工具只是取数的手段,证据才是结论。最后一节把采集、保存、对比固化成脚本,让「上次那台机器为什么慢」有据可查。

命令以 Ubuntu 24.04 的 procps 4.0、sysstat 12.6、perf 6.8、bpftrace 0.20 为准。工具装不全时先 apt install sysstat linux-tools-common linux-tools-$(uname -r) bpftrace,不要用来源不明的静态二进制。

0.14.1 先问「哪个资源」,再选工具

USE 把每个资源拆成三个问题:

资源Utilization(用得多不多)Saturation(排队严不严重)Errors(有没有报错)
CPUmpstat 的 %usr+%sysvmstat 的 r、load averagedmesg 的 MCE、perf 的 throttle
内存free 的 usedvmstat 的 si/so、OOMdmesg 的 OOM、memory.events
磁盘iostat 的 %utilaqu-sz、awaitdmesg 的 I/O error、SMART
网络sar -n DEV 的带宽ss -ti 的 retrans、nstat 的 dropsip -s link 的 errors

先把症状归到一类,再取三个数。比如「应用变慢」先看是 CPU 时间涨、内存回收、磁盘等待还是网络重传;没有归类就抓包或 perf,多半是在错误的方向上深挖。

$ uptime
 10:52:11 up 12 days,  3:41,  2 users,  load average: 1.24, 0.98, 0.87
$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi       3.2Gi       8.1Gi       123Mi       4.4Gi        11Gi
Swap:           2.0Gi          0B       2.0Gi
$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 8123456 234567 3456789    0    0    12    34  567  890  3  1 96  0  0
 0  0      0 8123400 234567 3456789    0    0     0     8  432  701  2  1 97  0  0
 2  0      0 8123100 234567 3456789    0    0     0    12  501  820  4  1 95  0  0
$ iostat -xz 1 1
Linux 6.8.0-45-generic (lab) 	03/02/2025 	_x86_64_	(8 CPU)
​
Device            r/s     rkB/s   r_await   w/s     wkB/s   w_await  aqu-sz  %util
nvme0n1          12.0    480.0      0.20   8.0    320.0      0.35    0.01    2.4

uptime 的三个 load 值是 1/5/15 分钟指数平均。Linux 的 load 统计「可运行 + 不可中断睡眠」,所以 D 状态也会推高 load;load 1.24 在 8 核机器上不算高,但要看趋势和 vmstat 的 r 列。

free 的 free 小不代表内存不足,buff/cache 是可回收的页缓存,available 才是「新程序大约还能用多少」。available 远大于 free 是正常的;available 接近 0 且 si/so 非零,才是真的内存压力。

vmstat 每秒一行:r 是可运行任务数,b 是不可中断睡眠数,si/so 是换入/换出(KB/s),us/sy/id/wa 是 CPU 时间占比。wa 高说明 CPU 在等 I/O,不一定是磁盘慢,也可能是 NFS 或锁。

iostat 的第一次输出是自开机以来的平均值,不是当前值;iostat 1 的第二行起才是区间值。%util 对单队列 SATA 盘有意义,对 NVMe 这种多队列设备会低估并行度;判断饱和要看 aqu-sz(平均队列长度)和 await(平均等待时间)。

0.14.2 CPU:从 load average 到 perf

CPU 排障分三步:先确认是不是 CPU 瓶颈,再找哪个进程,最后找进程里哪段代码。

mpstat -P ALL 1 1          # 每个核的 us/sy/wa/id,先看是否单核打满
pidstat -u 1 1             # 每个进程的 %usr/%system/%CPU
ps -eo pid,ppid,stat,%cpu,%mem,comm --sort=-%cpu | head

mpstat -P ALL 能看出「平均不高但某个核 100%」——单线程应用、中断亲和、taskset 绑核都会造成这种不均衡。pidstat -u 的 %system 高说明系统调用/内核路径忙,%usr 高说明用户态计算忙,%wait 高说明进程在等 CPU。

perf 是从「哪个函数」往下钻的工具:

sudo perf stat -p 2411 sleep 5          # 5 秒内的 cycles、instructions、cache-miss
sudo perf top -p 2411                   # 实时看热点函数(需要符号)
sudo perf record -F 99 -g -p 2411 -- sleep 10
sudo perf report --stdio | head -40

perf stat 给总量,perf top 给实时热点,perf record 采样后离线分析。-F 99 是采样频率,-g 记录调用栈;频率越高开销越大,生产环境从 99 Hz 起步。perf 需要 perf_event_paranoid 允许,普通用户常需 sudo;容器里还受 CAP_PERFMON 和宿主 perf_event_paranoid 限制。

perf report 的 Overhead 是采样占比,不是精确 CPU 百分比;Children 是把子调用累计进来。看热点先看 Children 高的调用链,再看 Self 高的叶子函数。没有符号时显示地址,装 linux-image-$(uname -r)-dbgsym 或应用的 debug 包才有函数名。

$ mpstat -P ALL 1 1
Linux 6.8.0-45-generic (lab) 	03/02/2025 	_x86_64_	(8 CPU)
​
10:53:01 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
10:53:02 AM  all    3.12    0.00    1.05    0.31    0.00    0.12    0.00    0.00    0.00   95.40
10:53:02 AM    0    0.00    0.00    0.50    0.00    0.00    0.00    0.00    0.00    0.00   99.50
10:53:02 AM    3   24.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00    0.00   75.00
$ pidstat -u 1 1
Linux 6.8.0-45-generic (lab) 	03/02/2025 	_x86_64_	(8 CPU)
10:53:01 AM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
10:53:02 AM  1000      2411   23.00    1.00    0.00    0.00   24.00     3  app
$ sudo perf stat -p 2411 sleep 2
 Performance counter stats for process id '2411':
          2,004.12 msec task-clock                #    0.996 CPUs utilized
               312      context-switches          #  155.679 /sec
                 4      cpu-migrations            #    1.996 /sec
            184,221      page-faults               #   91.921 K/sec
      6,812,441,203      cycles                    #    3.399 GHz
      9,120,553,118      instructions              #    1.34  insn per cycle
​
      2.012 seconds time elapsed

all 行平均 3% 而 3 号核 24%,说明负载集中在单核;这时 mpstat 的平均值没有意义,必须看 -P ALL。pidstat 把 24% 归到 PID 2411,%wait 为 0 说明它不是在等 CPU。perf stat 的 instructions per cycle 只有 1.34,偏低,常见于分支预测失败多、内存访问不规则或锁竞争;context-switches 多则可能是锁或 I/O 等待。

CPU 高的常见原因:死循环/正则回溯、锁自旋、频繁 GC、系统调用过多、软中断集中在一个核。perf 能看到前四个;软中断用 mpstat -I SUM 和 /proc/softirqs 看。

0.14.3 内存:free 的 available 才是答案

内存问题分「真泄漏」「缓存占满」「回收抖动」三类:

free -h
vmstat 1 5
ps -eo pid,rss,vsz,comm --sort=-rss | head
sudo slabtop -o | head
cat /proc/meminfo | grep -E 'MemAvailable|Dirty|Writeback|Slab'

free 的 buff/cache 是页缓存、目录项、inode 缓存,可回收;available 是内核估算的「不换出也能分配」的量。available 低、si/so 非零,说明系统在换页,延迟会明显上升。

ps 的 RSS 是常驻物理内存,VSZ 是虚拟地址空间;RSS 高不一定是泄漏,可能是 mmap 文件或共享库。判断泄漏看趋势:同一进程 RSS 持续上涨、GC 后不回落、/proc/PID/smaps_rollup 的 Anonymous 持续涨,才像泄漏。

slabtop 看内核对象缓存;SUnreclaim 高可能是内核泄漏(驱动、文件系统)。/proc/meminfo 的 Dirty/Writeback 高说明回写压力大,配合 iostat 看磁盘。

$ grep -E '^(MemTotal|MemFree|MemAvailable|AnonPages|Cached|Slab|SReclaimable|SUnreclaim|Dirty|Writeback):' /proc/meminfo
MemTotal:       16384000 kB
MemFree:         8300000 kB
MemAvailable:   11500000 kB
AnonPages:       2100000 kB
Cached:          4300000 kB
Slab:             420000 kB
SReclaimable:     310000 kB
SUnreclaim:       110000 kB
Dirty:              2048 kB
Writeback:             0 kB

AnonPages 是匿名页(堆、栈、匿名 mmap),持续上涨才像泄漏;Cached 是文件页缓存,可回收;Slab 的 SReclaimable 可回收,SUnreclaim 不可回收,后者持续涨要查驱动/文件系统。Dirty 是待回写页,Writeback 是正在回写页;两者高且 iostat 的 w_await 高,说明回写跟不上。

OOM 发生时内核日志会有 Out of memory: Killed process ...,cgroup v2 还会在 memory.events 里记 oom_kill:

sudo dmesg -T | grep -iE 'out of memory|killed process'
cat /sys/fs/cgroup/<group>/memory.events

容器里的 OOM 不一定杀容器,可能只杀容器内的进程;docker inspect 的 OOMKilled 和 memory.events 要一起看。vm.overcommit_memory、vm.swappiness 是可调参数,但调之前先确认是「真的不够」还是「缓存/回收策略」问题。

0.14.4 磁盘:iostat 的 await 与 aqu-sz

磁盘慢分「带宽满」「IOPS 满」「延迟高」三种,iostat -xz 1 的列分别对应:

列含义什么时候是问题
r/s、w/s每秒读/写请求数接近设备 IOPS 上限
rkB/s、wkB/s每秒读/写字节接近带宽上限
r_await、w_await平均等待时间(ms)明显高于基线,且 aqu-sz 上升
aqu-sz平均队列长度持续大于 1–2,说明排队
%util设备忙时间占比单队列盘有意义;NVMe 可能失真

await 高、aqu-sz 高、%util 接近 100%,是典型的饱和。%util 100% 但 await 很低,可能是小 IO 把设备占满但每次很快,不一定是瓶颈。NVMe 的并行队列让 %util 很难到 100%,要看 aqu-sz 和应用的延迟。

iostat -xz 1
sudo iotop -oPa
pidstat -d 1

iotop -oPa 只显示有 I/O 的进程,-P 按进程、-a 累计;pidstat -d 给每个进程的读写速率。找到进程后,用 strace -c -p PID 看它发了哪些 I/O 系统调用,或者 perf 看调用栈。

$ iostat -xz 1 2 | tail -6
Device            r/s     rkB/s   r_await   w/s     wkB/s   w_await  aqu-sz  %util
nvme0n1         420.0  16800.0      1.20  180.0   7200.0      2.80    1.42   38.0
$ sudo iotop -oPa -n 2 -d 1 | head -5
Total DISK READ:       120.00 K/s | Total DISK WRITE:        80.00 K/s
  PID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 2411 be/4 app      120.00 K/s   80.00 K/s  0.00 %  2.10 %  app

r_await 1.2 ms、w_await 2.8 ms、aqu-sz 1.42、%util 38%:队列里有约 1.4 个请求在等,但延迟还不算离谱。如果 await 涨到几十毫秒、aqu-sz 持续大于 2,才是磁盘饱和。iotop 的 IO> 列是该进程等待 I/O 的时间占比,配合 PID 直接定位。

文件系统层的问题不在 iostat 里:df -h/df -i 看空间和 inode(0.10),mount 看是否 noatime、是否 sync,dmesg 看 I/O error。O_DIRECT、fsync 频繁、小文件元数据操作都会让「磁盘看起来不快」,但 iostat 的吞吐可能很低。

0.14.5 网络与调度延迟

网络性能先看带宽、重传、丢包,再看应用协议:

sar -n DEV 1 5
ss -s
ss -ti state established
nstat -az | grep -E 'TcpRetrans|TcpExtTCPLostRetransmit|TcpExtListenDrops'
ethtool -S enp0s31f6 | grep -iE 'drop|error|miss'

sar -n DEV 给每张网卡的 rxkB/s/txkB/s、rxpck/s/txpck/s、rxdrop/s/txdrop/s。ss -ti 的 retrans 是重传次数,rtt 是往返时间;重传持续增长说明链路丢包或对端窗口小。nstat 的 TcpRetransSegs、TcpExtListenDrops 是累计计数器,适合看趋势。

ethtool -S 的 rx_dropped、tx_dropped、rx_missed_errors 是网卡/驱动层丢包;ip -s link 的 errors/dropped 是协议栈统计。两者都非零时,先查网卡队列、ring buffer、CPU 软中断是否集中。

$ ss -ti state established '( dport = :443 )' | head -4
ESTAB 0 0 192.168.1.20:54321 93.184.216.34:443
	 cubic wscale:7,7 rto:204 rtt:12.4/3.1 ato:40 mss:1460 pmtu:1500
	 retrans:3/120 reordering:3
$ nstat -az | grep -E 'TcpRetransSegs|TcpExtListenDrops|TcpExtTCPLostRetransmit'
TcpRetransSegs                 1240  0.0
TcpExtListenDrops              3     0.0
TcpExtTCPLostRetransmit        72    0.0

retrans:3/120 表示当前这条连接重传 3 次、累计重传 120 次(不是发送段数);rtt:12.4/3.1 是 12.4 ms 平滑 RTT、3.1 ms 偏差,抖动偏大。TcpRetransSegs 是累计重传段数,单看总数没意义,要看增速;ListenDrops 增长说明 accept 队列溢出,回到 0.13.3 的 Recv-Q/Send-Q 查应用 accept 速度。

延迟尖刺不一定是网络:perf sched latency 看调度延迟,bpftrace 的 runqlat 看运行队列等待。CPU 被限流(cgroup cpu.max)、内存回收、锁竞争都会表现为「应用偶尔卡一下」。

sudo perf sched record -a -- sleep 10
sudo perf sched latency -s max
sudo bpftrace -e 'tracepoint:sched:sched_wakeup { @[comm] = count(); }'

0.14.6 追踪:strace、perf trace、bpftrace

系统调用追踪回答「进程在等什么」。strace -c 先看统计,再决定要不要逐条看:

strace -c -T -f -p 2411          # 统计每个 syscall 的次数、耗时、错误
strace -T -yy -e trace=%file -p 2411   # 只看文件相关,显示 fd 路径
strace -f -e trace=network -s 256 -p 2411

-c 是汇总,-T 是每次调用耗时,-f 跟子进程,-yy 把 fd 翻译成路径/socket,-e trace= 过滤类别。strace 的开销很大,高并发服务上可能让延迟翻倍;生产上先 -c 采样几秒,或者用 perf trace/bpftrace。

perf trace 比 strace 轻,输出格式类似:

sudo perf trace -p 2411 -- sleep 5
sudo perf trace -e openat,connect -p 2411

bpftrace 用 eBPF 在内核里聚合,开销比 strace 小,适合看分布:

# 按进程统计 openat 次数
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }'
# 统计 read 的返回字节分布
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ { @bytes = hist(args->ret); }'

bpftrace 需要内核 BTF/头文件和 root;容器里要 --privileged 或相应 capability。脚本写错可能拖慢系统,先在测试机验证,生产上限制运行时间和输出量。

0.14.7 症状→工具→证据矩阵

症状先看工具关键证据下一步
CPU 高哪个进程/哪个核top、pidstat -u、mpstat -P ALL%usr/%sys 高,单核打满perf top/record 找函数
load 高但 CPU 空闲是否有 D 状态vmstat 1、ps -eo statb 列大、wa 高iostat、NFS、/proc/PID/stack
内存持续上涨是缓存还是匿名页free、ps rss、smaps_rollupavailable 降、Anonymous 涨堆分析、GC 日志、泄漏复现
OOM谁被杀、哪个 cgroupdmesg、memory.eventsKilled process、oom_kill限制/扩容、查泄漏
磁盘慢带宽、IOPS 还是延迟iostat -xz、iotop、pidstat -dawait/aqu-sz 高找进程、查文件系统/RAID
磁盘满空间还是 inodedf -h、df -i、lsof +L1Use%/IUse%、(deleted)清理、扩容、重开日志
网络慢带宽、重传、丢包sar -n DEV、ss -ti、nstatretrans 增长、drop 计数mtr、tcpdump、查网卡
延迟尖刺调度还是锁perf sched、bpftrace runqlat运行队列等待长调 cgroup/亲和、查锁
应用慢用户态还是内核态perf、strace -c%sys 高、syscall 耗时优化调用、缓存、批处理
启动慢哪个 unitsystemd-analyze blame/critical-chain关键链上的 unit0.12 的 unit 优化

这张表说明:同一个「慢」字,证据不同,工具链也不同。先用一行命令把症状归类,再沿着表往下走;不要同时开五个工具,输出会互相干扰。

0.14.8 把采集固化成脚本

一次诊断的命令很容易丢,下次遇到同样问题又要重来。写一个采集脚本,固定输出目录、时间戳、元数据和各项指标:

#!/usr/bin/env bash
# collect.sh — 采集一次系统快照,保存到带时间戳的目录
set -Eeuo pipefail
out=${1:-/var/tmp/sysreport-$(date +%Y%m%dT%H%M%S)}
mkdir -p "$out"
exec > >(tee "$out/summary.txt") 2>&1

printf '== %s ==\n' "$(date -Is)"
uname -a
printf '\n== cpu ==\n'; nproc; uptime; mpstat -P ALL 1 1 || true
printf '\n== memory ==\n'; free -h; vmstat 1 3
printf '\n== disk ==\n'; df -hT; df -i
printf '\n== io ==\n'; iostat -xz 1 2 || true
printf '\n== network ==\n'; ip -br addr; ip route; ss -s; ss -lntp
printf '\n== top ==\n'
ps -eo pid,ppid,stat,%cpu,%mem,rss,comm --sort=-%cpu | head -15
printf '\n== kernel ==\n'; dmesg -T | tail -50 || true
printf '\nreport: %s\n' "$out"

关键点在 exec > >(tee ...) 2>&1:所有输出同时进终端和文件,不需要给每条命令加重定向。|| true 让缺少 mpstat/iostat 的机器也能跑完,不会因为一个工具没装就中断。输出目录带时间戳,两次采集可以 diff -u 对比。

$ chmod +x collect.sh && ./collect.sh /var/tmp/report-a
== 2025-03-02T10:56:20+00:00 ==
Linux lab 6.8.0-45-generic #45-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
​
== cpu ==
8
 10:56:20 up 12 days,  3:41,  2 users,  load average: 1.24, 0.98, 0.87
...
== top ==
    PID    PPID STAT %CPU %MEM   RSS COMMAND
   2411       1 Ssl  24.0  2.1 341200 app
...
report: /var/tmp/report-a
$ diff -u /var/tmp/report-a/summary.txt /var/tmp/report-b/summary.txt | head -12
--- /var/tmp/report-a/summary.txt	2025-03-02 10:56:20
+++ /var/tmp/report-b/summary.txt	2025-03-02 11:26:20
@@ -12,7 +12,7 @@
- 10:56:20 up 12 days,  load average: 1.24, 0.98, 0.87
+ 11:26:20 up 12 days,  load average: 3.80, 2.10, 1.40
@@ -28,5 +28,5 @@
-   2411       1 Ssl  24.0  2.1 341200 app
+   2411       1 Ssl  68.0  2.1 341200 app

两次快照的 diff 直接给出变化:load 从 1.24 涨到 3.80、同一 PID 的 CPU 从 24% 涨到 68%,RSS 没变,说明不是内存泄漏而是 CPU 变忙。没有基线时,这些数字只是孤立的读数;有了同机、同格式的两次快照,才能判断趋势。

sysstat 的历史数据比一次性快照更有用。确认 sysstat 已启用:

systemctl status sysstat
ls /var/log/sysstat/
sar -u -f /var/log/sysstat/sa02 | tail

sa02 是每月 2 号的二进制采样文件,sar -u -f 回放 CPU,sar -n DEV -f 回放网络。默认每 10 分钟采样一次,保留 7 天(/etc/sysstat/sysstat 可改)。事故发生后,先看 sar 的历史曲线,再用 collect.sh 抓现场。

0.14.9 命令行工程化:基准、格式化与 dotfiles

性能优化要用数据说话,hyperfine 比 time 更适合做命令基准:

hyperfine --warmup 3 --runs 10 \
    'grep -r "needle" /usr/share/doc' \
    'rg "needle" /usr/share/doc'

hyperfine 会自动多次运行、报告均值/标准差/离群值,并在命令间做统计比较;--warmup 预热缓存,--prepare 在每次运行前重置状态。time 只给一次结果,受缓存和调度影响大,不适合比较。

脚本质量靠三件套:bash -n 语法检查、shellcheck -x 静态检查、shfmt -i 4 -ci -sr 格式化。它们能进 pre-commit 或 CI,和 0.8 的 bats 一起构成最小质量门。shellcheck 的规则号和禁用方式在 0.8.7 已经讲过,这里只强调:不要全局 disable,每条禁用都写理由。

dotfiles 用 git 裸仓库管理,不需要额外工具:

git init --bare "$HOME/.dotfiles"
alias dot='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
dot config status.showUntrackedFiles no
dot add .bashrc .vimrc .config/nvim
dot commit -m 'init dotfiles'

换机器时 git clone --bare <repo> ~/.dotfiles,再 dot checkout。.gitignore 里写 *、用 dot add -f 显式添加,避免把整个 home 纳入版本控制。stow 适合「一个目录一个包」的布局,direnv 管项目级环境变量,just/make 管常用命令入口;工具不在多,能复现就行。

性能与工具的六个误解
  1. 「load average 就是 CPU 使用率」——Linux 的 load 包含不可中断睡眠(D),NFS/磁盘卡死会让 load 很高而 CPU 空闲。
  2. 「free 的 free 小就是内存不足」——buff/cache 可回收,available 才是可用估计;只看 free 会误判。
  3. 「%util 100% 就是磁盘瓶颈」——单队列盘才有较强相关性;NVMe 多队列下要看 aqu-sz、await 和应用延迟。
  4. 「strace 开销很小,可以长期挂」——每次 syscall 都陷入 ptrace,开销可能让延迟翻倍;生产优先 perf trace/bpftrace。
  5. 「iostat 第一行就是当前值」——第一行是自开机以来的平均,区间值从第二次输出开始。
  6. 「性能问题加 CPU/内存就能解决」——先定位是利用率、排队还是错误;加资源可能只是把瓶颈推到下一层。

0.14.10 PSI 与 cgroup:把「压力」变成数字

load average 和 %util 都是间接指标,真正的排队发生在内核的等待队列里。PSI(Pressure Stall Information)直接统计「有多少时间有任务因为这类资源而卡住」,比利用率更接近用户体验。

cat /proc/pressure/cpu
# some avg10=12.34 avg60=8.10 avg300=3.20 total=12345678
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
cat /proc/pressure/io
# some avg10=4.20 avg60=1.30 avg300=0.40 total=456789
# full avg10=1.10 avg60=0.30 avg300=0.10 total=98765

some 表示「至少有一个任务被卡住」的时间占比,full 表示「所有可运行任务都被卡住」的时间占比。full 只有 memory 和 io 两行;CPU 的 full 在较新内核才有(5.13+),旧内核只有 some。

指标含义怎么用
avg10最近 10 秒看当前是否正在卡
avg60 / avg3001/5 分钟看趋势,避免瞬时抖动
total累计微秒两次采样相减算区间占比
full全员卡住最接近「服务不可用」

PSI 的价值在「高利用率但低压力」与「低利用率但高压力」的区分:CPU some 很高但 full 为 0,说明有任务在排队但仍有任务在跑;io full 持续大于 0,说明所有任务都在等 I/O,磁盘已经是硬瓶颈。

PSI 需要内核 CONFIG_PSI=y,有些发行版默认关闭,可用 psi=1 启动参数打开:

grep -E 'psi|CONFIG_PSI' /boot/config-$(uname -r) 2>/dev/null || true
dmesg | grep -i psi

cgroup v2 把 PSI 下放到每个控制组:/sys/fs/cgroup/<path>/{cpu,memory,io}.pressure。这让你能回答「是哪个服务在制造压力」,而不是只看整机。

cgroup v2 的关键文件:

文件内容读法
cpu.maxquota period,如 50000 1000000.5 个 CPU
cpu.weight1–10000,默认 100相对权重,不是上限
cpu.statusage_usec、nr_throttled、throttled_usec节流次数/时长
memory.current当前用量含 page cache
memory.max硬上限超过触发 cgroup OOM
memory.high软上限超过先节流回收
memory.eventshigh、max、oom、oom_killOOM 证据
memory.statanon、file、slab、pgscan、pgsteal拆解用量
io.stat每设备的 rbytes、wbytes、rios、wios谁在做 I/O
io.maxrbps、wbps、riops、wiopsI/O 限速

cpu.max 的 quota 是「每个 period 可用多少微秒」;50000 100000 是 50% 的单核。cpu.stat 的 nr_throttled 增长说明被限流,throttled_usec 是累计被 throttled 的时间。容器里看到 cpu.max 很小、nr_throttled 很大,应用延迟高就不是代码问题,是配额。

memory.high 与 memory.max 的区别:前者让内核提前回收、放慢分配,后者直接 OOM。生产上先用 memory.high 做软限制,留出缓冲;memory.max 只比实际用量高一点,防止单容器拖垮整机。

查看某个 systemd 服务的 cgroup:

systemctl status nginx.service | grep -E 'Memory|Tasks|CPU'
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
cat /sys/fs/cgroup/system.slice/nginx.service/memory.events
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
systemd-cgtop -m

路径随 cgroup 层级变化:systemd 通常在 /sys/fs/cgroup/system.slice/<unit>/,用户服务在 user.slice/user-1000.slice/user@1000.service/。容器运行时(Docker、containerd)有自己的层级,docker inspect 的 CgroupParent 能给路径。

临时给命令加限制,不写 unit:

systemd-run --user --scope -p MemoryMax=512M -p CPUQuota=50% \
    bash -c 'stress-ng --vm 2 --vm-bytes 400M --timeout 30s'

--scope 在当前会话里同步运行,--service 则创建独立 unit。MemoryMax 触发 OOM 时,journalctl 会出现 oom-kill,systemctl status 的 Result=oom-kill 也是线索。

0.14.11 内存:RSS、PSS、USS 与回收

ps 的 RSS 把共享页重复计算:同一个 libc 页被 50 个进程映射,50 个进程的 RSS 都各算一次。看「谁真的占内存」要用 PSS/ USS。

指标含义共享页怎么算来源
RSS常驻集大小每个进程都算一份/proc/PID/statm、ps
PSS按共享进程数分摊除以共享者数量/proc/PID/smaps_rollup
USS独占内存只算本进程私有smaps_rollup 的 Private_*
Swap被换出的页单独统计smaps_rollup
AnonHugePagesTHP 匿名大页计入 RSSsmaps_rollup
grep -E '^(Rss|Pss|Pss_Anon|Pss_File|Pss_Shmem|Private_Clean|Private_Dirty|Swap|Anonymous|AnonHugePages):' \
    /proc/$(pgrep -n nginx)/smaps_rollup
smem -tk | head -10          # 需要 apt install smem
ps_mem -p -s                 # 需要 apt install ps_mem
pmap -x 2411 | tail -1

smaps_rollup 是内核 4.14+ 提供的聚合视图,比逐个读 smaps 快得多;它只对目标进程有权限时可见。smem 的 USS/PSS 列比 ps 的 RSS 更接近「加内存能省多少」。

free 的 available 来自内核的 MemAvailable 估算:它把大部分 page cache 与可回收 slab 算作可用,但不包括正在回写、被锁页、不可回收的 slab。available 长期低于 5%、si/so 非零、pgscan_direct 增长,才是内存压力。

grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapCached|Active|Inactive|Dirty|Writeback|AnonPages|Slab|SReclaimable|SUnreclaim|PageTables|Committed_AS|CommitLimit' /proc/meminfo
vmstat -s | grep -E 'pages scanned|pages reclaimed|page faults'
sar -B 1 5
计数器含义异常信号
pgscan_direct进程直接触发回收持续增长:分配速度超过回收
pgsteal实际回收的页与 pgscan 比例低:回收困难
pgmajfault主缺页(要读盘)高:内存不足或 mmap 文件被换出
workingset_refault回收后又被访问高:工作集大于内存
allocstall分配时直接回收停顿高:内存压力已影响分配

Committed_AS 是「所有进程承诺但未必使用的内存」;CommitLimit 是 overcommit 策略下的上限。vm.overcommit_memory=2 时,Committed_AS 接近 CommitLimit 会让 malloc 失败,即使 free 还有很多——这是另一种「内存不足」。

OOM 的证据链:

dmesg -T | grep -iE 'oom|killed process'
journalctl -k -b | grep -i oom
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj
cat /sys/fs/cgroup/.../memory.events

oom_score 是内核启发式打分(0–1000),oom_score_adj 是管理员偏移(-1000 到 1000)。-1000 表示「尽量不杀」,但不是绝对免疫;cgroup OOM 只看组内,memory.oom.group=1 会整组一起杀。

slabtop -s c 看内核对象缓存;SUnreclaim 高且持续增长,可能是内核泄漏(dentry、inode、网络连接跟踪),不是用户进程。drop_caches 能验证「是不是缓存占着」,但生产上不要用它「优化内存」:清掉缓存只会让接下来的读更慢。

THP(透明大页)能减少 TLB miss,也会带来分配延迟与内存碎片:

cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
grep -E 'AnonHugePages|ShmemPmdMapped' /proc/meminfo

enabled 的 [always] 表示全局启用,[madvise] 表示只对显式 madvise(MADV_HUGEPAGE) 的区域启用。数据库、JVM 这类延迟敏感进程通常建议 madvise,避免 khugepaged 在后台合并时造成抖动。

0.14.12 块层:从 %util 到真实延迟

块 I/O 的路径是:应用 read/write → VFS → page cache → 块层 → I/O 调度器 → 设备队列 → 驱动 → 盘。每一层都可能排队,iostat 看到的是块层及其以下的合并结果。

iostat -x 的核心字段:

字段含义单位怎么读
r/s / w/s每秒读/写请求数次IOPS
rkB/s / wkB/s每秒读/写字节KiB吞吐
rareq-sz / wareq-sz平均请求大小KiB顺序/随机线索
r_await / w_await平均等待(含排队)ms应用感受到的延迟
aqu-sz平均队列长度个排队严重程度
%util设备忙的时间占比%单队列盘参考,NVMe 失真

await = 排队时间 + 服务时间。aqu-sz 高、await 高、%util 高:设备饱和;aqu-sz 低、await 高:单次请求慢(盘慢、驱动、远端的 NFS/云盘);%util 高、aqu-sz 低、await 低:只是有持续的小 I/O,不一定是瓶颈。

Little’s law 可以交叉验证:aqu-sz ≈ IOPS × await(单位换算后)。如果 iostat 的三个数对不上,先怀疑采样窗口或字段单位,而不是设备。

%util 的计算基于 io_ticks(设备队列非空的时间),不是「带宽利用率」。NVMe 有多队列、可以并行处理成百上千个请求,%util 100% 只表示「采样窗口内队列一直非空」,不代表不能更快。判断 NVMe 要看 aqu-sz、await 和应用 P99。

I/O 调度器:

cat /sys/block/nvme0n1/queue/scheduler
cat /sys/block/sda/queue/scheduler
cat /sys/block/sda/queue/nr_requests
cat /sys/block/sda/queue/read_ahead_kb
调度器特点适用
none/noop基本不排序,直接下发NVMe、SSD、虚拟盘
mq-deadline保证读延迟上限通用、数据库
kyber按延迟目标节流低延迟设备
bfq按 cgroup/进程公平分配带宽桌面、多租户限速

调度器是 none 不代表没有队列,设备本身仍有 nr_requests。改调度器要写 udev 规则或 tuned,不要 echo 到 sysfs 就以为持久。

抓单次 I/O 的延迟分布:

sudo blktrace -d /dev/nvme0n1 -o - | blkparse -i -
sudo bpftrace -e 'tracepoint:block:block_rq_complete { @us = hist((nsecs - @start[args->dev, args->sector]) / 1000); }'

第二条依赖 bpftrace 与块层 tracepoint,写法只是示意;生产用现成的 biolatency.bt:

sudo bpftrace /usr/share/bpftrace/tools/biolatency.bt

它输出按毫秒分桶的直方图,能看出「大多数 I/O 很快,但尾部有 100 ms 尖刺」。平均值会掩盖尾部,P99 才是用户感知。

fio 是可控压测工具,不要在生产盘上直接跑:

fio --name=randread --rw=randread --bs=4k --iodepth=32 --numjobs=4 \
    --runtime=30 --time_based --group_reporting \
    --filename=/var/tmp/fio.test --size=1G

iodepth 是队列深度,numjobs 是并发任务数;两者相乘是总并发。--direct=1 绕过 page cache,测的是设备而不是内存。压测时同步看 iostat -x 1,把 fio 的 IOPS/延迟与 iostat 的 r/s/await 对照,能判断瓶颈在设备还是文件系统。

0.14.13 打开文件取证:lsof 与 /proc/PID/fd

0.10.3 的「df 满、du 小」在这里落地:文件被删后目录项没了,但只要还有进程持有 fd,inode 和数据块就不会释放。lsof 把 /proc/<pid>/fd、/proc/<pid>/maps、/proc/net/tcp 拼成一张「谁打开了什么」的表;它不是内核接口,而是 /proc 的视图,所以权限和 namespace 决定了它能看见谁。

数据源提供什么读取条件
/proc/<pid>/fdfd → inode/路径同 uid 或 root(受 Yama 限制)
/proc/<pid>/mapsmmap 的文件同上
/proc/net/tcp、tcp6、unixsocket 四元组通常全局可读
/proc/<pid>/mountinfo挂载与 namespace同上

lsof 的 FD 列把「打开方式」和「fd 编号」合在一起:

FD含义
cwd当前工作目录
rtd根目录
txt可执行文件
memmmap 的文件
0u / 1w / 2wstdin/stdout/stderr;r 读、w 写、u 读写
3r / 4w普通 fd 与打开模式
IPv4 / IPv6 / unixsocket(TYPE 列)

删除的文件不一定在 FD 列标出来,但 NAME 末尾会有 (deleted),+L1 输出还会多一个 NLINK 列(值为 0)。看到这两个信号,空间就不是「被目录占着」,而是「被 fd 占着」。

常用选项:

选项作用注意
-p PID按进程-p 1,2411 可多个
-u user按用户-u ^root 排除
-c name按命令名支持通配
-i [46][proto][@host][:port]网络文件-iTCP:8080
-sTCP:LISTEN过滤 TCP 状态常与 -i 连用
-nP不解析主机名/端口名排障必加,避免反向 DNS 卡住
-a多条件取交集默认是并集
+d dir目录下一层不递归
+D dir递归目录很慢,先缩小范围
+L1链接数 < 1已删除仍打开
-d 0,1,2指定 fd配 -a 用
-t只输出 PID适合 kill/脚本
-F字段化输出脚本解析用
+c 0不截断 COMMAND默认截到 9 字符
-b避免阻塞调用NFS stale 时用

「空间去哪了」的取证顺序:

df -h /var
# 只列 link count < 1 且大小非零的项;NLINK 列在 SIZE/OFF 之后
sudo lsof -nP +L1 | awk 'NR==1 || $7+0 > 0'
sudo lsof -nP +L1 | sort -k7 -nr | head

# 确认是哪个 fd 后,截断它;不要 rm 一个已经不在目录里的名字
sudo truncate -s 0 /proc/2411/fd/7
# 或者: : | sudo tee /proc/2411/fd/7 >/dev/null
df -h /var

truncate 走的是 fd,不是路径,所以对已删除文件有效;前提是该 fd 可写。对只读打开的 fd 会失败,正确做法是让进程重开日志(systemctl reload nginx)或重启进程。如果进程不是以 O_APPEND 打开,fd 的写偏移仍停在原处,下一次写会把文件撑成稀疏文件;O_APPEND 的进程则从新的末尾继续写。生产上优先用「重开文件」的轮转方式(0.12.12),而不是长期依赖截断 fd。

「卸载不掉」的取证:

findmnt -T /mnt/data
sudo lsof +D /mnt/data | head -20      # 递归,目录大时慢
sudo fuser -vm /mnt/data              # 按挂载点找进程
sudo umount /mnt/data

fuser -vm 直接按挂载点聚合,比 lsof +D 快;lsof +D 能看到具体文件,适合确认「是哪个日志/临时文件」。两者都看不到内核线程持有的引用(如 loop 设备、挂起的 NFS),这时 umount -l 只是把挂载点从命名空间摘掉,不是真正卸载。

「谁在监听/连谁」:

ss -lntp 'sport = :8080'
sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
sudo lsof -p 2411 -a -d 0,1,2
sudo lsof -nP -i @10.0.2.15
需求首选工具理由
谁占着这个文件lsof /path路径 → 进程
这个进程打开了什么lsof -p PID进程 → 文件
谁占着挂载点fuser -vm按挂载点聚合,快
谁在监听端口ss -lntp直接读内核 socket 表
已删除但仍打开lsof +L1唯一直接证据
原始 fd/偏移ls -l /proc/PID/fd不依赖 lsof

lsof 的五个坑:

  1. 对已删除文件 rm 同名路径——目录项已经没了,rm 找不到;空间由 fd 持有,只能截断 fd 或重启进程。
  2. 容器里看不到宿主进程——lsof 读的是当前 PID namespace 的 /proc;在容器里查宿主,用 nsenter -t <pid> -m -p lsof ... 或在宿主上查。
  3. +D 递归扫描很慢——大目录会扫几分钟;先用 +d 看一层,或直接 fuser -vm。
  4. COMMAND 被截断——默认只显示 9 个字符,+c 0 显示全名;grep 前先确认没被截。
  5. NFS 上 lsof 卡住——stale 句柄会阻塞在 stat;加 -b 跳过可能阻塞的调用。

lsof 与 ss 的边界:ss 读内核 socket 表,快且能看到 Recv-Q/Send-Q/retrans;lsof 把 socket 映射到进程和 fd,适合回答「这个端口是哪个进程、哪个 fd」。两者一起用:ss 定位连接,lsof -p 定位 fd。

0.14.14 perf 与 bpftrace:采样、火焰图与开销

perf 基于硬件/软件事件采样,适合回答「CPU 时间花在哪些函数」。bpftrace 基于 eBPF,适合回答「某个事件发生了多少次、延迟分布如何」。两者都不是「零开销」。

perf stat 先给全局画像:

perf stat -p <pid> -- sleep 10
perf stat -e cycles,instructions,cache-references,cache-misses,branches,branch-misses,context-switches,cpu-migrations,page-faults -p <pid> -- sleep 10
perf stat -a -e cycles,instructions -- sleep 5
事件含义异常线索
cycles / instructions周期/指令数IPC = instructions/cycles
cache-misses末级缓存 miss高:内存访问模式差
branch-misses分支预测失败高:分支密集/随机
context-switches上下文切换高:锁竞争/阻塞
cpu-migrationsCPU 迁移高:亲和性/负载均衡
page-faults缺页高:内存分配/映射

IPC 低(<1)不一定是 CPU 慢,可能是内存墙、分支预测失败、锁等待。perf stat 要和 perf record 一起看,不要只凭一个数下结论。

采样生成火焰图:

perf record -F 99 -g -p <pid> -- sleep 30
perf report --stdio --no-children | head -30
perf script > /tmp/perf.script
# 需要 FlameGraph 仓库
stackcollapse-perf.pl /tmp/perf.script > /tmp/perf.folded
flamegraph.pl /tmp/perf.folded > /tmp/perf.svg

-F 99 是 99 Hz,避免与定时器同频采样;-g 采调用栈;--call-graph dwarf 在栈被优化后更可靠,但开销更大。perf report --no-children 看自身开销,--children 看包含子调用的累计,两者回答不同问题。

perf 的权限由 perf_event_paranoid 控制:

cat /proc/sys/kernel/perf_event_paranoid
# 2:只允许用户态采样;1:允许内核态;0/-1:更宽

生产上不要长期设成 -1;用 perf record 短时采样,或给特定二进制 setcap cap_perfmon+ep。内核符号看不到时,检查 kptr_restrict 与 kernel.perf_event_paranoid。

perf trace 是 strace 的低开销替代:

perf trace -p 2411 -- sleep 5
perf trace -e openat,connect -p 2411

它基于 tracepoint,不做 ptrace 逐调用暂停,开销比 strace 小,但不如 strace 直观(参数解码较少)。

bpftrace 的一行式:

# 按进程统计 openat 次数
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }'

# 某进程的用户态栈采样
sudo bpftrace -e 'profile:hz:99 /pid == 1234/ { @[ustack] = count(); }'

# 每 5 秒打印并清空
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_read { @reads[comm] = count(); } interval:s:5 { print(@reads); clear(@reads); }'

profile:hz:99 是定时采样,tracepoint 是事件计数,kprobe 是内核函数入口。kprobe 的开销取决于命中频率,热路径上挂 kprobe 可能显著拖慢系统;先 -l 确认事件名,再用窄过滤(/pid == .../)限制范围。

现成工具在 /usr/share/bpftrace/tools/:

工具回答依赖
biolatency.bt块 I/O 延迟分布block tracepoint
runqlat.bt运行队列等待时间sched tracepoint
offcputime.bt阻塞在哪些栈sched switch
cachestat.btpage cache 命中/回收内核函数
execsnoop.bt谁在执行新程序execve
opensnoop.bt谁打开了哪些文件openat

ftrace 是内核自带的追踪器,不需要额外装包:

sudo trace-cmd record -p function_graph -l vfs_read -- sleep 2
sudo trace-cmd report | head -40

function_graph 能看到调用链与每段耗时,但开销大、输出多;适合短时间定位,不适合长期开。

0.14.15 三条性能取证脚本

0.14.15.1 压力快照

#!/usr/bin/env bash
# pressure-watch.sh [秒数] —— PSI + cgroup + D 状态进程
set -euo pipefail
dur=${1:-10}

echo "== PSI"
for r in cpu memory io; do
    echo "-- $r"
    cat "/proc/pressure/$r" 2>/dev/null || echo "PSI unavailable"
done

echo "== cgroup (self)"
cg=$(awk -F: '$1=="0" {print $3}' /proc/self/cgroup)
base="/sys/fs/cgroup${cg}"
for f in cpu.stat memory.current memory.events io.stat; do
    [[ -r $base/$f ]] && { echo "-- $f"; cat "$base/$f"; }
done

echo "== D-state"
ps -eo stat,pid,ppid,wchan:24,comm | awk 'NR==1 || $1 ~ /^D/'

echo "== top cpu"
ps -eo pcpu,pmem,rss,pid,comm --sort=-pcpu | head -6

/proc/self/cgroup 的格式是 0::/user.slice/...,awk -F: 取第三段得到路径。cgroup v1 的格式不同(多个 N:controller:path),脚本里只处理 v2;v1 机器要按控制器分别拼路径。

0.14.15.2 内存归因

#!/usr/bin/env bash
# mem-top.sh [N] —— 按 PSS/USS 找内存大户
set -euo pipefail
n=${1:-10}
printf '%-8s %10s %10s %10s %s\n' PID RSS_kB PSS_kB USS_kB COMM
for pid in $(ps -eo pid= --sort=-rss | head -50); do
    f="/proc/$pid/smaps_rollup"
    [[ -r $f ]] || continue
    rss=$(awk '/^Rss:/ {print $2}' "$f")
    pss=$(awk '/^Pss:/ {print $2}' "$f")
    uss=$(awk '/^Private_Clean:|^Private_Dirty:/ {s+=$2} END {print s+0}' "$f")
    comm=$(cat "/proc/$pid/comm" 2>/dev/null || echo '?')
    printf '%-8s %10s %10s %10s %s\n' "$pid" "${rss:-0}" "${pss:-0}" "${uss:-0}" "$comm"
done | sort -k3 -nr | head -$((n + 1))

先按 RSS 取前 50 个候选,再算 PSS/ USS,避免遍历所有进程的 smaps_rollup(每个都要进入内核,进程多时很慢)。Private_Clean + Private_Dirty 是 USS 的近似;Pss_Anon + Pss_File + Pss_Shmem 才是 PSS 的完整拆分。

0.14.15.3 I/O 证据包

#!/usr/bin/env bash
# io-forensics.sh [设备] —— 调度器、队列、iostat、PSI io
set -euo pipefail
dev=${1:-$(lsblk -ndo NAME,TYPE | awk '$2=="disk" {print $1; exit}')}
echo "== device: $dev"
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS "/dev/$dev"
for f in scheduler nr_requests read_ahead_kb rotational; do
    printf '%-16s ' "$f"; cat "/sys/block/$dev/queue/$f" 2>/dev/null || echo n/a
done

echo "== iostat"
iostat -xz 1 3 "/dev/$dev" 2>/dev/null || iostat -xz 1 3

echo "== PSI io"
cat /proc/pressure/io 2>/dev/null || true

echo "== D-state"
ps -eo stat,pid,wchan:24,comm | awk '$1 ~ /^D/'

iostat -p 是「包含分区」开关,不是设备参数;设备名直接跟在后面(如 iostat -x 1 /dev/sda)。脚本用 || 回退到全设备输出。rotational 为 0 通常是 SSD/NVMe,为 1 是机械盘或虚拟盘未声明。

0.14.16 两个实验:CPU 节流与 I/O 延迟

实验一:用 cgroup 制造 CPU 节流,观察 PSI 与 cpu.stat

# 终端 A:启动一个受 20% CPU 限制的忙循环
systemd-run --user --scope -p CPUQuota=20% \
    bash -c 'while :; do :; done' &
scope_pid=$!

# 终端 B:观察
top -b -n 2 -d 1 -p "$scope_pid" | tail -5
cat /proc/pressure/cpu
cg=$(awk -F: '$1=="0" {print $3}' /proc/$scope_pid/cgroup)
cat "/sys/fs/cgroup${cg}/cpu.stat"
cat "/sys/fs/cgroup${cg}/cpu.max"

kill "$scope_pid"

预期:进程的 %CPU 被压到约 20%;cpu.stat 的 nr_throttled 持续增长,throttled_usec 增加;/proc/pressure/cpu 的 some 在忙循环期间升高。cpu.max 显示 20000 100000(20%)。这个实验说明「CPU 用满 100%」和「被配额限制」是两种不同的饱和。

实验二:用 fio 看 await、aqu-sz 与 %util

cd /var/tmp
fio --name=randread --rw=randread --bs=4k --iodepth=32 --numjobs=4 \
    --runtime=20 --time_based --group_reporting --direct=1 \
    --filename=/var/tmp/fio.test --size=512M &
fio_pid=$!
iostat -xz 1 5
wait "$fio_pid"
rm -f /var/tmp/fio.test

预期:fio 报告 IOPS 与 clat 百分位;iostat 在压测期间 r/s 升高、aqu-sz 大于 0、r_await 升高。若 aqu-sz 很高而 %util 不到 100%,说明设备仍有余量但队列在排队;若 await 的 P99 远高于平均,说明尾部延迟才是问题。

没有 fio 时用 dd 做顺序写粗测,但它只测吞吐、不测 IOPS/延迟:

dd if=/dev/zero of=/var/tmp/dd.test bs=1M count=1024 oflag=direct status=progress
rm -f /var/tmp/dd.test

0.14.17 USE 取数表:vmstat、pidstat、perf stat 的字段与误差

前面的工具各讲了一段,这一节把它们压成一张「资源 → 指标 → 采样窗口 → 误判」的表。USE 的每个问题只选一个主指标,再看一个交叉验证指标;指标越多,越容易在噪声里迷路。

资源U(用得多不多)S(排队)E(错误)采样窗口常见误判
CPUmpstat -P ALL 1 的 %usr+%sysvmstat 1 的 r、perf sched 的 runqueuedmesg -T 的 MCE、perf stat 的 throttle10–30 s整机平均掩盖单核打满;VM 里 %steal 不是自己的 CPU
内存free -h 的 availablevmstat 的 si/so、PSI memory 的 fullmemory.events 的 oom_kill60 s把 page cache 当泄漏;容器 memory.current 含 cache
磁盘iostat -xz 的 %util、r/s/w/saqu-sz、awaitdmesg 的 I/O error、SMART10–30 sNVMe 多队列下 %util 失真;await 含排队时间
网络sar -n DEV 1 的 rxkB/s/txkB/sss -ti 的 retrans、nstat 的 dropsip -s link 的 errors/dropped60 s网卡 offload/聚合让计数与抓包对不上

vmstat 的字段常被望文生义,下面这张表按「它到底统计什么」列:

字段含义单位读法/边界
r可运行进程数(running + runnable)个持续大于 nproc 才是 CPU 排队;单核 1.0 也可能正常
b不可中断睡眠(D)个高且 CPU idle 高,先查磁盘/NFS/驱动,不是 CPU
si/so换入/换出KiB/s非零说明有换页;但容器可能禁用 swap,看到 0 不代表没压力
bi/bo块设备读/写KiB/s是块层流量,不是应用读写;page cache 命中时很小
us/sy用户态/内核态 CPU%sy 高看 syscall/软中断;us 高看计算
wa等 I/O 的空闲%是「CPU 没事干在等」,不一定是本地磁盘,可能是 NFS/远端存储
st被 hypervisor 偷走的时间%VM 里持续高说明宿主超卖,加内存/调应用都没用

pidstat 和 perf stat 的关键字段:

字段来源含义读法
%usr/%systempidstat -u用户态/内核态 CPU%system 高先看 syscall、锁、软中断
%waitpidstat -u等 CPU 的时间占比高且 %CPU 不高,说明 CPU 争用,不是 I/O
%CPUpidstat/top进程所有线程之和可以超过 100%;-t 才看单线程
task-clockperf stat任务占用 CPU 的时间CPUs utilized 接近 1 表示约一个核
cycles/instructionsperf stat周期/指令数IPC = instructions/cycles,低可能是内存墙
cache-misses/branch-missesperf stat末级缓存 miss/分支预测失败看比率,不只看绝对数
context-switchesperf stat上下文切换高可能是锁竞争、阻塞 I/O、调度抖动

三个采样陷阱必须记住。第一,vmstat 1 5 的第一行是开机以来的平均,后面的行才是区间值;拿第一行判断当前状态,会把一整天的平均当成这一秒。第二,pidstat 的 %CPU 是进程内所有线程之和,四线程各占 25% 会显示 100%,正好等于一个核;要区分「四线程分摊」还是「单线程跑满」,加 -t 才能看到每个线程。第三,perf stat 的硬件计数器在事件数超过可用计数器时会多路复用,输出里的计数是缩放后的估计值,可能带 (xx.xx%) 标记;要比较两次结果,先看有没有复用,必要时用 --no-scale 或减少事件。

perf record 是采样,perf stat 是计数。99 Hz 采 10 秒只有约 990 个样本;要判断占 1% 的尾部,样本误差可能比信号还大。把 -F 提到 999、延长采样时间,或者先用 perf stat 确认总量,再用 perf record 看热点。perf stat -I 1000 能按秒打印增量,适合看趋势,但间隔太小本身会推高开销。

0.14.18 一次完整取证:triage.sh

0.14.8 的 collect.sh 是「定期快照」,这一节的脚本是「事故现场取证」:只读、带超时、保留工具版本、把 CPU/内存/IO/网络/文件证据按段落写入 summary.txt,元数据单列一份。原则有四条:不覆盖已有目录、每条命令有超时、缺工具不中断、输出可打包带走。

#!/usr/bin/env bash
# triage.sh — 只读取证包:CPU/内存/IO/网络/文件
# 用法: sudo ./triage.sh [-o DIR] [-s SECONDS] [-p PID]
set -Eeuo pipefail
umask 077
out=; dur=10; pid=
while getopts ':o:s:p:h' opt; do
    case $opt in
        o) out=$OPTARG ;;
        s) dur=$OPTARG ;;
        p) pid=$OPTARG ;;
        h) sed -n '2,4p' "$0"; exit 0 ;;
        *) echo "unknown option: -$OPTARG" >&2; exit 2 ;;
    esac
done
[[ $dur =~ ^[0-9]+$ ]] && ((dur >= 1 && dur <= 120)) || { echo "bad -s" >&2; exit 2; }
if [[ -n $pid ]]; then
    [[ $pid =~ ^[0-9]+$ && -d /proc/$pid ]] || { echo "no such pid: $pid" >&2; exit 2; }
fi
out=${out:-/var/tmp/triage-$(date +%Y%m%dT%H%M%S)}
if [[ -e $out ]]; then
    echo "output exists: $out" >&2; exit 2
fi
mkdir -p "$out"
exec > >(tee "$out/summary.txt") 2>&1

section() { printf '\n===== %s =====\n' "$1"; }
have() { command -v "$1" >/dev/null 2>&1; }
{
    date -Is; hostname; uname -a
    printf 'boot_id=%s\n' "$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo n/a)"
    printf 'virt=%s\n' "$(systemd-detect-virt 2>/dev/null || echo n/a)"
    printf 'nproc=%s\n' "$(nproc 2>/dev/null || echo n/a)"
    uptime
    tr '\0' ' ' </proc/cmdline; echo
} | tee "$out/meta.txt"

section load; cat /proc/loadavg
section pressure
for r in cpu memory io; do
    printf -- '-- %s\n' "$r"
    cat "/proc/pressure/$r" 2>/dev/null || echo n/a
done
section memory
grep -E '^(MemTotal|MemFree|MemAvailable|AnonPages|Cached|Dirty|Writeback|Slab|SUnreclaim):' /proc/meminfo
section vmstat; timeout $((dur + 5)) vmstat 1 "$dur" || true
section iostat
if have iostat; then timeout $((dur + 5)) iostat -xz 1 "$dur" || true; else echo n/a; fi
if have mpstat; then section mpstat; timeout 10 mpstat -P ALL 1 2 || true; fi
section tasks
ps -eo pid,ppid,stat,pcpu,pmem,rss,comm --sort=-pcpu | head -15 || true
ps -eo stat,pid,wchan:24,comm | awk '$1 ~ /^D/'
if [[ -n $pid ]]; then
    section "pid $pid"
    timeout $((dur + 5)) pidstat -u -d -r -p "$pid" 1 "$dur" || true
    sed -n '1,30p' "/proc/$pid/status" || true
    cat "/proc/$pid/stack" 2>/dev/null || true
fi
section deleted-open
if have lsof; then
    timeout 30 lsof -b -nP +L1 2>/dev/null | awk 'NR==1 || $NF ~ /\(deleted\)/' | head -20 || true
else
    echo n/a
fi
section sockets; ss -s 2>/dev/null || true; ss -lntup 2>/dev/null | head -30 || true
section netstat
nstat -az 2>/dev/null | grep -E 'TcpRetransSegs|TcpExtListenDrops|TcpExtTCPLostRetransmit|TcpOutSegs' || true
ip -s link 2>/dev/null || true
section journal; journalctl -b -p err -n 100 --no-pager 2>/dev/null || true
section dmesg; dmesg -T 2>/dev/null | tail -100 || true
if have perf; then
    section perf
    if [[ -n $pid ]]; then timeout 20 perf stat -p "$pid" -- sleep 5 || true
    else timeout 20 perf stat -a -- sleep 5 || true; fi
fi
section artifacts
find "$out" -maxdepth 1 -type f -printf '%f %s\n' | sort
printf 'report: %s\n' "$out"

运行方式与读法:

sudo ./triage.sh -s 30 -p 2411 -o /var/tmp/case-2411
# 先看顺序:meta → pressure → vmstat/iostat → tasks → deleted-open → journal
less /var/tmp/case-2411/summary.txt
# 归档,带哈希,防止传递过程中被改
tar czf /var/tmp/case-2411.tar.gz -C /var/tmp case-2411
sha256sum /var/tmp/case-2411.tar.gz

umask 077 是因为报告里可能有命令行参数、文件路径、连接四元组;默认 022 会让同机其他用户读到。timeout 是给 NFS、lsof +L1、perf 这类可能卡住的命令兜底;|| true 保证一个工具缺失不会让整份报告中断。meta.txt 里的 boot_id、nproc、cmdline 是后面解释数据的坐标系:没有 nproc,vmstat 的 r 就没法判断是否排队;没有 boot_id,journalctl -b -1 的报告就接不上。

不要一上来就跑 perf record。先用 pressure 和 vmstat 判断是 CPU、内存还是 IO,再决定要不要采样;deleted-open 只在「df 满但 du 小」时看,sockets 只在「连不上/连接堆积」时看。把所有工具都跑一遍,输出会互相干扰,也拖长取证时间。

0.14.19 两个实验:CPU 饱和与 IO 队列深度

实验一:两个 worker 抢一个核,观察 %wait 与 runqueue

# 终端 A:两个忙循环绑到 3 号核
taskset -c 3 stress-ng --cpu 2 --timeout 60s &
sleep 2

# 终端 B:利用率、每线程、perf、runqueue 延迟
mpstat -P ALL 1 3
pidstat -u -t 1 3 | grep -E 'stress-ng|UID|Linux'
perf stat -C 3 -e cycles,instructions,context-switches -- sleep 5
sudo bpftrace /usr/share/bpftrace/tools/runqlat.bt 1 5

预期:mpstat 只有 3 号核接近 100%,其余核空闲;pidstat -t 显示两个 worker 各约 50%,%wait 大于 0;perf stat -C 3 的 task-clock 仍约 1 个 CPU(一个核的时间),context-switches 明显升高;runqlat 的 P99 从「一个 worker」时的亚毫秒升到毫秒级。把 --cpu 2 改成 --cpu 1 重跑,%wait 应接近 0、runqueue 尾部明显下降——这就是「利用率 100%」和「排队/饱和」的区别。

实验二:fio 的队列深度如何改变 await 与 aqu-sz

cd /var/tmp
iostat -xz 1 6 > /tmp/iostat-qd1.txt &
iostat_pid=$!
fio --name=qd1 --rw=randread --bs=4k --iodepth=1 --numjobs=1 --direct=1 \
    --filename=/var/tmp/fio.test --size=512M --runtime=20 --time_based \
    --group_reporting --output=/tmp/fio-qd1.txt
wait "$iostat_pid"

iostat -xz 1 6 > /tmp/iostat-qd32.txt &
iostat_pid=$!
fio --name=qd32 --rw=randread --bs=4k --iodepth=32 --numjobs=1 --direct=1 \
    --filename=/var/tmp/fio.test --size=512M --runtime=20 --time_based \
    --group_reporting --output=/tmp/fio-qd32.txt
wait "$iostat_pid"

grep -E 'read: IOPS|clat percentiles|99.00th|99.90th' /tmp/fio-qd1.txt /tmp/fio-qd32.txt
grep -E 'Device|nvme0n1|sda' /tmp/iostat-qd1.txt /tmp/iostat-qd32.txt
rm -f /var/tmp/fio.test

预期:qd1 的 aqu-sz 接近 0,r_await 约等于单次设备延迟,IOPS 受限于「一次只有一个请求」;qd32 的 aqu-sz 大于 1,r_await 变大,IOPS 先上升。若 IOPS 随队列深度继续线性上升、await 基本不变,设备还有余量;若 IOPS 平台化而 await 继续涨,说明设备饱和,加队列深度只会增加延迟。--direct=1 绕过 page cache,测的是设备而不是内存;--size=512M 限制文件大小,别在生产盘上跑。

0.14.20 二十三个性能观测陷阱

#现象原因修复/判断
1load 高但 CPU idle 90%D 状态/NFS/驱动 I/Ovmstat 的 b、ps 的 D
2free 只剩 400 MB 就加内存available 才是可用估计看 available、si/so
3NVMe %util 100% 就判死多队列下 %util 失真看 aqu-sz、await、应用 P99
4iostat 第一行当当前值第一行是开机以来平均用第二次输出
5await 高但 aqu-sz 低单次 I/O 慢(盘/云盘/驱动)查设备延迟、biolatency
6strace -p 让延迟翻倍ptrace 逐 syscall 暂停用 perf trace/bpftrace
7perf 看不到内核符号perf_event_paranoid/kptr_restrict短时提权,不要长期 -1
8PSI 文件不存在内核未开 CONFIG_PSIpsi=1 启动参数或换内核
9cgroup 路径找不到v1/v2 混用、容器层级不同mount | grep cgroup、docker inspect
10容器 OOM 但 free 充足容器 memory.max 到顶看 memory.events 的 oom_kill
11RSS 加起来超过物理内存共享页重复计算用 PSS/ USS
12清缓存后性能更差drop_caches 丢了热数据不要生产环境清缓存
13sar 没有历史数据sysstat 未启用/采样被关systemctl status sysstat
14lsof +L1 看到 deleted 就去 rm 同名文件目录项已删除,rm 找不到;空间由 fd 持有截断 /proc/PID/fd/N 或让进程重开文件
15容器里 lsof 看不到宿主进程PID/mount namespace 隔离在宿主查,nsenter -t <pid> -m -p lsof ...
16vmstat 第一行当当前值和 iostat 一样,第一行是开机以来平均vmstat 1 5 从第二行读
17多线程进程 %CPU 100% 就以为单线程跑满pidstat/top 的 %CPU 是线程之和pidstat -t 看单线程,按 nproc 折算
18perf stat 两次结果差很多事件多路复用,计数是缩放估计看 (xx.xx%)、--no-scale、减少事件
1999 Hz 采 10 秒就下 P99 结论样本约 990,尾部误差大提高 -F/延长时间,先看样本数
20PSI some 高就判服务不可用some 只表示部分任务卡住看 full、持续时间、应用 P99
21cgroup memory.current 到 memory.max 但 free 很多含 page cache,限额按组算看 memory.stat 的 anon/file、memory.events
22lsof +L1 的 awk $7 取大小错位列位置随版本/COMMAND 截断变化lsof -F 字段化,或 +c 0 后按列名解析
23ss -ti 的 retrans 很大就以为全网重传它是单连接当前值,不是系统累计系统级看 nstat -az 的 TcpRetransSegs 增速

第 5 条与第 3 条要一起看:%util 高 + aqu-sz 低 + await 低,通常只是「队列一直非空但每次很快」,不是瓶颈;%util 低 + await 高,可能是远端存储或驱动层的固定延迟。单一指标永远不足以定论。

0.14.21 本章出口检查

检查项达标线没达标去哪
用 USE 给四类资源各选指标能说出 U/S/E 三个问题0.14.1
解释 load 高但 CPU 空闲能举 D 状态/NFS0.14.1、0.14.7
用 pidstat/perf 找到热点进程与函数能读出 %usr/%sys 与 Overhead0.14.2
用 free 的 available 判断内存压力能区分缓存与匿名页0.14.3
用 iostat 的 await/aqu-sz 判断饱和不只看 %util0.14.4
用 ss -ti/nstat 看网络重传能读出 retrans0.14.5
用 strace -c/bpftrace 追系统调用知道开销边界0.14.6
跑通 collect.sh 并对比两次快照输出目录、元数据、diff 可用0.14.8
用 lsof +L1 定位已删除但仍占空间的文件能说出为什么 rm 无效、如何截断 fd0.14.13
用 vmstat/pidstat/perf stat 读出 U/S/E能说出采样窗口与三个误判0.14.17
跑通 triage.sh 并归档meta.txt、summary.txt、tar+sha256 齐全0.14.18
完成 CPU/IO 对照实验能解释 %wait、aqu-sz、await 的变化0.14.19
即时自测 · 0.14

Q1 8 核机器 load average 12,top 的 CPU idle 90%。最可能是什么?给出两条验证命令。

Q2 free -h 显示 free 只有 400 MB,但 available 有 9 GB,si/so 为 0。需要加内存吗?为什么?

Q3 iostat -xz 1 显示某块 NVMe 的 %util 98%,但 await 0.2 ms、aqu-sz 0.1。这是磁盘瓶颈吗?该看什么?

Q4 线上服务延迟偶发升高,strace -c -p 一挂上延迟更高。换什么工具?为什么?

答案

A1 大概率是 D 状态进程(磁盘/NFS/驱动 I/O)把 load 推高。验证:vmstat 1 看 b 列和 wa;ps -eo stat,pid,comm | awk '$1 ~ /^D/' 找进程;cat /proc/PID/stack(root)看内核栈。CPU idle 高说明不是 CPU 排队。

A2 不需要。buff/cache 可回收,available 9 GB 说明新程序还能分配;si/so 为 0 说明没有换页。只有 available 持续下降、si/so 非零、OOM 出现,才是内存不足。

A3 不一定。%util 对 NVMe 多队列会失真;await 0.2 ms、aqu-sz 0.1 说明没有排队、延迟很低。看应用侧 P99 延迟、iostat 的 r/s/w/s 是否接近设备上限,以及是否只是持续的小 IO 占满采样窗口。

A4 用 perf trace 或 bpftrace(eBPF)做聚合/采样,开销比 strace 的逐 syscall ptrace 小;也可以先用 pidstat/perf stat 判断是用户态还是内核态,再决定是否追踪。strace 适合短时间定位,不适合长期挂在高并发服务上。

0.14.22 第 0 篇收束:命令行能力自检

第 0 篇的 14 章到这里收束。下面这张表不考记忆,考「遇到事能不能找到入口」;任何一项说不清,回到对应章节,或者翻 R.1 命令行速查。

能力自检问题对应章节
定位给你一个 PID,能读出它的父进程、进程组、会话、状态0.9
权限能解释 Permission denied 是 DAC、ACL、capability 还是 mount 选项0.4
文本能用 grep/sed/awk 从日志里抽出 Top 10 来源 IP0.6
脚本能写一个带锁、日志、trap 清理、可被 bats 测试的脚本0.8
存储能解释 df 与 du 的差异,并在 loop 上扩一次 LVM0.10
服务能把脚本写成 service + timer,并用 journalctl 查失败0.12
网络能按链路→地址→路由→端口→应用排一次「连不上」0.13
性能能按症状选工具,并说出手上这条证据证明什么0.14

母本对应

母本章节PDF 页本书落点程度
鸟哥第 16 章 16.3 程序观察p.789–8060.14.17背景(ps/top 字段)
鸟哥第 16 章 16.4 特殊文件与程序p.807–8130.14.17–0.14.18背景(/proc、lsof)
鸟哥第 20 章 20.2 硬件数据收集p.986–9910.14.17背景(CPU/内存/磁盘清单)
鸟哥第 16 章 程序管理与 SELinux 初探p.776–840(lsof 见 p.809–813)0.14.1–0.14.2、0.14.13背景(ps/top/lsof)
鸟哥第 18 章 认识与分析登录文件p.886–9200.14.8背景(日志与历史数据)
shell-book第 24 章 编写简单的脚本实用工具p.571–6060.14.8、0.14.15背景(采集脚本的结构与归档套路)
shell-book第 25 章 井井有条(版本控制)p.607–6200.14.9精选(dotfiles/版本管理)
Shotts第 30 章 故障诊断p.368–3790.14.1、0.14.7、0.14.18完整(排查顺序、防御式脚本与证据)
lsof(8) / proc(5)打开文件与 /proc 视图man0.14.13精选(fd、+L1、namespace)
网络第 9 章 网络性能—0.14.5精选(带宽、重传、丢包)
奔跑吧 Linux 内核第 6 章 性能工具—0.14.2、0.14.6精选(perf/ftrace 用户视角)

下一站:第 0 篇到此结束。第 1 篇 计算机系统从「命令怎么用」进入「程序怎么跑」:从源文件到进程、信息的表示、汇编、缓存与链接;想先检验命令行能力,可以翻 R.1 命令行速查 做一遍自测。