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(有没有报错) |
|---|---|---|---|
| CPU | mpstat 的 %usr+%sys | vmstat 的 r、load average | dmesg 的 MCE、perf 的 throttle |
| 内存 | free 的 used | vmstat 的 si/so、OOM | dmesg 的 OOM、memory.events |
| 磁盘 | iostat 的 %util | aqu-sz、await | dmesg 的 I/O error、SMART |
| 网络 | sar -n DEV 的带宽 | ss -ti 的 retrans、nstat 的 drops | ip -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 stat | b 列大、wa 高 | iostat、NFS、/proc/PID/stack |
| 内存持续上涨 | 是缓存还是匿名页 | free、ps rss、smaps_rollup | available 降、Anonymous 涨 | 堆分析、GC 日志、泄漏复现 |
| OOM | 谁被杀、哪个 cgroup | dmesg、memory.events | Killed process、oom_kill | 限制/扩容、查泄漏 |
| 磁盘慢 | 带宽、IOPS 还是延迟 | iostat -xz、iotop、pidstat -d | await/aqu-sz 高 | 找进程、查文件系统/RAID |
| 磁盘满 | 空间还是 inode | df -h、df -i、lsof +L1 | Use%/IUse%、(deleted) | 清理、扩容、重开日志 |
| 网络慢 | 带宽、重传、丢包 | sar -n DEV、ss -ti、nstat | retrans 增长、drop 计数 | mtr、tcpdump、查网卡 |
| 延迟尖刺 | 调度还是锁 | perf sched、bpftrace runqlat | 运行队列等待长 | 调 cgroup/亲和、查锁 |
| 应用慢 | 用户态还是内核态 | perf、strace -c | %sys 高、syscall 耗时 | 优化调用、缓存、批处理 |
| 启动慢 | 哪个 unit | systemd-analyze blame/critical-chain | 关键链上的 unit | 0.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 管常用命令入口;工具不在多,能复现就行。
- 「load average 就是 CPU 使用率」——Linux 的 load 包含不可中断睡眠(
D),NFS/磁盘卡死会让 load 很高而 CPU 空闲。 - 「
free的free小就是内存不足」——buff/cache可回收,available才是可用估计;只看free会误判。 - 「
%util100% 就是磁盘瓶颈」——单队列盘才有较强相关性;NVMe 多队列下要看aqu-sz、await和应用延迟。 - 「
strace开销很小,可以长期挂」——每次 syscall 都陷入 ptrace,开销可能让延迟翻倍;生产优先perf trace/bpftrace。 - 「
iostat第一行就是当前值」——第一行是自开机以来的平均,区间值从第二次输出开始。 - 「性能问题加 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 / avg300 | 1/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.max | quota period,如 50000 100000 | 0.5 个 CPU |
cpu.weight | 1–10000,默认 100 | 相对权重,不是上限 |
cpu.stat | usage_usec、nr_throttled、throttled_usec | 节流次数/时长 |
memory.current | 当前用量 | 含 page cache |
memory.max | 硬上限 | 超过触发 cgroup OOM |
memory.high | 软上限 | 超过先节流回收 |
memory.events | high、max、oom、oom_kill | OOM 证据 |
memory.stat | anon、file、slab、pgscan、pgsteal | 拆解用量 |
io.stat | 每设备的 rbytes、wbytes、rios、wios | 谁在做 I/O |
io.max | rbps、wbps、riops、wiops | I/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 |
AnonHugePages | THP 匿名大页 | 计入 RSS | smaps_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>/fd | fd → inode/路径 | 同 uid 或 root(受 Yama 限制) |
/proc/<pid>/maps | mmap 的文件 | 同上 |
/proc/net/tcp、tcp6、unix | socket 四元组 | 通常全局可读 |
/proc/<pid>/mountinfo | 挂载与 namespace | 同上 |
lsof 的 FD 列把「打开方式」和「fd 编号」合在一起:
| FD | 含义 |
|---|---|
cwd | 当前工作目录 |
rtd | 根目录 |
txt | 可执行文件 |
mem | mmap 的文件 |
0u / 1w / 2w | stdin/stdout/stderr;r 读、w 写、u 读写 |
3r / 4w | 普通 fd 与打开模式 |
IPv4 / IPv6 / unix | socket(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 的五个坑:
- 对已删除文件
rm同名路径——目录项已经没了,rm找不到;空间由 fd 持有,只能截断 fd 或重启进程。 - 容器里看不到宿主进程——
lsof读的是当前 PID namespace 的/proc;在容器里查宿主,用nsenter -t <pid> -m -p lsof ...或在宿主上查。 +D递归扫描很慢——大目录会扫几分钟;先用+d看一层,或直接fuser -vm。COMMAND被截断——默认只显示 9 个字符,+c 0显示全名;grep前先确认没被截。- 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-migrations | CPU 迁移 | 高:亲和性/负载均衡 |
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.bt | page 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(错误) | 采样窗口 | 常见误判 |
|---|---|---|---|---|---|
| CPU | mpstat -P ALL 1 的 %usr+%sys | vmstat 1 的 r、perf sched 的 runqueue | dmesg -T 的 MCE、perf stat 的 throttle | 10–30 s | 整机平均掩盖单核打满;VM 里 %steal 不是自己的 CPU |
| 内存 | free -h 的 available | vmstat 的 si/so、PSI memory 的 full | memory.events 的 oom_kill | 60 s | 把 page cache 当泄漏;容器 memory.current 含 cache |
| 磁盘 | iostat -xz 的 %util、r/s/w/s | aqu-sz、await | dmesg 的 I/O error、SMART | 10–30 s | NVMe 多队列下 %util 失真;await 含排队时间 |
| 网络 | sar -n DEV 1 的 rxkB/s/txkB/s | ss -ti 的 retrans、nstat 的 drops | ip -s link 的 errors/dropped | 60 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/%system | pidstat -u | 用户态/内核态 CPU | %system 高先看 syscall、锁、软中断 |
%wait | pidstat -u | 等 CPU 的时间占比 | 高且 %CPU 不高,说明 CPU 争用,不是 I/O |
%CPU | pidstat/top | 进程所有线程之和 | 可以超过 100%;-t 才看单线程 |
task-clock | perf stat | 任务占用 CPU 的时间 | CPUs utilized 接近 1 表示约一个核 |
cycles/instructions | perf stat | 周期/指令数 | IPC = instructions/cycles,低可能是内存墙 |
cache-misses/branch-misses | perf stat | 末级缓存 miss/分支预测失败 | 看比率,不只看绝对数 |
context-switches | perf 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 二十三个性能观测陷阱
| # | 现象 | 原因 | 修复/判断 |
|---|---|---|---|
| 1 | load 高但 CPU idle 90% | D 状态/NFS/驱动 I/O | vmstat 的 b、ps 的 D |
| 2 | free 只剩 400 MB 就加内存 | available 才是可用估计 | 看 available、si/so |
| 3 | NVMe %util 100% 就判死 | 多队列下 %util 失真 | 看 aqu-sz、await、应用 P99 |
| 4 | iostat 第一行当当前值 | 第一行是开机以来平均 | 用第二次输出 |
| 5 | await 高但 aqu-sz 低 | 单次 I/O 慢(盘/云盘/驱动) | 查设备延迟、biolatency |
| 6 | strace -p 让延迟翻倍 | ptrace 逐 syscall 暂停 | 用 perf trace/bpftrace |
| 7 | perf 看不到内核符号 | perf_event_paranoid/kptr_restrict | 短时提权,不要长期 -1 |
| 8 | PSI 文件不存在 | 内核未开 CONFIG_PSI | psi=1 启动参数或换内核 |
| 9 | cgroup 路径找不到 | v1/v2 混用、容器层级不同 | mount | grep cgroup、docker inspect |
| 10 | 容器 OOM 但 free 充足 | 容器 memory.max 到顶 | 看 memory.events 的 oom_kill |
| 11 | RSS 加起来超过物理内存 | 共享页重复计算 | 用 PSS/ USS |
| 12 | 清缓存后性能更差 | drop_caches 丢了热数据 | 不要生产环境清缓存 |
| 13 | sar 没有历史数据 | sysstat 未启用/采样被关 | systemctl status sysstat |
| 14 | lsof +L1 看到 deleted 就去 rm 同名文件 | 目录项已删除,rm 找不到;空间由 fd 持有 | 截断 /proc/PID/fd/N 或让进程重开文件 |
| 15 | 容器里 lsof 看不到宿主进程 | PID/mount namespace 隔离 | 在宿主查,nsenter -t <pid> -m -p lsof ... |
| 16 | vmstat 第一行当当前值 | 和 iostat 一样,第一行是开机以来平均 | vmstat 1 5 从第二行读 |
| 17 | 多线程进程 %CPU 100% 就以为单线程跑满 | pidstat/top 的 %CPU 是线程之和 | pidstat -t 看单线程,按 nproc 折算 |
| 18 | perf stat 两次结果差很多 | 事件多路复用,计数是缩放估计 | 看 (xx.xx%)、--no-scale、减少事件 |
| 19 | 99 Hz 采 10 秒就下 P99 结论 | 样本约 990,尾部误差大 | 提高 -F/延长时间,先看样本数 |
| 20 | PSI some 高就判服务不可用 | some 只表示部分任务卡住 | 看 full、持续时间、应用 P99 |
| 21 | cgroup memory.current 到 memory.max 但 free 很多 | 含 page cache,限额按组算 | 看 memory.stat 的 anon/file、memory.events |
| 22 | lsof +L1 的 awk $7 取大小错位 | 列位置随版本/COMMAND 截断变化 | lsof -F 字段化,或 +c 0 后按列名解析 |
| 23 | ss -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 状态/NFS | 0.14.1、0.14.7 |
用 pidstat/perf 找到热点进程与函数 | 能读出 %usr/%sys 与 Overhead | 0.14.2 |
用 free 的 available 判断内存压力 | 能区分缓存与匿名页 | 0.14.3 |
用 iostat 的 await/aqu-sz 判断饱和 | 不只看 %util | 0.14.4 |
用 ss -ti/nstat 看网络重传 | 能读出 retrans | 0.14.5 |
用 strace -c/bpftrace 追系统调用 | 知道开销边界 | 0.14.6 |
跑通 collect.sh 并对比两次快照 | 输出目录、元数据、diff 可用 | 0.14.8 |
用 lsof +L1 定位已删除但仍占空间的文件 | 能说出为什么 rm 无效、如何截断 fd | 0.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 |
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 来源 IP | 0.6 |
| 脚本 | 能写一个带锁、日志、trap 清理、可被 bats 测试的脚本 | 0.8 |
| 存储 | 能解释 df 与 du 的差异,并在 loop 上扩一次 LVM | 0.10 |
| 服务 | 能把脚本写成 service + timer,并用 journalctl 查失败 | 0.12 |
| 网络 | 能按链路→地址→路由→端口→应用排一次「连不上」 | 0.13 |
| 性能 | 能按症状选工具,并说出手上这条证据证明什么 | 0.14 |
母本对应
| 母本 | 章节 | PDF 页 | 本书落点 | 程度 |
|---|---|---|---|---|
| 鸟哥 | 第 16 章 16.3 程序观察 | p.789–806 | 0.14.17 | 背景(ps/top 字段) |
| 鸟哥 | 第 16 章 16.4 特殊文件与程序 | p.807–813 | 0.14.17–0.14.18 | 背景(/proc、lsof) |
| 鸟哥 | 第 20 章 20.2 硬件数据收集 | p.986–991 | 0.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–920 | 0.14.8 | 背景(日志与历史数据) |
| shell-book | 第 24 章 编写简单的脚本实用工具 | p.571–606 | 0.14.8、0.14.15 | 背景(采集脚本的结构与归档套路) |
| shell-book | 第 25 章 井井有条(版本控制) | p.607–620 | 0.14.9 | 精选(dotfiles/版本管理) |
| Shotts | 第 30 章 故障诊断 | p.368–379 | 0.14.1、0.14.7、0.14.18 | 完整(排查顺序、防御式脚本与证据) |
lsof(8) / proc(5) | 打开文件与 /proc 视图 | man | 0.14.13 | 精选(fd、+L1、namespace) |
| 网络 | 第 9 章 网络性能 | — | 0.14.5 | 精选(带宽、重传、丢包) |
| 奔跑吧 Linux 内核 | 第 6 章 性能工具 | — | 0.14.2、0.14.6 | 精选(perf/ftrace 用户视角) |
下一站:第 0 篇到此结束。第 1 篇 计算机系统从「命令怎么用」进入「程序怎么跑」:从源文件到进程、信息的表示、汇编、缓存与链接;想先检验命令行能力,可以翻 R.1 命令行速查 做一遍自测。