Linux-18 性能分析工具链与方法论
前面几篇讲了 CPU、内存、磁盘、网络各自的原理和排查工具。这一篇把它们串成一套方法论——面对一个「服务变慢了」的告警,按什么顺序看什么指标,才能最快收敛到根因。
核心观点:性能分析最大的浪费是「凭直觉猜」。有方法论的排查是收敛的,凭猜的排查是发散的。
与其他篇的分工:本篇讲工具链本身,nginx-12 性能调优实战 讲 nginx 场景的应用,golang 系列的 pprof 讲 Go 语言层面的分析。
1. USE 方法论
Brendan Gregg 提出的 USE 方法是最实用的性能分析框架。对每一类资源,检查三个指标:
U — Utilization(使用率) 资源有多忙?
S — Saturation(饱和度) 有多少请求在排队?
E — Errors(错误) 有没有错误发生?
**关键洞察:饱和度比使用率更能说明问题。**使用率 100% 不一定有问题(第 11 篇讲的 SSD %util),但队列在增长一定有问题。
1.1 各资源的 USE 检查表
| 资源 | Utilization | Saturation | Errors |
|---|---|---|---|
| CPU | top 的 us+sy |
vmstat 的 r 列 > 核数 |
— |
| 内存 | free 的 available 占比 |
si/so 换页、majflt |
dmesg 的 OOM |
| 磁盘 | iostat 的 %util(参考) |
await、aqu-sz |
dmesg 的 IO error |
| 网络 | sar -n DEV 的带宽占比 |
ss -s 的队列、nstat 溢出 |
ip -s link 的 errors/drops |
| 文件描述符 | /proc/<pid>/fd 数量 / 限制 |
— | Too many open files |
# 一条命令过一遍 USE(快速体检)
echo "=== CPU ==="; vmstat 1 2 | tail -1 | awk '{print "r="$1, "us="$13, "sy="$14, "wa="$16, "st="$17}'
echo "=== MEM ==="; free -m | awk 'NR==2{printf "available=%.0f%%\n", $7/$2*100}'
echo "=== DISK ==="; iostat -x 1 2 | awk '/^[a-z]/ && NF>10 {print $1, "await="$10, "aqu="$12, "util="$NF}' | tail -3
echo "=== NET ==="; ss -s | head -2
echo "=== ERR ==="; dmesg -T 2>/dev/null | tail -3
1.2 性能工具地图
按观测层次组织,知道「想看某一层该用什么工具」:
应用层
+----------------------------------------+
| 语言 profiler: pprof / jstack / py-spy |
+----------------------------------------+
系统调用层
+----------------------------------------+
| strace / ltrace / perf trace / bpftrace|
+----------------------------------------+
内核子系统
+----------+----------+----------+-------+
| CPU | 内存 | 磁盘 | 网络 |
+----------+----------+----------+-------+
| top | free | iostat | ss |
| mpstat | vmstat | iotop | sar-n |
| pidstat | smem | blktrace | nstat |
| perf | pmap | fatrace | tcpd. |
+----------+----------+----------+-------+
全局观测
+----------------------------------------+
| vmstat / sar / dstat / perf / eBPF |
+----------------------------------------+
工具的两大类别,用途完全不同:
| 类型 | 代表 | 特点 | 用途 |
|---|---|---|---|
| 计数器型 | vmstat、iostat、sar |
读内核已有的统计,开销近零 | 常态监控、初步定位 |
| 追踪型 | strace、perf、bpftrace |
拦截事件,有开销 | 深入定位,短时使用 |
**规律:先用计数器型工具定位到「哪个资源、哪个进程」,再用追踪型工具深入「具体是什么调用」。**顺序反了会付出不必要的性能代价。
2. 60 秒定位法
Netflix 团队总结的一套流程,登上一台机器后按顺序跑,一分钟内建立完整认知:
uptime # ① 负载趋势
dmesg -T | tail -20 # ② 内核错误(OOM、IO error)
vmstat 1 5 # ③ 全局:CPU/内存/IO/上下文切换
mpstat -P ALL 1 3 # ④ 每个核的均衡性
pidstat 1 3 # ⑤ 哪个进程在消耗
iostat -xz 1 3 # ⑥ 磁盘详情
free -h # ⑦ 内存构成
sar -n DEV 1 3 # ⑧ 网络吞吐
sar -n TCP,ETCP 1 3 # ⑨ TCP 连接与重传
top # ⑩ 综合确认
每一步要看什么、看到什么算异常:
2.1 uptime:负载趋势
uptime
# 14:32:01 up 42 days, 3:11, 2 users, load average: 12.35, 8.42, 4.11
# ^^^^^ ^^^^ ^^^^
# 1min 5min 15min
nproc
# 8
判读(第 7 篇讲过):load/核数 > 0.7 需要关注。三个数字的趋势比绝对值重要:12, 8, 4 说明负载正在快速上升,问题在恶化。
2.2 dmesg:先排除硬故障
dmesg -T | grep -iE "error|fail|oom|killed|reset" | tail -20
这一步不能跳过——OOM、磁盘错误、网卡重置这些硬性问题会让后面所有的指标分析失去意义。
"Out of memory: Killed process" -> 内存不足,第 10 篇
"blk_update_request: I/O error" -> 磁盘硬件问题
"nvme nvme0: I/O timeout" -> 磁盘或驱动异常
"NETDEV WATCHDOG: eth0 transmit timed out" -> 网卡问题
"TCP: out of memory -- consider tuning tcp_mem" -> TCP 内存不足
2.3 vmstat:全局一览
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 24 2 0 234560 91564 818900 0 0 4096 89234 3421 45623 15 32 21 32 0
扫一遍的判断顺序(第 7 篇有详细说明):
① r 列 vs 核数 -> 24 > 8,CPU 严重排队
② b 列 -> 2,有进程阻塞在 IO
③ si/so -> 0,没有换页(好)
④ wa -> 32%,IO 等待偏高
⑤ st -> 0,没被虚拟化层偷走
⑥ cs -> 45623,偏高但要结合业务量看
⑦ us vs sy -> sy 32% 高于 us 15%,内核态占比异常
这个例子指向两个方向:CPU 排队(r=24)+ 内核态高(sy=32%)+ IO 等待(wa=32%)——很可能是「IO 慢导致进程堆积,同时大量系统调用」。
2.4 mpstat:核间均衡性
mpstat -P ALL 1 3
# 14:32:05 CPU %usr %nice %sys %iowait %irq %soft %idle
# 14:32:06 all 12.0 0.0 8.0 2.0 0.0 9.8 68.2
# 14:32:06 0 8.0 0.0 6.0 1.0 0.0 82.0 3.0 <- CPU0 软中断打满
# 14:32:06 1 13.0 0.0 8.0 2.0 0.0 0.5 76.5
# 14:32:06 2 12.0 0.0 9.0 2.0 0.0 0.3 76.7
这是 top 看不出来的问题:整体 %idle 68% 看着很空闲,但 CPU0 的 %soft 已经 82%——网络软中断全压在一个核上(第 7 篇 §7.1 讲过)。这时候吞吐上不去,加机器也没用,要做的是把中断分散。
# 确认中断分布
cat /proc/interrupts | grep -E "^\s*[0-9]+:" | awk '{s=0; for(i=2;i<=NF-2;i++) s+=$i; if(s>100000) print $0}' | head -5
# 看软中断类型
watch -n1 'cat /proc/softirqs | head -12'
# 解法:开启 RPS 把软中断分散到多核
echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 或者用 irqbalance / 多队列网卡的 RSS
ethtool -l eth0 # 看网卡支持多少队列
ethtool -L eth0 combined 8 # 开启 8 个队列
2.5 pidstat:定位到进程
# CPU
pidstat -u 1 3 | sort -k8 -rn | head -5
# 时间 UID PID %usr %system %guest %CPU CPU Command
# 14:32:06 0 8821 12.00 188.00 0.00 200.00 3 mysqld
# ^^^^^^ 内核态占了 188%
# 内存(含缺页)
pidstat -r 1 3 | sort -k5 -rn | head -5
# PID minflt/s majflt/s VSZ RSS %MEM Command
# 8821 1234.00 45.00 8234012 4123400 25.1 mysqld
# ^^^^^ 主要缺页!在读磁盘或换页(第 10 篇)
# 磁盘 IO
pidstat -d 1 3 | sort -k5 -rn | head -5
# PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
# 8821 0.00 178234.00 0.00 8923 mysqld
# ^^^^ IO 延迟高
# 上下文切换(第 7 篇的关键区分)
pidstat -w 1 3 | sort -k4 -rn | head -5
# PID cswch/s nvcswch/s Command
# 1234 234012.0 892.0 myapp
# ^^^^^^^^ 自愿切换极高 -> 锁竞争或 IO 等待
pidstat 的四个模式(-u -r -d -w)分别对应 CPU、内存、磁盘、上下文切换,可以合起来用:
pidstat -urdw -p $(pgrep -d, myapp) 1 3
2.6 sar:历史数据
sar 最大的价值是能看历史——问题已经过去了,但你想知道当时发生了什么。
# 安装并开启采集
apt install sysstat
# Debian: 改 /etc/default/sysstat 的 ENABLED="true"
systemctl enable --now sysstat
# 查历史数据(默认每 10 分钟采集一次,保留 7~28 天)
sar -u # 今天的 CPU
sar -u -f /var/log/sysstat/sa06 # 指定某一天(6 日)
sar -u -s 03:00:00 -e 04:00:00 # 指定时间段 <- 排查凌晨故障必备
sar -r # 内存
sar -b # IO 总量
sar -d -p # 各磁盘(-p 显示设备名而不是 dev8-0)
sar -n DEV # 网卡吞吐
sar -n TCP,ETCP # TCP 连接与重传
sar -q # 负载与队列
sar -W # 换页
「昨晚 3 点服务抖动」这类问题,sar 是唯一能回答的工具:
sar -u -s 02:50:00 -e 03:20:00
# 03:00:01 all 85.23 0.00 12.34 1.22 0.00 1.21
# ^^^^^ 3 点整 CPU 冲到 85%,说明有定时任务
sar -q -s 02:50:00 -e 03:20:00 # 确认负载也同时冲高
crontab -l; systemctl list-timers # 找出 3 点的定时任务
生产环境务必开启 sysstat——这是事后追溯的唯一依据,没有它就只能等问题复现。
3. top:用对了才有用
top

3.1 必须掌握的交互键
| 键 | 作用 |
|---|---|
1 |
展开每个 CPU 核(等效 mpstat,看均衡性) |
H |
显示线程而不是进程 |
M |
按内存排序 |
P |
按 CPU 排序(默认) |
T |
按累计时间排序 |
c |
显示完整命令行 |
x |
高亮排序列 |
V |
树状显示父子关系 |
f |
选择显示哪些列(可加 nMaj、SWAP、P) |
W |
保存当前配置到 ~/.toprc |
k |
杀进程 |
e/E |
切换内存单位 |
1 和 H 是最有价值的两个:1 能立刻发现单核瓶颈,H 能定位到具体线程(第 6 篇的实战用到)。
# 脚本里用批处理模式
top -bn2 -d1 | grep "^%Cpu" | tail -1
# ^^^ 必须取第二次!第一次是开机至今的平均值(第 7 篇 §7)
# 只看某个进程及其线程
top -H -p $(pgrep -d, myapp)
3.2 htop 与 btop
apt install htop
htop
# - 彩色、鼠标可点、树状视图
# - F5 树状 F6 排序 F9 kill
# - 可直接看到每个核的条形图
htop 在交互排查时更好用,但脚本和自动化仍用 top -b。极简容器里通常两个都没有,只能读 /proc。
4. strace 与 ltrace
strace 的原理和用法在第 9 篇 §4 讲得很详细,这里补充与其他工具的配合和 ltrace。
# 核心用法回顾
strace -c -f -p PID # 统计各系统调用耗时占比 <- 首选
strace -T -f -p PID # 显示每个调用的耗时
strace -e trace=file -p PID # 只看文件操作
strace -y -p PID # fd 显示为实际文件名
ltrace 追踪库函数调用(不是系统调用):
ltrace -c ./myapp
# % time seconds usecs/call calls function
# 45.23 2.340 234 10000 malloc
# 22.11 1.144 114 10000 free
# 12.03 0.622 622 1000 sqlite3_step
# 只看某个库的调用
ltrace -e '*@libssl*' ./myapp
ltrace 的实用场景:怀疑 malloc/free 过于频繁、想知道调用了哪个第三方库函数、排查动态库加载问题。但它的开销比 strace 更大(拦截的事件多得多),生产环境几乎不能用。
三者的层次关系:
ltrace -> 库函数调用(malloc、printf、OpenSSL 函数)
strace -> 系统调用(read、write、futex)
perf -> 内核函数、CPU 采样、硬件事件
5. perf:采样式分析
perf 是 Linux 性能分析的主力工具,它基于采样而不是拦截,所以开销小得多(通常 < 5%),可以在生产环境短时使用。
apt install linux-tools-common linux-tools-$(uname -r)
# 或 dnf install perf
5.1 perf top:实时热点
perf top
# Samples: 89K of event 'cycles', Event count: 45234012345
# Overhead Shared Object Symbol
# 23.45% myapp [.] json_parse
# 12.03% libc-2.35.so [.] __memcpy_avx_unaligned
# 8.92% [kernel] [k] copy_user_enhanced_fast_string
# ^^^ [k]=内核 [.]=用户态
# 只看某个进程
perf top -p 1234
# 按调用链聚合
perf top -g
perf top 是「CPU 到底在执行什么代码」的最直接答案,比 pprof 的优势是能看到内核态(第 9 篇的实战案例里,%system 高时 pprof 无能为力)。
5.2 perf record / report:离线分析
# 采样 30 秒(-g 记录调用栈,--call-graph dwarf 更准但更慢)
perf record -F 99 -a -g -- sleep 30
# ^^^^ 99Hz 采样(用 99 而非 100 避免与定时器同频)
# ^^ 全系统
perf report
perf report --stdio --sort=comm,dso | head -20
# 只采某个进程
perf record -F 99 -p 1234 -g -- sleep 30
# 采样特定事件
perf list # 看支持哪些事件
perf record -e cache-misses -a -- sleep 10
perf record -e 'syscalls:sys_enter_*' -p 1234 -- sleep 5
-F 99 而不是 100 是个小技巧——避免采样频率与内核定时器(通常 100Hz 或 250Hz)同频产生的锁步效应(lockstep),那会导致采样总是落在同样的代码位置上。
5.3 perf stat:硬件计数器
perf stat -d ./myapp
# 1,234.56 msec task-clock # 0.998 CPUs utilized
# 1,234 context-switches # 0.999 K/sec
# 4,567,890,123 cycles # 3.700 GHz
# 8,901,234,567 instructions # 1.95 insn per cycle <- IPC
# 234,567,890 branch-misses # 2.34% of all branches
# 12,345,678 L1-dcache-load-misses # 5.67% of all L1-dcache accesses
IPC(instructions per cycle)是判断 CPU 效率的关键指标:
IPC > 2.0 好,CPU 流水线充分利用
IPC 1.0~2.0 正常
IPC < 1.0 差 —— CPU 大量时间在【等内存】,不是在计算
-> 检查 cache-misses、内存访问模式、数据结构布局
「CPU 100% 但 IPC 只有 0.5」意味着 CPU 在空转等内存——这时候优化算法复杂度没用,要优化数据局部性(比如把链表换成数组、减少指针跳转)。
5.4 火焰图
火焰图是把 perf 的采样数据可视化的标准方式。
# 准备工具
git clone --depth 1 https://github.com/brendangregg/FlameGraph
cd FlameGraph
# 三步生成
perf record -F 99 -p 1234 -g -- sleep 30
perf script > out.perf
./stackcollapse-perf.pl out.perf | ./flamegraph.pl > flame.svg
# 一条命令版本
perf record -F 99 -p 1234 -g -- sleep 30 && \
perf script | ~/FlameGraph/stackcollapse-perf.pl | ~/FlameGraph/flamegraph.pl > flame.svg
怎么读火焰图:
+---------------------------------------------+
| main | <- 底部是调用栈的根
+------------------+--------------------------+
| handleRequest | backgroundJob | <- 宽度 = 占 CPU 的比例
+---------+--------+--------------------------+
|parseJSON| queryDB| |
+---------+ | |
| memcpy | | | <- 顶部是正在执行的函数
+---------+--------+--------------------------+
**最重要的一条:看「宽」的不看「高」的。**栈很深(很高)只说明调用层次多,不代表慢;宽的平顶(plateau)才是消耗 CPU 的地方。
符号缺失的处理:
# 火焰图里出现大量 [unknown] 或十六进制地址
# 原因:二进制被 strip 了,或者缺少 frame pointer
# Go:默认保留符号,但需要 frame pointer(Go 1.7+ 默认有)
# 编译时不要加 -ldflags="-s -w"(那会去掉符号表)
# C/C++:编译时加
gcc -fno-omit-frame-pointer -g ...
# Java:需要 perf-map-agent 或用 async-profiler(推荐)
# Python:用 py-spy(更简单)
py-spy record -o flame.svg --pid 1234 --duration 30
--call-graph dwarf 可以在没有 frame pointer 时还原调用栈,但开销大得多:
perf record -F 99 -p 1234 --call-graph dwarf -- sleep 10
5.5 off-CPU 分析
**火焰图只能看到「CPU 在做什么」,看不到「进程在等什么」。**很多性能问题是等待而非计算——等锁、等 IO、等网络。这需要 off-CPU 火焰图:
# 用 bpftrace 采集 off-CPU 时间(需要内核 4.9+)
bpftrace -e '
kprobe:finish_task_switch {
$prev = (struct task_struct *)arg0;
if ($prev->state == 1) { // TASK_INTERRUPTIBLE
@[kstack, ustack, comm] = sum(nsecs - @start[$prev->pid]);
}
@start[tid] = nsecs;
}' -c ./myapp
更简单的方式是用 BCC 工具:
apt install bpfcc-tools
/usr/share/bcc/tools/offcputime -df -p 1234 30 > out.stacks
~/FlameGraph/flamegraph.pl --color=io --title="Off-CPU Time" < out.stacks > offcpu.svg
**规律:CPU 使用率高用 on-CPU 火焰图,延迟高但 CPU 不高用 off-CPU 火焰图。**这两张图合起来才是完整的性能画像。
6. eBPF 与 bpftrace
eBPF 是现代 Linux 可观测性的基础设施——它让你能在内核里运行安全的小程序,几乎无侵入地观测任何事件。
apt install bpfcc-tools bpftrace # Debian/Ubuntu
dnf install bcc-tools bpftrace # RHEL
# 内核要求 4.9+,5.x 才好用
uname -r
6.1 为什么 eBPF 优于 strace
| strace | eBPF/bpftrace | |
|---|---|---|
| 原理 | ptrace 拦截,每次调用停两次 |
内核内挂钩子,直接聚合 |
| 开销 | 10~100 倍慢 | 通常 < 5% |
| 生产可用 | 只能短时 | 可常态运行 |
| 能观测的范围 | 系统调用 | 系统调用 + 内核函数 + 用户函数 + 硬件事件 |
| 聚合能力 | 只能事后统计 | 内核内直接聚合直方图 |
「内核内聚合」是关键优势:strace 要把每个事件传到用户态再统计,eBPF 直接在内核里累加计数器,只把最终结果传出来。数据量差几个数量级。
6.2 bpftrace 单行命令
# 统计进程的系统调用次数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter /pid == 1234/ { @[probe] = count(); }'
# 按系统调用名统计
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
# 文件打开监控(哪个进程在读什么文件)
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
# 磁盘 IO 延迟直方图 <- 极实用
# 用 tracepoint 而不是 kprobe:接口稳定,跨内核版本都能用
bpftrace -e '
tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete /@start[args->dev, args->sector]/ {
@usecs = hist((nsecs - @start[args->dev, args->sector]) / 1000);
delete(@start[args->dev, args->sector]);
}'
# @usecs:
# [64, 128) 1823 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [128, 256) 923 |@@@@@@@@@@@@@@@@@@@@ |
# [1K, 2K) 12 | |
# [8K, 16K) 3 | | <- 有几个 8ms+ 的长尾
# TCP 连接监控
bpftrace -e 'kprobe:tcp_connect { printf("%s -> connect\n", comm); }'
# 统计 TCP 重传
bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm] = count(); }'
坑:
kprobe挂的是内核函数名,属于不稳定接口——函数被重命名、内联或改成 static 之后探针就失效(报Can't attach to kernel function)。比如blk_account_io_start在 5.11+ 就挂不上了。所以能用tracepoint就优先用它(内核承诺的稳定 ABI),用bpftrace -l 'tracepoint:block:*'可以列出可用的探针点。这也是直接用 BCC 现成工具(见下)比手写 bpftrace 更省心的原因——它们已经处理了各版本的差异。
直方图(hist)是 bpftrace 最有价值的能力——平均值会掩盖长尾,直方图能直接看到分布。「平均延迟 1ms」可能是「99% 是 0.1ms,1% 是 100ms」,后者才是 P99 问题的根源。
6.3 BCC 工具集
BCC 提供了几十个开箱即用的工具,不用写 eBPF 代码:
ls /usr/share/bcc/tools/
| 工具 | 作用 |
|---|---|
execsnoop |
实时显示所有新进程执行(抓短命进程神器) |
opensnoop |
显示所有文件打开操作 |
biolatency |
磁盘 IO 延迟直方图 |
biosnoop |
每个磁盘 IO 的详情 |
cachestat |
page cache 命中率 |
tcplife |
TCP 连接的生命周期与吞吐 |
tcpretrans |
TCP 重传事件 |
runqlat |
CPU 运行队列等待时间(调度延迟) |
offcputime |
off-CPU 时间分析 |
profile |
CPU 采样(生成火焰图用) |
funclatency |
任意函数的延迟分布 |
memleak |
内存泄漏检测 |
execsnoop 是抓「短命进程」的唯一有效手段:
/usr/share/bcc/tools/execsnoop -T
# TIME PCOMM PID PPID RET ARGS
# 14:32:01 sh 12345 1 0 /bin/sh -c /opt/check.sh
# 14:32:01 curl 12346 12345 0 /usr/bin/curl -s http://localhost/health
# 14:32:02 python3 12350 1 0 /usr/bin/python3 /opt/miner.py
# ^^^^^^^^^ 发现异常进程
场景:top 里看到 CPU 有毛刺但抓不到进程(因为进程存活只有几十毫秒),ps 和 top 的采样根本抓不到。execsnoop 能捕获每一次 exec,包括挖矿脚本、失控的定时任务、反复重启的进程。
runqlat 能量化调度延迟:
/usr/share/bcc/tools/runqlat 10 1
# usecs : count distribution
# 0 -> 1 : 8923 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# 8 -> 15 : 1203 |@@@@@ |
# 1024 -> 2047 : 234 |@ | <- 有 2ms 的调度延迟
# 8192 -> 16383 : 12 | | <- 极端情况 16ms
这直接量化了「进程从可运行到真正上 CPU 等了多久」——第 7 篇讲的 CPU 竞争问题,runqlat 是最精确的度量方式。如果这个值的长尾很长,说明 CPU 争抢严重,比看 load 精确得多。
7. 压测:拿到基线才能谈优化
**没有基线的优化是盲目的。**先测出当前能力,再改,再测——第 7 篇的方法论同样适用。
7.1 工具选择
| 工具 | 适用 |
|---|---|
wrk |
HTTP 压测首选,轻量、高并发、支持 Lua 脚本 |
wrk2 |
wrk 的改进版,固定速率压测(测延迟更准) |
ab |
简单,但单线程、不支持 keepalive 细节,已过时 |
vegeta |
固定 QPS 压测,输出延迟分布好 |
k6 |
脚本化复杂场景,有 Web UI |
sysbench |
CPU / 内存 / 磁盘 / MySQL 基准 |
fio |
磁盘 IO 基准的标准工具 |
iperf3 |
网络带宽 |
# wrk:12 线程 400 连接压 30 秒
wrk -t12 -c400 -d30s --latency http://localhost:8080/api/ping
# Running 30s test @ http://localhost:8080/api/ping
# 12 threads and 400 connections
# Latency Distribution
# 50% 1.23ms
# 75% 2.11ms
# 90% 4.53ms
# 99% 28.34ms <- 关注 P99
# Requests/sec: 45234.12
# fio:测磁盘随机读 IOPS
fio --name=randread --ioengine=libaio --direct=1 --rw=randread \
--bs=4k --numjobs=4 --iodepth=32 --size=1G --runtime=60 \
--group_reporting
# ^^^^^^^^^ --direct=1 绕过 page cache,测真实磁盘能力
# iperf3
iperf3 -s # 服务端
iperf3 -c server -P 4 # 客户端,4 个并发流
7.2 压测的常见陷阱
① 压测客户端成了瓶颈
-> 客户端 CPU 打满、fd 不够、端口耗尽
-> 先确认客户端 CPU < 70%,或用多台机器压
② 没有预热
-> JIT 未编译、连接池未建立、page cache 未填充
-> 先跑 1 分钟丢弃结果,再正式测
③ 只看平均值
-> 平均 5ms 可能是「99% 1ms + 1% 400ms」
-> 必须看 P99/P999(--latency)
④ 数据集太小
-> 全部命中缓存,测不出真实性能
-> 数据量要超过内存大小
⑤ 客户端与服务端同机
-> 互相抢 CPU,结果偏低
-> 至少分开部署
⑥ 协调遗漏(coordinated omission)
-> wrk 等工具在响应慢时会自动降低发送速率,掩盖了真实延迟
-> 用 wrk2 或 vegeta 做【固定速率】压测
「协调遗漏」是个隐蔽但严重的问题:传统压测工具是「发一个等一个」,服务端慢的时候客户端自动放慢了发送,于是测出来的延迟比真实情况乐观得多。固定速率压测(wrk2 -R 10000)才能暴露真实的延迟表现。
# wrk2:固定 10000 QPS,测延迟分布
wrk2 -t8 -c200 -d60s -R10000 --latency http://localhost:8080/api/ping
7.3 压测时同步观测
压测的价值一半在于「压的同时看系统指标」:
# 开三个终端
# 终端 1:压测
wrk -t12 -c400 -d60s --latency http://localhost:8080/api/ping
# 终端 2:系统指标
vmstat 1
# 终端 3:进程指标
pidstat -urdw -p $(pgrep -d, myapp) 1
这样能直接看出瓶颈在哪一层:us 高是业务计算、sy 高是系统调用、wa 高是磁盘、r 高是 CPU 不够、cs 暴涨是锁竞争。
8. 实战:一次完整的性能排查
现象:订单查询接口 P99 从 120ms 涨到 2.8s,QPS 未变化,持续了 20 分钟。
① 60 秒定位法
uptime
# load average: 18.42, 15.23, 9.87 <- 8 核,load 18,且在上升
dmesg -T | tail -20
# 无异常 <- 排除硬故障和 OOM
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 2 16 0 234560 91564 818900 0 0 8192 12034 2341 8923 8 12 22 58 0
# ^^ 16 个进程阻塞在 D 状态
# ^^ wa 58%
第一个结论:不是 CPU 问题,是 IO。r=2 说明 CPU 不排队,b=16 和 wa=58% 指向磁盘(第 7 篇的判读规则)。
② 确认磁盘状态
iostat -xz 1 3
# Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz rareq-sz %util
# nvme0n1 8234.0 120.0 32936.0 7680.0 12.45 0.82 102.3 4.0 99.90
# ^^^^^ NVMe 应 < 1ms,现在 12ms
# ^^^^^ 队列 102!
# ^^^ 4KB = 随机小 IO
三个条件全满足(第 11 篇的判断标准):await 是基准的 12 倍、aqu-sz 102 远大于 1、8234 IOPS。确认磁盘饱和,且是随机读为主。
③ 定位到进程
pidstat -d 1 3 | sort -k4 -rn | head -3
# UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
# 0 8821 31234.00 6234.00 0.00 18923 mysqld
# ^^^^^^^^ 读了 31MB/s
④ 用 eBPF 看是什么在读
# 磁盘 IO 延迟分布
/usr/share/bcc/tools/biolatency 10 1
# usecs : count distribution
# 64 -> 127 : 892 |@@@@ |
# 1024 -> 2047: 8234 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# 16384 -> 32767: 234 |@ |
# ^ 大量 1~2ms 的 IO,说明是真的在读盘不是命中缓存
# page cache 命中率
/usr/share/bcc/tools/cachestat 1 5
# HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
# 12034 89234 1203 11.9% 89 812
# ^^^^^ 命中率只有 12%!正常应该 > 90%
**关键发现:page cache 命中率从平常的 95% 掉到 12%。**说明工作集突然变大,或者缓存被冲掉了(第 10 篇讲的 page cache 机制)。
⑤ 确认缓存为什么失效
free -h
# total used free shared buff/cache available
# Mem: 15Gi 12Gi 1.2Gi 128Mi 1.8Gi 2.1Gi
# ^^^^^ cache 只剩 1.8G(平常 9G)
# ^^^^ used 涨到 12G
# 谁吃掉了内存
ps -eo pid,rss,comm --sort=-rss | head -5
# PID RSS COMMAND
# 8821 6234012 mysqld
# 15234 4823401 python3 <- 这是什么?平常没有
# 用 execsnoop 看这个进程怎么来的(它可能反复重启)
/usr/share/bcc/tools/execsnoop -T | head -20
# TIME PCOMM PID PPID RET ARGS
# 14:35:02 python3 15901 1 0 /usr/bin/python3 /opt/etl/full_export.py
# ^^^^^^^^^^^^^^^^^^^^^^^ 找到了
ps -o lstart= -p 15234
# Thu Aug 6 14:15:03 2026 <- 20 分钟前启动,与故障时间吻合
⑥ 根因确认
# 这个 ETL 脚本在做什么
cat /proc/15234/io
# read_bytes: 234560000000 <- 读了 234GB
# rchar: 234890000000 <- 几乎全部是真实磁盘读,没走缓存
# 它读的是什么
ls -l /proc/15234/fd/ | grep -v socket
# 3 -> /data/warehouse/orders_2020.csv
原因:有人手动跑了一个全量导出脚本,它顺序扫描了 234GB 的历史数据。这带来两个后果:
- page cache 被冲掉——大量一次性读入的数据挤掉了 MySQL 的热数据缓存
- 磁盘 IOPS 被占满——MySQL 的随机读请求排在长队列后面
这是典型的「缓存污染 + IO 争抢」双重打击。
⑦ 处理
# 立即止血:降低 ETL 的 IO 优先级(第 7 篇讲的 ionice)
ionice -c 3 -p 15234 # idle 级别,只在磁盘空闲时读
renice -n 19 -p 15234
# 验证
iostat -xz 1 3
# Device r/s r_await aqu-sz %util
# nvme0n1 2103.0 1.82 8.2 78.20
# ^^^^ 从 12ms 降到 1.8ms
⑧ 根治:三个层面
# ① 让大文件顺序读不污染 page cache
# 应用层用 posix_fadvise(POSIX_FADV_DONTNEED) 主动丢弃已读数据
# Python 里的做法
import os
with open('/data/big.csv', 'rb') as f:
while chunk := f.read(1024 * 1024):
process(chunk)
# 告诉内核这段数据不会再用,可以立即回收
os.posix_fadvise(f.fileno(), 0, f.tell(), os.POSIX_FADV_DONTNEED)
# ② 用 cgroup v2 限制 ETL 任务的 IO(第 19 篇会详讲)
mkdir -p /sys/fs/cgroup/etl.slice
echo "259:0 rbps=52428800" > /sys/fs/cgroup/etl.slice/io.max # 限制 50MB/s
echo 15234 > /sys/fs/cgroup/etl.slice/cgroup.procs
# 或者用 systemd(第 14 篇)
# /etc/systemd/system/etl.service
[Service]
Nice=19
IOSchedulingClass=idle
IOReadBandwidthMax=/dev/nvme0n1 50M
MemoryMax=2G
# ③ 加监控:page cache 命中率 + 磁盘队列长度
# 这两个指标在本次故障中最先劣化,是最好的早期信号
结果:P99 从 2.8s 回到 130ms,cachestat 命中率回到 94%,aqu-sz 降到 0.9。
规律总结:
| 现象 | 指向 |
|---|---|
wa 高 + b 列高 |
磁盘瓶颈(不是 CPU) |
await 超基准数倍 + aqu-sz > 1 |
磁盘真饱和 |
| page cache 命中率骤降 | 缓存被冲掉,通常是大文件顺序读 |
内存 used 上涨 + cache 下降 |
有进程在吃内存 |
top/ps 抓不到的 CPU 毛刺 |
用 execsnoop 抓短命进程 |
核心教训:批处理任务和在线服务共享磁盘时,必须用 ionice 或 cgroup 隔离——否则一个全量扫描就能拖垮整个在线服务。
9. 工具速查表
按「我想知道什么」组织:
| 想知道 | 用什么 |
|---|---|
| 系统整体忙不忙 | uptime、vmstat 1 |
| CPU 花在哪 | top(1 看各核)、mpstat -P ALL、perf top |
| 单核是否成瓶颈 | mpstat -P ALL 1 |
| 哪个进程/线程耗 CPU | pidstat -u、top -H |
| CPU 在执行什么函数 | perf top、火焰图 |
| 调度延迟多少 | runqlat |
| 内存够不够 | free -h 看 available |
| 谁在吃内存 | ps --sort=-rss、/proc/<pid>/smaps_rollup 的 Pss |
| 有没有内存泄漏 | pidstat -r 看 RSS 趋势、memleak |
| 磁盘是否饱和 | iostat -xz 1 看 await、aqu-sz |
| 哪个进程在读写磁盘 | pidstat -d、iotop -oPa |
| 磁盘延迟分布 | biolatency |
| 缓存命中率 | cachestat、/proc/<pid>/io 的 rchar vs read_bytes |
| 网络吞吐 | sar -n DEV 1、iftop |
| 连接状态分布 | ss -s、ss -tan state xxx |
| 有没有丢包重传 | sar -n ETCP、nstat、ip -s link |
| 进程在等什么 | strace -p、/proc/<pid>/wchan、offcputime |
| 系统调用分布 | strace -c -f、perf trace |
| 谁在创建短命进程 | execsnoop |
| 谁在打开某个文件 | opensnoop、fatrace |
| 历史数据(问题已过去) | sar -f /var/log/sysstat/saXX |
| 内核在做什么 | perf top(看 [k])、bpftrace |
10. 面试题
Q:USE 方法论是什么?为什么说饱和度比使用率更重要?
USE 是对每类资源检查三个维度:Utilization(使用率,有多忙)、Saturation(饱和度,有多少在排队)、Errors(错误)。饱和度更重要是因为使用率 100% 不一定有问题——最典型的例子是 SSD 的 %util,它的定义是「至少有一个请求在处理的时间占比」,源自机械盘单磁头时代;NVMe 有几十上千个并行队列,%util=100% 可能只用了 5% 的实际能力。而队列在增长一定有问题——iostat 的 aqu-sz > 1、vmstat 的 r 列超过核数、accept 队列的 Recv-Q 接近 Send-Q,这些都是明确的饱和信号。所以判断磁盘瓶颈要看 await 和 aqu-sz,不看 %util。
Q:登上一台变慢的机器,你按什么顺序排查?
用 60 秒定位法:① uptime 看负载趋势(三个数字的变化方向比绝对值重要);② dmesg -T | tail 先排除 OOM、磁盘错误、网卡重置这类硬故障——这一步不能跳过,否则后面的分析都失去意义;③ vmstat 1 看全局,按 r(CPU 排队)→ b(IO 阻塞)→ si/so(换页)→ wa(IO 等待)→ st(虚拟化偷取)→ us vs sy 的顺序扫一遍;④ mpstat -P ALL 1 看核间均衡——这能发现 top 看不出的单核瓶颈(比如网络软中断全压在 CPU0);⑤ pidstat -urdw 定位到具体进程;⑥ iostat -xz、free -h、sar -n DEV 深入各子系统。核心原则是先用零开销的计数器型工具收敛范围,再用有开销的追踪型工具(strace/perf)深入。
Q:strace、perf、bpftrace 分别适合什么场景?
strace 基于 ptrace 拦截每次系统调用,进程会被停两次,开销 10~100 倍——只能短时用、必须配 timeout 和 -c,适合快速看「系统调用分布异常」。perf 基于采样(默认按 CPU 周期定期抓栈),开销通常 < 5%,能看到内核态函数——这是它相对语言级 profiler(pprof)的关键优势,%system 高时 pprof 完全无能为力。bpftrace/eBPF 在内核内挂钩子并直接聚合,开销最小、可常态运行,能观测系统调用+内核函数+用户函数+硬件事件,还能在内核里直接生成直方图(hist())——这点很重要,因为平均值会掩盖长尾,直方图能直接暴露 P99 问题。顺序:先 perf/bpftrace,strace 只在需要看具体参数时用。
Q:火焰图怎么看?看宽的还是看高的?
看宽的。横轴不是时间,而是把采样到的调用栈按函数名排序后堆叠,宽度 = 该函数出现在采样中的比例 = 占用 CPU 的比例。纵轴是调用栈深度,栈很深(很高)只说明调用层次多,不代表慢。所以要找宽的平顶(plateau)——那就是 CPU 实际消耗的地方,然后从它往下看调用链确定是谁调用的。常见问题是出现大量 [unknown]:原因是二进制被 strip 了或缺 frame pointer,C/C++ 要加 -fno-omit-frame-pointer -g,Go 不要用 -ldflags="-s -w",Java 用 async-profiler,Python 用 py-spy。另外火焰图只能看 on-CPU,等锁、等 IO 这类问题要用 offcputime 生成 off-CPU 火焰图——延迟高但 CPU 不高时必须看这个。
Q:perf stat 里的 IPC 是什么?低了说明什么?
IPC = instructions per cycle,每个 CPU 周期执行的指令数。IPC > 2.0 说明流水线利用充分,< 1.0 说明 CPU 大量时间在等内存而不是在计算。这个指标的价值在于它能区分两种「CPU 100%」:一种是真的在做计算(IPC 高),优化方向是降低算法复杂度;另一种是在空转等内存(IPC 低,配合 cache-misses 高),优化方向是改善数据局部性——把链表换成数组、减少指针跳转、调整结构体字段顺序减少 cache line 浪费。后一种情况下优化算法复杂度往往没有效果,因为瓶颈不在指令数而在内存访问。
Q:top/ps 里抓不到的 CPU 毛刺怎么排查?
用 execsnoop(BCC 工具)。top 和 ps 都是采样的——top 默认 3 秒刷新,ps 是瞬时快照,存活只有几十毫秒的进程根本抓不到。execsnoop 基于 eBPF 挂在 exec 系统调用上,能捕获每一次进程创建,输出时间、进程名、PID、PPID 和完整命令行。典型用途:发现挖矿脚本、失控的健康检查(每秒 fork 几十个 curl)、反复崩溃重启的进程、以及定时任务风暴。这是「CPU 有规律毛刺但找不到进程」这类问题的唯一有效手段。
Q:sar 有什么独特价值?
它是唯一能回答「昨晚 3 点发生了什么」的工具。其他工具都只能看当下,而 sar(sysstat 包)默认每 10 分钟采集一次系统各项指标并持久化到 /var/log/sysstat/,保留 7~28 天。所以「凌晨服务抖动」「上周五的故障」这类已经过去的问题,只能靠 sar -f /var/log/sysstat/sa06 -s 03:00:00 -e 04:00:00 来回溯 CPU、内存、IO、网络、负载的历史曲线,进而判断是定时任务、流量高峰还是资源耗尽。生产环境务必开启 sysstat——没有它就只能等问题复现,而有些问题一个月才出现一次。
Q:压测时要注意哪些陷阱?什么是协调遗漏?
六个常见陷阱:① 压测客户端成了瓶颈(先确认客户端 CPU < 70%);② 没预热(JIT 未编译、连接池未建立、page cache 未填充,先跑一分钟丢弃结果);③ 只看平均值(平均 5ms 可能是 99% 的 1ms + 1% 的 400ms,必须看 P99/P999);④ 数据集太小全部命中缓存;⑤ 客户端与服务端同机互抢 CPU;⑥ 协调遗漏(coordinated omission)——这是最隐蔽的一个:wrk、ab 这类工具是「发一个等一个」,服务端变慢时客户端会自动降低发送速率,于是测出的延迟比真实情况乐观得多,掩盖了真正的长尾。解法是用 wrk2 -R <QPS> 或 vegeta 做固定速率压测,不管服务端多慢都按设定速率发送。另外压测的价值一半在于同步观测系统指标(vmstat + pidstat),这样能直接看出瓶颈在 CPU、磁盘还是锁竞争。
Q:在线服务和批处理任务跑在同一台机器上,怎么避免互相影响?
三个层面。① IO 优先级:ionice -c 3 -p <pid> 把批处理设为 idle 级别,只在磁盘空闲时读写——注意 nice 只管 CPU 不管 IO,两者是独立的。② cgroup 限制:cgroup v2 的 io.max 限带宽和 IOPS、memory.max 限内存、cpu.max 限 CPU;用 systemd 更方便,unit 里写 Nice=19、IOSchedulingClass=idle、IOReadBandwidthMax=、MemoryMax=。③ 避免缓存污染——这一点最容易被忽略:批处理顺序扫描大文件会把 page cache 里的在线服务热数据全部挤掉,导致命中率从 95% 掉到 10%,即使 IO 带宽没打满,在线服务也会因为大量 cache miss 而变慢。解法是应用层用 posix_fadvise(POSIX_FADV_DONTNEED) 主动丢弃已读数据,或用 O_DIRECT 完全绕过 page cache。监控上要盯 cachestat 的命中率,它往往是这类故障最早劣化的指标。
上一篇:Linux-17 SSH 密钥管理与多账号实战 | 下一篇:Linux-19 namespace 与 cgroup
xingliuhua