性能排查进阶:perf 与 eBPF 的眼睛
第 9 章讲了 top、free、日志这些"体检报告"。这一章讲拿到报告之后怎么办——机器慢了,怎么在十分钟内指出是哪一行代码、哪个进程、哪块盘的问题。核心工具就四个:vmstat 看全局、iostat 看磁盘、perf 看 CPU 热点、bpftrace 看内核里正在发生什么。
先想再敲:USE 方法,别瞎试
是什么:USE 是 Brendan Gregg 提出的一套排查口诀——对每一类资源,依次问三个问题:Utilization(用得有多满)、Saturation(有没有在排队)、Errors(有没有报错)。
为什么有用:新手排障的典型姿势是"看到 CPU 高就往 CPU 上查、看到内存高就加内存",然后在错误的资源上耗掉两小时。USE 强迫你先把四类资源都过一遍,很快就能排除掉三个,剩下那个才是真的。
| 资源 | 使用率 U | 饱和度 S(排队) | 错误 E |
|---|---|---|---|
| CPU | mpstat -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 卡、还是在换页。方向错了,工具越高级越浪费时间。
| 列 | 含义与判断标准 |
|---|---|
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 盘上一个慢请求就能把这一秒填满)。
| 列 | 含义与判断 |
|---|---|
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 的输出怎么下结论
生产上最常见的磁盘 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 能做。
| 指标 | 怎么解读 |
|---|---|
| Children / Self 占比 | Self 是这个函数自己烧的时间,Children 是它调用的下游加起来的时间。优化要找 Self 高的;Children 高说明得往下钻,罪魁在更深处。 |
| IPC | Instructions Per Cycle,每周期执行多少条指令。现代 CPU 一般能到 1.5~4;如果 IPC 明显低于 1,说明 CPU 大量时间在等内存(cache miss)或分支预测失败——此时加机器不如先改数据结构。 |
| cache-misses | 缓存未命中率过高(比如超过 5%)时,程序往往是"内存访问模式不友好",比如遍历链表、随机访问大数组。 |
① 装不上: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 的定位是"可观测 + 只读为主":它能看、能统计、能限流,但炸不了内核。这也是它能在生产环境大规模铺开的原因。
| 工具 | 回答什么问题 |
|---|---|
execsnoop | 谁在疯狂起进程?一秒几百个 exec 的元凶往往不是业务,而是某个写坏的脚本或健康检查。 |
biolatency | 磁盘 IO 延迟的直方图。比平均值有用得多:平均值 2ms 但 1% 的请求要 500ms,才是真正的用户感知问题。 |
biosnoop | 每一次 IO:谁发的、读写哪个扇区、多少字节、耗时多久。定位"谁在偷偷读盘"。 |
runqlat | 调度延迟直方图:进程从"想跑"到"真跑上"等了多久。CPU 不够用时的直接证据。 |
funclatency | 某个内核函数的耗时分布,比如 funclatency-bpfcc vfs_read。 |
tcpconnect / tcplife / tcpdrop | 谁连了谁、连接活了多久、哪些包被内核丢了。比抓包更适合看"连接级"的宏观画像。 |
memleak | 跟踪未释放的内存分配,定位用户态或内核态的内存泄漏。 |
bpftrace 一行命令流:不用写程序,直接问问题
① 内核太老:基本能力要 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 负责内核态事实,两者都能在生产上低开销使用;但前提是先存证再动手,别在排障过程中把现场破坏了。