目录

Linux-18 性能分析工具链与方法论

前置阅读:Linux-07 进程调度与上下文切换Linux-10 内存管理Linux-11 文件系统与磁盘 IO

前面几篇讲了 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 topus+sy vmstatr 列 > 核数
内存 freeavailable 占比 si/so 换页、majflt dmesg 的 OOM
磁盘 iostat%util(参考) awaitaqu-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    |
    +----------------------------------------+

工具的两大类别,用途完全不同

类型 代表 特点 用途
计数器型 vmstatiostatsar 读内核已有的统计,开销近零 常态监控、初步定位
追踪型 straceperfbpftrace 拦截事件,有开销 深入定位,短时使用

**规律:先用计数器型工具定位到「哪个资源、哪个进程」,再用追踪型工具深入「具体是什么调用」。**顺序反了会付出不必要的性能代价。

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

./top输出.png

3.1 必须掌握的交互键

作用
1 展开每个 CPU 核(等效 mpstat,看均衡性)
H 显示线程而不是进程
M 按内存排序
P 按 CPU 排序(默认)
T 按累计时间排序
c 显示完整命令行
x 高亮排序列
V 树状显示父子关系
f 选择显示哪些列(可加 nMajSWAPP
W 保存当前配置到 ~/.toprc
k 杀进程
e/E 切换内存单位

1H 是最有价值的两个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 有毛刺但抓不到进程(因为进程存活只有几十毫秒),pstop 的采样根本抓不到。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=16wa=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 的历史数据。这带来两个后果:

  1. page cache 被冲掉——大量一次性读入的数据挤掉了 MySQL 的热数据缓存
  2. 磁盘 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. 工具速查表

按「我想知道什么」组织:

想知道 用什么
系统整体忙不忙 uptimevmstat 1
CPU 花在哪 top1 看各核)、mpstat -P ALLperf top
单核是否成瓶颈 mpstat -P ALL 1
哪个进程/线程耗 CPU pidstat -utop -H
CPU 在执行什么函数 perf top、火焰图
调度延迟多少 runqlat
内存够不够 free -havailable
谁在吃内存 ps --sort=-rss/proc/<pid>/smaps_rollupPss
有没有内存泄漏 pidstat -r 看 RSS 趋势、memleak
磁盘是否饱和 iostat -xz 1awaitaqu-sz
哪个进程在读写磁盘 pidstat -diotop -oPa
磁盘延迟分布 biolatency
缓存命中率 cachestat/proc/<pid>/iorchar vs read_bytes
网络吞吐 sar -n DEV 1iftop
连接状态分布 ss -sss -tan state xxx
有没有丢包重传 sar -n ETCPnstatip -s link
进程在等什么 strace -p/proc/<pid>/wchanoffcputime
系统调用分布 strace -c -fperf trace
谁在创建短命进程 execsnoop
谁在打开某个文件 opensnoopfatrace
历史数据(问题已过去) sar -f /var/log/sysstat/saXX
内核在做什么 perf top(看 [k])、bpftrace

10. 面试题

Q:USE 方法论是什么?为什么说饱和度比使用率更重要?

USE 是对每类资源检查三个维度:Utilization(使用率,有多忙)、Saturation(饱和度,有多少在排队)、Errors(错误)。饱和度更重要是因为使用率 100% 不一定有问题——最典型的例子是 SSD 的 %util,它的定义是「至少有一个请求在处理的时间占比」,源自机械盘单磁头时代;NVMe 有几十上千个并行队列,%util=100% 可能只用了 5% 的实际能力。而队列在增长一定有问题——iostataqu-sz > 1vmstatr 列超过核数、accept 队列的 Recv-Q 接近 Send-Q,这些都是明确的饱和信号。所以判断磁盘瓶颈要看 awaitaqu-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 -xzfree -hsar -n DEV 深入各子系统。核心原则是先用零开销的计数器型工具收敛范围,再用有开销的追踪型工具(strace/perf)深入。

Q:straceperfbpftrace 分别适合什么场景?

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 工具)。topps 都是采样的——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)——这是最隐蔽的一个:wrkab 这类工具是「发一个等一个」,服务端变慢时客户端会自动降低发送速率,于是测出的延迟比真实情况乐观得多,掩盖了真正的长尾。解法是用 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=19IOSchedulingClass=idleIOReadBandwidthMax=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