楼层: 首页/ 软件技术/ Linux/ 性能排查进阶:perf 与 eBPF 的眼睛
15

性能排查进阶:perf 与 eBPF 的眼睛

perf · eBPF · sar · vmstat · iostat

第 9 章讲了 top、free、日志这些"体检报告"。这一章讲拿到报告之后怎么办——机器慢了,怎么在十分钟内指出是哪一行代码、哪个进程、哪块盘的问题。核心工具就四个:vmstat 看全局、iostat 看磁盘、perf 看 CPU 热点、bpftrace 看内核里正在发生什么。

先想再敲:USE 方法,别瞎试

是什么:USE 是 Brendan Gregg 提出的一套排查口诀——对每一类资源,依次问三个问题:Utilization(用得有多满)、Saturation(有没有在排队)、Errors(有没有报错)。

为什么有用:新手排障的典型姿势是"看到 CPU 高就往 CPU 上查、看到内存高就加内存",然后在错误的资源上耗掉两小时。USE 强迫你先把四类资源都过一遍,很快就能排除掉三个,剩下那个才是真的。

USE 速查表:每一格右边的命令,就是这一格的答案来源
资源使用率 U饱和度 S(排队)错误 E
CPUmpstat -P ALL 1 看每核 us+sy;单核打满最常见。vmstat 的 r 列 > 核数;runqlat-bpfcc 看调度延迟。dmesg -T 里的 MCE / 硬件报错。
内存free -m 看 available(不是 free!)。vmstat 的 si/so 非 0 = 在用 swap;/proc/pressure/memory。dmesg 里的 OOM killer 记录。
磁盘iostat -xz 1 的 %util。aqu-sz 和 await(SSD 上 await 应远小于 1ms)。dmesg 的 I/O error;smartctl -a 看盘健康。
网络sar -n DEV 1 看带宽占比。ss -ti 的 retrans;ip -s link 的 drop/overrun。nstat -az | grep -i -E 'err|drop'。

一分钟看全局:vmstat 与 sar

是什么:vmstat 是"一次采样、看到全部"的系统级快照工具;sar 是它的历史版本——它会把数据记录到磁盘,让你事后回看"昨晚 3 点到底发生了什么"。

为什么先看它:因为 perf 这类工具是"往下挖"的,你得先知道往哪个方向挖。vmstat 一屏就能告诉你:是 CPU 忙、还是 IO 卡、还是在换页。方向错了,工具越高级越浪费时间。

# ── 一分钟拿到全局画像 ── uptime # 1/5/15 分钟平均负载 dmesg -T | tail -50 # 内核最近说了什么:OOM、磁盘错、网卡 down 都在这里 vmstat 1 5 # 每秒采样一次,共 5 次 iostat -xz 1 5 # -x 扩展信息、-z 不显示全零设备 mpstat -P ALL 1 5 # 每个 CPU 核分别的占用(看是不是单核打满) pidstat -u -d 1 5 # 哪个进程在吃 CPU(-u)和磁盘(-d) # ── sar:打开历史记录,才能复盘"半夜那次卡顿" ── sudo apt install -y sysstat # Debian / Ubuntu sudo sed -i 's/^ENABLED="false"/ENABLED="true"/' /etc/default/sysstat sudo systemctl restart sysstat sar -u 1 3 # CPU(这三条是实时看) sar -r 1 3 # 内存 sar -n DEV 1 3 # 网卡 sar -d 1 3 # 磁盘 sar -u -f /var/log/sysstat/sa20 # 回看 20 号那天的数据(事后复盘神器)
vmstat 字段对照:重点盯这六列,别的可以先忽略
列含义与判断标准
r正在运行 + 等待 CPU 的进程数。持续大于 CPU 核数 → CPU 真的不够用了。
b处于不可中断睡眠(D 状态)的进程数。这些进程多半卡在磁盘或网络文件系统上,杀都杀不掉。
si / so每秒从 swap 换入/换出的内存量。只要不是 0,就说明内存已经不够了——一旦开始 swap,性能会断崖式下跌,这是最高优先级的问题。
us / sy用户态 / 内核态 CPU 占比。sy 长期偏高往往意味着系统调用太频繁、上下文切换太多,或者被虚拟化/安全软件拖累。
wa等待 IO 的 CPU 时间占比。注意:wa 高不代表 IO 是瓶颈,只代表"CPU 闲在这里等"。要配合 iostat 确认磁盘真的忙。
cs每秒上下文切换次数。几万到几十万次是"切换风暴",通常是线程数过多或锁竞争导致,此时 CPU 看着很忙但吞吐很低。

负为什么"负载很高但 CPU 很空闲"

因为 Linux 的平均负载(load average)把 D 状态(不可中断睡眠)的进程也算进去了。一个卡在坏盘、卡在 NFS、卡在云端块存储超时的进程,会一直占着 load 而不消耗 CPU。

怎么确认:vmstat 的 r 很小、b 却有值 —— 这就是"负载高、CPU 闲"的指纹。接着用 ps -eo state,pid,ppid,comm | awk '$1=="D"' 把 D 状态进程列出来,再用 cat /proc/<PID>/stack(需要 root)看它卡在哪个内核函数里。

iostat:磁盘到底是真瓶颈还是背锅

是什么:iostat(sysstat 包)按设备粒度统计吞吐、延迟、队列深度。它是"磁盘慢不慢"的唯一权威答案。

为什么不能只看 %util:很多人一看 %util 接近 100% 就断定磁盘打满了。但 %util 的含义是"设备有 IO 在飞行的时间比例",对能并发处理大量请求的 NVMe SSD 或多盘 RAID 来说,它可以在几乎满负载时依然很低,也可能在完全没饱和时显示 100%(单通道 SATA 盘上一个慢请求就能把这一秒填满)。

iostat -xz 关键列:把 await 和 aqu-sz 放在一起看,结论才可靠
列含义与判断
r/s w/s每秒读、写请求数(IOPS)。先看是不是数量本身就很夸张。
rkB/s wkB/s每秒读写吞吐量。判断是不是打满了盘的带宽(比如 1Gbps 网盘约 125MB/s)。
r_await w_await读、写请求的平均完成时间(含排队)。写延迟通常明显高于读,这是正常的,别只看一个。
aqu-sz平均队列长度(老版本叫 avgqu-sz)。大于 1~2 说明请求已经在排队,磁盘是真的来不及。
%util设备忙的时间占比。只在单盘 HDD/SATA SSD 上有参考价值,NVMe/RAID 上会严重失真。

实战:一次 iostat -xz 1 的输出怎么下结论

Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz %util sda 0.00 812.0 0.0 32480.0 0.00 28.40 6.8 99.2 nvme0n1 120.0 240.0 4800.0 9600.0 0.12 0.30 0.4 12.5 # sda:w_await 28ms、aqu-sz 6.8、%util 99% → 三者互相印证,这块盘确实是瓶颈 # (28ms 对机械盘偏高,对单块 SATA SSD 已经明显不正常) # nvme:%util 只有 12%,但每秒在扛 360 次 IO,延迟 0.1~0.3ms → 完全健康 # → 只看 %util 的人会误判 nvme 没问题,也可能误判 sda 的 99% 是"正常的满负载" # 下一步:找出是谁在写盘 sudo iotop -oPa # -o 只看有 IO 的、-P 按进程、-a 累计量 sudo pidstat -d 1 5 # 或者用 pidstat 看每个进程每秒读写多少 KB
坑:把"日志写爆磁盘"当成"磁盘坏了"

生产上最常见的磁盘 IO 打满,元凶不是数据库,而是某个程序在疯狂打日志——一行日志一次 write,还带 fsync,几万 QPS 的循环日志足以让任何盘跪下。看到 w_await 高,用 iotop -oPa 定位到进程后,先去看它的日志量和日志级别,再考虑换 SSD。

perf:找到"哪一行代码在烧 CPU"

是什么:perf 是 Linux 内核自带的性能分析工具(内核里的 perf_events 子系统 + 用户态命令行)。它通过硬件/软件性能计数器采样,告诉你 CPU 时间花在了哪些函数上,还能采到调用链(栈)。

为什么是 CPU 问题的终点站:top 只能告诉你"是 Java 占了 300%",而 perf 能告诉你"是 String.split 占了其中 40%"。从"哪个进程"到"哪个函数",这一步只有 profiler 能做。

# ── 实时看热点(最直观,先跑这个) ── sudo perf top -g # -g 打开调用图,能看出是谁调用的 # ── 采样并离线分析(更准,能对着报告慢慢看) ── sudo perf record -F 99 -p <PID> -g -- sleep 30 # 对 PID 以 99Hz 采样 30 秒 sudo perf record -F 99 -a -g -- sleep 30 # -a 全系统采样 sudo perf report --stdio # 文本模式看热点 sudo perf report -g graph,0.5,caller # 看调用方(谁拖累了谁) # ── 看计数器而不是采样:IPC、cache miss、分支预测 ── sudo perf stat -p <PID> sleep 5 sudo perf stat -e cycles,instructions,cache-misses,branch-misses -p <PID> sleep 5 # ── 火焰图(把千万个采样压成一张图,一眼看出最大的那一坨) ── sudo perf record -F 99 -a -g -- sleep 30 sudo perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg # 上面两个 .pl 来自 Brendan Gregg 的 FlameGraph 仓库,clone 下来即可用
perf 报告里的两个关键数字:会看它们,才算真的会看 perf
指标怎么解读
Children / Self 占比Self 是这个函数自己烧的时间,Children 是它调用的下游加起来的时间。优化要找 Self 高的;Children 高说明得往下钻,罪魁在更深处。
IPCInstructions Per Cycle,每周期执行多少条指令。现代 CPU 一般能到 1.5~4;如果 IPC 明显低于 1,说明 CPU 大量时间在等内存(cache miss)或分支预测失败——此时加机器不如先改数据结构。
cache-misses缓存未命中率过高(比如超过 5%)时,程序往往是"内存访问模式不友好",比如遍历链表、随机访问大数组。
坑:perf 装不上、采不到栈、看不了内核

① 装不上:perf 必须和内核版本匹配。Ubuntu 用 sudo apt install linux-tools-common linux-tools-$(uname -r);装错了版本会报 perf not found for kernel。② 采不到调用栈:多半是程序编译时省掉了帧指针。用 -fno-omit-frame-pointer 重新编译(C/C++ 项目现代编译器默认在 -O2 下会省略它)。③ 看不了内核栈:内核参数限制在 /proc/sys/kernel/perf_event_paranoid(默认 2)。设成 1 才能采内核态,0 才能做更宽的采集——但这是安全相关的开关,生产上改完记得评估影响,别长期开着。④ 容器里跑不了:需要 --privileged 或加 CAP_PERFMON/CAP_SYS_ADMIN。

eBPF:不用改代码、不用重启,看内核在干什么

是什么:eBPF(extended Berkeley Packet Filter)允许你把一小段程序动态挂到内核的任意钩子上(函数入口/出口、tracepoint、定时采样),在内核里执行并统计结果。bpftrace 和 BCC(*-bpfcc 那些工具)就是它的易用外壳。

为什么它是"观测能力的革命":以前想知道"谁在频繁 fork""哪个进程的 IO 延迟最高",你得改代码重新发布,或者上笨重昂贵的商业 APM。eBPF 让你在正在跑的生产机上,用一行命令问到答案,且开销极低。

安为什么 eBPF 敢在内核里跑陌生代码

因为内核里有个 verifier(校验器):程序加载前先做静态检查——不能有不可控的循环、不能越界访问内存、指令数有上限、不允许无边界指针运算。只有通过校验的程序才会被 JIT 编译执行,并且它读写数据只能通过受控的 map,不能直接改内核结构。

所以 eBPF 的定位是"可观测 + 只读为主":它能看、能统计、能限流,但炸不了内核。这也是它能在生产环境大规模铺开的原因。

BCC 工具里的"随身听":装上 bpfcc-tools / bcc-tools 就能直接用
工具回答什么问题
execsnoop谁在疯狂起进程?一秒几百个 exec 的元凶往往不是业务,而是某个写坏的脚本或健康检查。
biolatency磁盘 IO 延迟的直方图。比平均值有用得多:平均值 2ms 但 1% 的请求要 500ms,才是真正的用户感知问题。
biosnoop每一次 IO:谁发的、读写哪个扇区、多少字节、耗时多久。定位"谁在偷偷读盘"。
runqlat调度延迟直方图:进程从"想跑"到"真跑上"等了多久。CPU 不够用时的直接证据。
funclatency某个内核函数的耗时分布,比如 funclatency-bpfcc vfs_read。
tcpconnect / tcplife / tcpdrop谁连了谁、连接活了多久、哪些包被内核丢了。比抓包更适合看"连接级"的宏观画像。
memleak跟踪未释放的内存分配,定位用户态或内核态的内存泄漏。

bpftrace 一行命令流:不用写程序,直接问问题

# ── 装 ── sudo apt install -y bpftrace bpfcc-tools # Debian / Ubuntu sudo dnf install -y bpftrace bcc-tools # RHEL / Fedora # 注意:Ubuntu 的工具名带 -bpfcc 后缀,RHEL 系是原名 # ── 谁在调用 read(按进程名统计次数) ── sudo bpftrace -e 'kprobe:vfs_read { @[comm] = count(); }' # ── 谁在 exec(实时流式打印) ── sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-16s %s\n", comm, str(args->filename)); }' # ── 每 99Hz 采一次内核调用栈(自建火焰图的数据源) ── sudo bpftrace -e 'profile:hz:99 { @[kstack] = count(); }' # ── 谁被换出 CPU 最频繁(锁竞争 / 线程过多的信号) ── sudo bpftrace -e 'tracepoint:sched:sched_switch { @[prev_comm] = count(); }' # ── 直接用 BCC 封装好的 ── sudo biolatency-bpfcc 10 1 # 每 1 秒打印一次 IO 延迟直方图 sudo runqlat-bpfcc # 调度延迟直方图 sudo execsnoop-bpfcc # 实时看新起的进程
坑:eBPF 用不了,先查这四条

① 内核太老:基本能力要 4.9+,完整的 tracepoint/kprobe 体验建议 5.x 以上(uname -r 看版本)。② 权限不够:需要 root(或 CAP_BPF + CAP_PERFMON)。③ 容器里看不见内核:容器需要有相应的 capability,并且 /sys/kernel/debug(tracefs)要挂进来。④ verifier 报错看不懂:出现的 -EINVAL / Permission denied 八成是"访问了没做边界检查的指针",而不是权限问题——这时把 -v 加上看校验日志。另外提醒:别在高负载生产机上跑无过滤的全量 kprobe,bpftrace 自己也是有开销的。

三种典型症状的排查剧本

照着走就行:每一步都是"缩小范围",不是"试一下"
症状排查剧本
CPU 高 mpstat -P ALL 1 先看是单核打满还是全核打满——单核打满通常是单线程瓶颈或一把大锁;全核打满看是不是真的业务量大。→ pidstat -u 1 找到进程。→ perf top -g 找到函数。→ 如果是 JVM,用 jcmd <PID> Thread.print / async-profiler 看是不是 GC 在烧 CPU。
负载高、CPU 空闲 vmstat 1 看 b 列和 si/so。→ ps -eo state,pid,comm | awk '$1=="D"' 列出 D 状态进程。→ iostat -xz 1 确认盘忙不忙;dmesg -T | tail 看有没有 NFS/块设备超时。→ 这类进程 kill -9 也杀不掉,要先解决底层 IO。
内存缓慢增长 free -m 连续看 available 的趋势(不是 free!Linux 会拿空闲内存做缓存,free 少很正常)。→ ps -eo pid,comm,rss --sort=-rss | head 找 RSS 最大的进程。→ 看趋势:pmap -x <PID> 或 cat /proc/<PID>/smaps_rollup 区分是堆还是 mmap。→ JVM 看 jcmd <PID> GC.heap_info;native 用 memleak-bpfcc。关键判据是"持续增长不回落",短期波动不是泄漏。
坑:在生产机上"顺手调个参数试试"

排查阶段有三个动作看着无害、实际会毁掉现场:① echo 3 > /proc/sys/vm/drop_caches——它会清掉页缓存,让正常业务瞬间承受所有磁盘读,性能雪崩;② 调 vm.swappiness 或重启服务"看看好没好"——问题被暂时掩盖,证据没了;③ 直接 kill -9 一个 D 状态进程——多半杀不掉,还可能留下更乱的锁状态。正确做法:先采样、先存证(perf.data、pcap、jstack 输出),再动手改。

记
本章小结

① 排查顺序是 先全局后细节:vmstat 定方向 → iostat/ss -ti 定资源 → perf/bpftrace 定代码。用 USE 三问(使用率 / 排队 / 错误)保证不漏项。

② "负载高但 CPU 闲"要想到 D 状态进程(IO 卡住);"磁盘忙"不能只看 %util(NVMe 上会失真),要看 await 和 aqu-sz。

③ perf 负责用户态热点、eBPF 负责内核态事实,两者都能在生产上低开销使用;但前提是先存证再动手,别在排障过程中把现场破坏了。