第4章 可观测性工具
从计数器到动态追踪
章节导读
工具是性能工程师的武器库,但武器库太大反而让人迷失:Linux 上有数百个性能工具,功能重叠、口径各异、开销悬殊。本章的任务不是罗列工具,而是建立一张"工具分类地图"——按观测方法(计数器、剖析、追踪)与观测目标(CPU、内存、I/O、网络)两个维度组织所有工具,让你面对任何问题时能在 10 秒内说出"该用哪类工具、具体哪一个、开销多大"。
先建立方法论框架。Gregg 把性能观测方法分为三大类。第一类是计数器(counters):内核持续维护的累积统计,读取近乎零开销,如 /proc/stat 的 CPU 时间、/proc/net/dev 的收发包数、iostat 背后的块层统计。计数器回答"发生了多少"(how much),便宜到可以永远开着,是所有监控系统的基石;缺点是只有汇总没有归因——你知道重传了 1 万个包,不知道是哪个进程、哪条连接。第二类是剖析(profiling):以固定频率采样系统状态(通常是调用栈),用统计推断时间分布,如 perf record 每秒 99 次采样所有 CPU 的栈。剖析回答"时间花在哪",开销可控(1-5%),是定位热点的主力;缺点是采样有盲区——低于采样频率的短事件会漏掉,且默认只看到 on-CPU 时间。第三类是追踪(tracing):对每个事件(系统调用、I/O 完成、锁获取)记录详细数据,如 bpftrace 追踪每次 tcp_retransmit。追踪回答"具体发生了什么、为什么",精度最高但开销与事件频率成正比,需要谨慎使用。三类的关系是漏斗:计数器发现异常 → 剖析缩小范围 → 追踪锁定根因。
静态追踪与动态追踪是追踪的两种实现。静态追踪点(tracepoint)由内核/应用开发者预埋,稳定、有文档、开销可预期,Linux 内核有约 2000 个(sched、block、tcp、syscalls 等子系统),应用也可以埋自己的(USDT,User Statically-Defined Tracing,如 JVM、PostgreSQL、nginx 都有)。动态追踪(kprobe/uprobe)允许在运行时把探针挂到几乎任意函数入口或指令地址,无需修改代码与重启——这是 DTrace 在 2005 年带来的革命,Linux 通过 kprobes(2005)与 uprobes(2012)跟进。动态追踪的代价是无稳定性承诺(内核函数签名随版本变化)与略高的单事件开销。eBPF(extended Berkeley Packet Filter)是这一切的现代引擎:用户写的小程序经验证器安全检查后加载进内核,挂在 kprobe/tracepoint/perf 事件上,在内核态就地聚合(map),只把结果传回用户态——避免了"每事件拷贝到用户态"的传统开销,使生产环境动态追踪成为现实。BCC(2015-)提供 Python/Lua 前端,libbpf + CO-RE(一次编译到处运行)是新一代工程化方案,bpftrace(2018-)提供 awk 风格的一行命令 DSL。
火焰图(Flame Graph)是 Gregg 2011 年发明的可视化方法,值得单独一节。动机:perf report 的调用树在大型程序中有成千上万条路径,人眼无法从中提取"时间都花在哪"的整体图景。火焰图把成千上万条栈按"栈前缀合并"的方式组织:相同的栈底帧合并,宽度表示该代码路径的样本数(正比于时间)。读图三原则:X 轴是字母序不是时间序(不要解读为"先后");Y 轴是栈深度(上层帧调用下层帧——注意火焰图是"被调用者在上");找最宽的平顶(plateau)——那是热点。变体家族:CPU 火焰图(on-CPU 采样)、off-CPU 火焰图(阻塞时间归因,回答"在等什么")、内存火焰图(分配量归因)、差分火焰图(两次采样对比,红色变宽=变慢,用于版本对比与优化验证)、热度图(wakeup/latency 变体)、以及把多种资源合成一张的"subsecond offset"时间序列火焰图。火焰图的生成管线是固定的三步:采集(perf record -F 99 -a -g)→ 折叠(stackcollapse-perf.pl)→ 渲染(flamegraph.pl),各语言运行时(JVM、Node、Python、Go)有对应的栈解析器处理 JIT 与符号问题。
工具开销评估是生产环境使用的生命线。Gregg 的开销谱系(从低到高):读计数器(<0.1%)→ 采样剖析 99Hz(1-5%,与频率和栈深度成正比)→ tracepoint 计数(每事件约 0.5-1μs,低频事件可忽略)→ kprobe/uprobe 计数(每事件 1-2μs)→ 高频事件的全量追踪(每秒百万事件的 read 追踪可达 10-30% 开销)→ ptrace 类(strace,10-100 倍减速)→ 全量抓包(tcpdump 高 pps 下丢包且耗 CPU)。评估方法是 A/B 基准:相同负载下开关工具各跑一轮,对比吞吐与延迟。实用守则:生产环境常驻观测只用计数器与低频采样;追踪类工具限时(timeout)、限域(-p PID、按设备过滤)、限频(采样或聚合在内核态完成);永远先问"这个事件每秒发生多少次"再决定追踪策略。
本章最后给出按子系统的工具速查(以 Linux 为例):CPU——mpstat、pidstat、perf stat/record、BCC profile/runqlat、bpftrace;内存——free、vmstat、/proc/meminfo、slabtop、BCC memleak/oomkill、perf mem;磁盘——iostat、iotop、BCC biosnoop/biolatency/bitesize、blktrace;网络——sar -n、ss、netstat -s、tcpdump、BCC tcplife/tcpretrans/tcprtt、tcptop;应用——语言专属剖析器(第 5 章)。这张地图与第 2 章的 USE 矩阵正交组合:USE 告诉你查哪个资源的哪个维度,工具地图告诉你用什么查。两者合璧,就是 Gregg 所说的"性能分析的完整武器库"。
核心概念
- 计数器
- 剖析
- 追踪
- 火焰图
- eBPF
要点精析
计数器类工具的判读要点与陷阱。计数器是累积值,必须差分(两次采样相减除以间隔)才有意义;工具输出的"第一次采样"通常是开机以来的均值,必须丢弃(iostat/vmstat/sar 皆如此)。/proc/stat 的 CPU 各状态列(user/nice/system/idle/iowait/irq/softirq/steal/guest)中,iowait 的语义最常被误解:它是"CPU 空闲且有未完成 I/O"的时间,既不代表 I/O 慢(I/O 期间 CPU 可以干别的),在多核下口径也模糊(一个核空闲另一个核忙时怎么算),Gregg 明确建议把 iowait 当参考而非依据,判断 I/O 瓶颈应看 iostat 的 await 与队列深度、PSI io。另一个陷阱是计数器的"观察者口径":/proc/PID/io 的 read_bytes 是到达块层的量(页缓存命中不计),rchar 是 read 系统调用的总量,两者差值恰是缓存收益——分不清口径就会得出荒谬结论。
采样式剖析(sampling profiling)的统计学本质。perf record -F 99 每秒中断 99 次记录当前栈,30 秒 × 99Hz × 8 核 ≈ 23760 个样本;某函数出现在 10% 的样本中,即推断它消耗约 10% 的 CPU 时间,置信区间由样本量决定(10% 占比在 2 万样本下误差约 ±0.4%)。三个关键参数:频率(-F,太低统计噪声大,太高开销大且可能触发内核的 perf throttling——dmesg 中 "perf: interrupt took too long" 即被降频)、栈深度(-g 默认 dwarf 或 fp,栈回溯失败会产生断裂的火焰图,帧指针缺失是常见原因,编译时 -fno-omit-frame-pointer 或 perf 的 --call-graph dwarf 解决)、范围(-a 全系统 / -p 单进程 / -C 指定核)。采样剖析的固有盲区:短于采样间隔的事件(如 10μs 的锁等待)大概率漏采;睡眠中的任务不被采(所以 CPU 火焰图看不到等待,需要 off-CPU 分析)。插桩式剖析(instrumentation,每个函数入口计数)无此盲区但开销大一个量级,只适合开发环境。
追踪工具谱系:ftrace、perf、BCC、bpftrace、SystemTap 的定位。ftrace 是内核内建的追踪框架(function tracer、function_graph、irqsoff、wakeup 等专用 tracer),通过 /sys/kernel/debug/tracing/ 操作,零依赖、开销低,适合内核级问题(如"哪个函数关中断最久"用 irqsoff tracer)。perf 是用户态统一前端,覆盖计数器、采样、tracepoint 记录(perf record -e sched:sched_switch)与 perf trace。BCC 是 eBPF 工具集(100+ 现成工具)+ Python 开发框架,需要目标机有内核 headers 与 LLVM(运行时编译)。bpftrace 是高级 DSL,一行命令即席查询(bpftrace -e 'kprobe:vfs_read { @[comm] = hist(arg2); }'),适合交互式排障。libbpf-tools 是 BCC 工具的 CO-RE 重写版,单二进制、无需编译环境,适合生产部署。SystemTap 是前 eBPF 时代的方案,需要 kernel debuginfo,逐渐退役。选择原则:即席诊断用 bpftrace,现成工具用 BCC/libbpf-tools,生产常驻用 libbpf-tools 或自研 CO-RE 程序,内核自带能力用 ftrace/perf。
火焰图家族的完整图谱与适用场景。CPU 火焰图(profile 工具或 perf record 生成):定位计算热点,回答"CPU 时间花在哪些代码路径"。off-CPU 火焰图(offcputime 工具):定位阻塞热点,回答"任务的时间浪费在等待什么"——锁、I/O、网络、调度延迟,栈顶的内核函数是等待类型指纹(futex_wait、io_schedule、wait_on_page_bit)。内存火焰图(memleak 工具加 --stacks):按分配调用栈聚合未释放内存,定位泄漏与分配热点。差分火焰图(difffolded.pl):两次 CPU 剖析对比,红=第二次更多、蓝=更少,是"优化前后对比"与"故障时段 vs 健康时段对比"的利器。唤醒火焰图(wakeuptime):谁唤醒了谁,分析不必要的唤醒(wakeups 工具)。还有按时间切片的 subsecond offset 火焰图,能看到热点随时间的漂移。读图共同原则:宽度=时间/数量占比,X 轴字母序非时间序,找最宽平顶;对比两张火焰图时先确认总样本数口径一致。
eBPF 的工作原理与安全模型。eBPF 程序是受限的字节码(11 个 64 位寄存器、512 字节栈、指令数上限),经 bpf() 系统调用提交给内核验证器(verifier):验证器模拟执行所有路径,确保程序必然终止(禁止无界循环,新内核允许有界循环)、内存访问全部在界(map 值访问有范围检查)、指针不泄漏到用户态。通过验证后 JIT 编译为本机指令,性能接近手写内核 C 代码。程序挂载点包括:kprobe/kretprobe(内核函数入口/返回)、uprobe/uretprobe(用户程序函数)、tracepoint、perf_event、socket/XDP(网络包路径)、cgroup、LSM。程序与用户态通过 map 通信(哈希、数组、直方图、ring buffer),聚合在内核态完成是关键设计——每秒百万事件只需传回一个直方图。CO-RE(Compile Once Run Everywhere)通过 BTF(内核类型信息)解决"eBPF 程序依赖内核结构体布局"的可移植性问题,libbpf + clang 编译一次即可跨内核版本运行。这套机制让"生产环境安全地运行自定义内核观测逻辑"从梦想变成日常。
BCC 核心工具清单(按子系统)。CPU:profile(CPU 采样火焰图原料)、runqlat(调度延迟直方图)、runqlen(运行队列长度)、cpuunclaimed(空闲 CPU 上的任务);内存:memleak(用户态/内核态内存泄漏追踪)、oomkill(OOM 事件详情)、slabratetop(slab 消耗排行);磁盘:biosnoop(每个块 I/O 的延迟明细:进程、设备、扇区、延迟)、biolatency(I/O 延迟直方图,对数桶)、bitesize(I/O 大小分布)、filetop/fileslower(文件级 I/O 排行与慢文件操作)、xfsslower/ext4slower(文件系统慢操作);网络:tcplife(每条 TCP 连接的生命周期:时长、流量)、tcpretrans(重传事件明细)、tcprtt(连接 RTT 分布)、tcptop(连接级流量 top)、tcplife 与 tcpstates(状态迁移)、udplife;追踪辅助:funclatency(任意函数的延迟直方图)、funccount(函数调用计数)、trace(对任意函数打印参数与返回值,条件过滤)、stackcount(栈聚合计数)、offcputime、wakeuptime。每个工具都有 --help 与 -i 间隔输出模式,man 页附内核版本要求。
bpftrace 语法速成与实战模式。结构:probe:filter { action }。探针类型:kprobe:func / kretprobe:func、uprobe:bin:func、tracepoint:subsys:event、profile:hz:99(定时采样)、interval:s:1(定时器)、software/hardware perf 事件。内建变量:pid、tid、comm、cpu、nsecs、kstack/ustack(内核/用户栈)、args(探针参数)、retval(kretprobe 返回值)。聚合:@map[key] = count()/sum()/avg()/hist()/lhist()/stats(),hist 是 2 的幂对数直方图,lhist 可自定义桶。实战模式举例——统计 read 返回大小分布:tracepoint:syscalls:sys_exit_read { @bytes = hist(args->ret); };找慢系统调用:tracepoint:syscalls:sys_enter_ { @start[tid] = nsecs; } tracepoint:syscalls:sys_exit_ /@start[tid]/ { $d = nsecs - @start[tid]; if ($d > 10000000) { printf("%s %s %dms
", comm, probe, $d/1000000); } delete(@start[tid]); };VFS 读延迟按进程:kprobe:vfs_read { @s[tid]=nsecs; } kretprobe:vfs_read /@s[tid]/ { @ns[comm]=hist(nsecs-@s[tid]); delete(@s[tid]); }。bpftrace 的 -e 单行与脚本文件两种形态,配合 map 清零(clear(@))可做周期报表。
工具开销的量化数据与生产守则。实测参考值(Gregg 书中与社区基准):perf record -F 99 全系统约 1-2% CPU;BCC biosnoop 在每秒 1 万 I/O 下约 1-3%;bpftrace 追踪每秒百万级事件(如所有 read)约 10-30%;strace 对系统调用密集程序减速 10-100 倍;tcpdump 在 10 万 pps 下丢包且占一个核。守则:(1)先估算事件频率(cat /proc/net/snmp 差分、iostat tps)再选工具;(2)高频事件用内核态聚合(hist/count)而非逐事件输出(printf);(3)限时:timeout 30 bpftrace ...;限域:-p PID、按 comm 过滤;(4)上线前在预发环境做 A/B 开销测量;(5)常驻观测(如 always-on profiling)用低频采样 + 环形缓冲,Netflix 的实践是每核 99Hz 采样、开销 <1%、数据保留数天,故障时"回放"当时的火焰图——这是"事前部署观测、事后分析"范式的典范。
符号解析:火焰图可读性的生死线。采样得到的栈是地址序列,需要符号表翻译:内核符号在 /proc/kallsyms(kptr_restrict 设置影响可见性,生产常需调 0);用户符号依赖二进制的符号表(strip 过的二进制显示十六进制地址,需 debuginfo 包或 -g 编译);JIT 语言(Java、Node、.NET)的代码在运行时生成,磁盘上没有符号——解法:Java 用 perf-map-agent(生成 /tmp/perf-PID.map)或 async-profiler(直接理解 JVM 栈);Node 用 --perf-basic-prof;Python 的 C 层栈看不到 Python 函数,需 py-spy 或 bpftrace 的 Python USDT。栈回溯方式也影响质量:fp(帧指针)快但要求编译保留帧指针(Ubuntu 默认 -fomit-frame-pointer 导致断栈);dwarf(用调试信息展开)准确但每样本拷贝 8KB 栈数据、开销大;LBR(Intel 硬件栈记录)只支持 32 层但零拷贝。看到"破碎"的火焰图(大量单帧或 [unknown]),先查符号与栈回溯方式。
观测数据的三种消费模式与工具选型。模式一:即席诊断(interactive)——工程师登录机器实时排障,用 top/vmstat/iostat + bpftrace/BCC 单行命令,要求工具即开即用、输出人类可读;模式二:常驻监控(monitoring)——计数器周期采集进时序库(Prometheus node_exporter 采集 /proc),配告警与大盘,要求低开销、稳定、可聚合,采样剖析可以低频常驻(如每 10 分钟采 30 秒);模式三:事后取证(post-mortem)——故障已过去,靠历史数据回放,要求"黑匣子"式记录(sar 的 /var/log/sa、always-on profiling、环形缓冲的追踪数据)。成熟组织的配置:模式二全覆盖(所有机器计数器进监控)、模式三关键服务(always-on CPU/off-CPU 剖析)、模式一靠工程师的工具素养。三种模式对应本书工具的不同用法:同一份 perf record 数据,即席看是排障,常驻采是黑匣子。
网络与存储观测的专用工具深潜。存储侧:blktrace/blkparse 是块层全事件追踪(重量级),biosnoop 是其 eBPF 轻量替代——每个 I/O 一行:时间、进程、comm、磁盘、扇区、大小、延迟,配合 -Q 参数还能拆分"队列时间 vs 设备时间",判断延迟在排队还是在设备;filetop 按文件聚合 I/O 量,回答"哪个文件被读爆了"(常见发现:某日志文件被反复全量读取)。网络侧:ss -tin 显示每条连接的 RTT、cwnd、重传、拥塞算法——单连接诊断的第一工具;tcpretrans 实时打印每个重传包(时间、PID、连接四元组、序列号、状态),区分重传与尾丢包探测(TLP);tcprtt 按连接聚合 RTT 直方图,回答"延迟是网络还是主机";execsnoop 追踪所有新进程执行(安全与性能两用,发现意外的 cron/脚本);opensnoop 追踪所有文件打开(发现配置探测风暴、日志句柄泄漏)。这些工具的共同点:把内核内部事件(重传、I/O 完成、进程创建)以进程/连接/文件粒度归因输出,填补了计数器"有总量无归因"的空白。
构建个人工具地图的方法。Gregg 建议用"观测目标 × 观测方法"矩阵组织知识:行是资源(CPU、内存、磁盘、网络、应用)与维度(利用率、饱和度、延迟、事件),列是方法(计数器、剖析、追踪、可视化),每个格子填入具体工具。例如"磁盘延迟 × 追踪"格子里是 biosnoop/biolatency,"CPU 热点 × 剖析 × 可视化"格子里是 perf record + 火焰图。学习路径:先精通每格子的一个工具(约 20 个覆盖 80% 场景),遇到工具不能回答的问题再扩展。刻意练习法:给自己出题(用 stress-ng/tc/fault injection 制造问题)然后用工具矩阵盲测诊断(第 2 章实验的模式)。工具会过时(SystemTap → eBPF),矩阵思维不会——新工具出现时,你只需问"它填补了矩阵的哪个格子"。这张个人地图加上第 2 章的 USE 清单,构成完整的排障操作系统。
代码示例
perf record -F 99 -a -g -- sleep 30
perf script > out.perf
git clone https://github.com/brendangregg/FlameGraph
./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
./FlameGraph/flamegraph.pl out.folded > cpu.svg
/* 差分火焰图:
./FlameGraph/difffolded.pl base.folded new.folded | ./FlameGraph/flamegraph.pl > diff.svg */这是 CPU 火焰图的标准生成管线,五步各自的意义:perf record -F 99 -a -g 以 99Hz 全系统采样调用栈 30 秒(99 避免与整秒定时任务共振;-a 覆盖所有进程包括内核线程;生产环境可加 -p 限定目标进程降低开销与数据量)。perf script 把二进制 perf.data 展开为文本栈格式(每样本一段:进程名 PID、时间戳、逐帧"符号+地址")。stackcollapse-perf.pl 把多行栈折叠为单行分号分隔格式并计数("swapper;start_secondary;cpu_idle 12345"),这一步同时完成符号清洗。flamegraph.pl 渲染交互式 SVG:鼠标悬停显示帧名与占比、点击缩放、Ctrl+F 搜索——SVG 必须用浏览器打开才有交互。差分火焰图用 difffolded.pl 合并两份 folded 文件(要求同一负载的两次采集),输出中红色=第二次占比增加、蓝色=减少,是验证优化效果与对比故障/健康时段的标准手段。常见故障排查:火焰图全是 [unknown] → 符号缺失(装 debuginfo 或检查 kptr_restrict);栈只有一两层 → 帧指针缺失(改 --call-graph dwarf,16384 或重编译加 -fno-omit-frame-pointer);Java 栈是十六进制 → 需要 perf-map-agent 或改用 async-profiler。/usr/share/bcc/tools/biolatency 1 10
/* 每秒输出一次块 I/O 延迟直方图,共 10 次 */
/usr/share/bcc/tools/biosnoop
/* 逐 I/O 明细:TIME PID COMM D DISK T SECTOR BYTES LAT(ms) */
/usr/share/bcc/tools/bitesize 10
/* I/O 大小分布直方图 */存储延迟诊断的三件套,全部基于 block tracepoint 的 eBPF 实现,开销远低于 blktrace。biolatency 把每个块 I/O 从发出到完成的延迟放进 2 的幂对数直方图(微秒桶),1 秒一刷:健康 SSD 的分布应集中在 64-512μs 桶,若出现 8ms、16ms 的长尾桶,说明有慢 I/O(设备饱和、RAID 重建、云盘限流);HDD 正常在 4-16ms 桶。它的价值在于"分布视角"——iostat 的 await 是均值,会掩盖双峰分布(90% 快 + 10% 极慢),直方图一眼看穿。加 -Q 参数从 I/O 进入队列开始计时,对比默认(从下发设备计时)可拆分排队延迟与设备延迟:排队占大头 → 主机侧饱和或调度器问题;设备占大头 → 存储后端问题。biosnoop 逐 I/O 输出明细,含发起进程(COMM/PID)——这是 iostat 给不了的归因能力,"哪个进程在打爆磁盘"一查便知;D 列区分读(R)写(W)。bitesize 的 I/O 大小分布揭示负载模式:4KB 随机为主(数据库)、128KB+ 顺序为主(流式读写)、大量 <4KB 的碎片 I/O(应用缓冲策略问题)。三个工具组合完成存储 USE 的延迟与负载刻画维度。/usr/share/bcc/tools/tcpretrans
/* TIME PID IP LADDR:LPORT T> RADDR:RPORT STATE */
/usr/share/bcc/tools/tcplife
/* PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS */
ss -tin dst 10.0.0.5
/* 单连接详情:rtt、cwnd、retrans、拥塞算法 */
/usr/share/bcc/tools/tcprtt -i 1
/* 按 RTT 分桶的直方图,每秒刷新 */网络性能诊断的组合拳。tcpretrans 实时打印每个 TCP 重传事件:T> 标记表示这是尾丢包探测(Tail Loss Probe,不是真重传);输出含进程 PID 与四元组,直接归因"谁在重传、和谁"。判读:偶发重传(每分钟个位数)是公网常态;持续重传且集中在某对端 → 该路径丢包;重传伴随 tcplife 中连接寿命极短 → 可能是 SYN 重传(对端 backlog 满)。tcplife 追踪每条 TCP 连接从建立到关闭的完整生命周期:存活时长(MS)、收发字节(TX_KB/RX_KB)——发现短连接风暴(大量 MS<100 的连接说明没有连接复用)、大流量连接归因,比抓包轻量几个数量级(eBPF 只记录事件元数据不碰包内容)。ss -tin 是单连接显微镜:rtt:x/y 是当前 RTT 与方差(毫秒),cwnd 是拥塞窗口(MSS 倍数),retrans:a/b 是当前/累计重传,配合 dst 过滤特定对端;连续观察 cwnd 是否被反复砍半可以确认拥塞事件。tcprtt 聚合所有连接的 RTT 分布,回答"延迟在不在网络路径上":RTT 直方图集中在 0-1ms(同机房)而应用延迟 100ms → 瓶颈不在网络传输。四工具覆盖网络问题的发现(tcprtt)→ 归因(tcpretrans/tcplife)→ 深挖(ss -tin)全链路。/usr/share/bcc/tools/profile -F 99 -p $(pgrep -f myapp) 30 -f > app.folded
/usr/share/bcc/tools/offcputime -p $(pgrep -f myapp) 30 -f > off.folded
/* 分别渲染火焰图 */
./FlameGraph/flamegraph.pl --title "On-CPU" app.folded > on.svg
./FlameGraph/flamegraph.pl --title "Off-CPU" --countname us off.folded > off.svgon-CPU 与 off-CPU 双火焰图是延迟分解的完整方案。profile 工具是 perf record 的 eBPF 等价物(基于 perf_event 采样),-f 直接输出 folded 格式省去 perf script 与折叠两步,对 JIT 语言支持更好(它读取 /tmp/perf-PID.map)。offcputime 追踪 sched_switch 事件:任务被换下时记录栈与时间戳,换回时计算 off-CPU 时长并按栈聚合,-f 输出 folded 格式(计数单位是微秒)。两张图合起来回答"请求的时间去哪了":on-CPU 图宽 → 计算密集,找最宽平顶优化算法/热点;off-CPU 图宽 → 等待密集,看栈顶内核函数分类:futex_wait(锁)、io_schedule(磁盘 I/O)、sk_wait_data(网络收等待)、ep_poll(事件循环空等,正常)、rq_qos_throttle(cgroup 限流)。实战判读案例:某服务 P99 高但 CPU 只有 30%——off-CPU 图显示 60% 的等待时间栈顶是 io_schedule 且用户栈指向日志写入函数,结论:同步写日志在磁盘抖动时拖慢请求,改异步批量写解决。注意 offcputime 对高频切换的程序开销较大(每次切换一个事件),生产使用加 -p 限定并控制在 30 秒内。bpftrace -e 'tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_read /@start[tid]/ {
$lat = nsecs - @start[tid];
if ($lat > 10000000) {
printf("%s pid=%d read blocked %d ms
", strftime("%H:%M:%S", nsecs), pid, $lat / 1000000);
}
delete(@start[tid]);
}'
/* 捕获所有耗时超过 10ms 的 read 系统调用 */这条 bpftrace 脚本是"慢事件猎手"模式的范本,展示 eBPF 即席追踪的典型结构。工作原理:sys_enter_read 追踪点在 read 开始时把当前纳秒时间戳存入以 tid 为键的 map;sys_exit_read 在返回时计算差值,超过阈值(10ms)才输出——过滤谓词 /@start[tid]/ 确保只处理有配对 enter 的 exit,delete 防止 map 泄漏。输出含时间戳(strftime 格式化 nsecs)、进程与阻塞时长。这个模式可以套用到任何成对的 enter/exit 追踪点:sys_enter_write(慢写)、sys_enter_fsync(慢刷盘——数据库延迟抖动排查利器)、sys_enter_connect(慢建连)、sys_enter_openat(慢打开——NFS 卡顿排查)。方法论价值:它是"守株待兔"式观测——偶发问题(每小时一次的 500ms 毛刺)无法靠人工盯屏捕获,挂一个低开销的条件过滤器等待事件自己上门。开销分析:read 是高频事件,但脚本只做两次 map 操作与一次比较(内核态完成,无输出拷贝),每秒 10 万次 read 下开销约 1-2%,可接受;若把阈值去掉逐事件 printf,开销会爆炸——这就是"内核态聚合/过滤"原则的体现。/usr/share/bcc/tools/memleak -p $(pgrep -f myapp) 10 5
/* 每 10 秒采样一次,共 5 次,显示未释放分配的栈聚合 */
/usr/share/bcc/tools/funclatency -p PID -d 30 /lib/x86_64-linux-gnu/libc.so.6:malloc
/* 测量 malloc 延迟分布 */
/usr/share/bcc/tools/stackcount -p PID "c:pthread_mutex_lock"
/* 统计锁获取的调用栈 */内存与锁的专项追踪工具。memleak 用 uprobe 挂 malloc/free(或内核 kmalloc/kfree),记录每笔分配的栈与大小,周期性扫描"已分配未释放"的集合并按栈聚合输出——比 valgrind 快得多(valgrind 减速 20-50 倍只能开发环境用,memleak 开销约 5-10% 可短时生产使用);判读要点:连续多次采样中同一栈的未释放量单调增长才是真泄漏(一次性增长可能是缓存预热);-a 参数按分配大小聚合,-T 显示时间戳观察增长趋势。funclatency 对任意函数(用户态或内核态)生成延迟直方图,示例测 malloc 延迟:健康时集中在 100ns 桶,若出现 μs-ms 长尾说明分配器在向内核要页(brk/mmap)或 arena 锁争用——这是"内存分配成为瓶颈"的直接证据。stackcount 聚合"到达某函数的所有调用栈"及次数,示例统计谁在调 pthread_mutex_lock:如果某个栈占比异常高,就是锁使用最频繁的代码路径,结合 offcputime 的 futex 等待可完成"锁争用"的完整证据链(谁在抢锁 + 等了多久)。三个工具都支持 -i 周期输出与 -p 进程限定,生产使用遵循限时限域原则。实验笔记
实验四:构建你的可观测性工作台——从计数器到火焰图的完整实操。
实验目标:(1)亲手生成并判读 CPU、off-CPU、差分三种火焰图;(2)掌握 BCC/bpftrace 在存储、网络、内存三个子系统的核心工具;(3)实测工具开销,建立"观测成本"的量化直觉。
环境准备:Linux 虚拟机(内核 5.x+,Ubuntu 22.04 最省事),4 核 8GB 为佳。安装:sudo apt install bpfcc-tools linux-tools-common linux-tools-generic bpftrace perf-tools-unstable sysstat;git clone FlameGraph 仓库。设置 sysctl kernel.perf_event_paranoid=-1 与 kernel.kptr_restrict=0 以便非 root 使用(实验环境)。准备负载生成器:sysbench(CPU/内存/线程)、fio(磁盘)、iperf3(网络)、stress-ng。
步骤一(CPU 火焰图与符号验证):运行 sysbench cpu --threads=4 run 制造纯计算负载,perf record -F 99 -a -g sleep 30 采集,走完整管线生成 cpu.svg。验证:火焰图应显示 sysbench 的素数计算函数占据主要宽度。然后故意制造"破碎火焰图":找一个小 C 程序用 -O2 默认编译(省略帧指针),采集观察断栈,再用 --call-graph dwarf 重采对比,理解栈回溯方式的影响。
步骤二(off-CPU 与差分火焰图):运行第 3 章实验的锁争用脚本 slow_app.py,采集 offcputime -f 30 秒生成 off-CPU 火焰图,确认 futex 等待栈占主导。接着做差分实验:对 sysbench threads 负载先采集 30 秒基线(base.folded),把线程数翻倍再采集(new.folded),用 difffolded.pl + flamegraph.pl 生成差分图,观察调度器路径(schedule、__switch_to)变红变宽——这是"并发增加导致调度开销上升"的可视化证据。
步骤三(存储工具链):用 fio 制造两种负载:(a)4K 随机读高队列深度(fio --name=rand --rw=randread --bs=4k --iodepth=32 --filename=/tmp/fio.test --size=1G --direct=1);(b)1M 顺序写。分别运行 biolatency 1、biosnoop、bitesize 10,记录两种负载下延迟直方图形态(随机读宽分布 vs 顺序写窄分布)、I/O 大小分布的差异。进阶:把 iodepth 提到 128,观察 biolatency -Q 的排队延迟桶右移——直观理解队列深度与延迟的排队论关系。
步骤四(网络工具链):iperf3 客户端-服务端打流(虚拟机对宿主机或两台 VM),期间运行 tcplife 观察连接生命周期与流量、tcprtt 看 RTT 分布。然后用 tc netem 注入 1% 丢包(tc qdisc add dev eth0 root netem loss 1%),运行 tcpretrans 捕获重传事件,同时观察 iperf3 吞吐下降与 ss -tin 中 cwnd 的锯齿变化。做完删除 qdisc(tc qdisc del dev eth0 root)。这个实验把"丢包 → 重传 → 拥塞窗口收缩 → 吞吐下降"的因果链完整可视化。
步骤五(工具开销实测):设计 A/B 基准——固定负载(sysbench cpu 或 fio),分别在"无工具""perf record -F 99""offcputime 运行中""bpftrace 追踪所有 read"四种条件下测量负载的吞吐(sysbench events/s 或 fio IOPS),计算每种工具的开销百分比。预期:采样类 1-5%,高频事件追踪可达 10%+。把结果做成表格,这就是你的"生产环境工具准入依据"。
预期结果:一个个人工具仓库(脚本 + 生成的 SVG + 开销测量表),以及能独立回答:给定"磁盘慢/网络抖/内存涨/CPU 高"四类报障,分别用哪三个工具在 5 分钟内给出证据。
进阶挑战:(1)用 bpftrace 实现一个自定义工具:统计每个进程的 fsync 延迟直方图(tracepoint:syscalls:sys_enter_fsync / sys_exit_fsync),对数据库负载运行并解读;(2)给 Java 或 Python 程序生成带完整应用符号的火焰图(perf-map-agent 或 py-spy record --format flamegraph 对比);(3)用 stackcount 找出系统中唤醒最频繁的路径,评估是否存在"唤醒风暴";(4)把 profile 工具配置成 cron 每 10 分钟采集 30 秒并归档 folded 文件,实现简易版 always-on profiling,一周后用差分火焰图对比工作日与周末的负载特征。
延伸阅读
- Brendan Gregg《Systems Performance》第 2 版第 4 章 Observability Tools 与第 5 章 Applications 的工具部分。原书第 4 章系统论述观测方法论(计数器/剖析/追踪的分类学)、工具开销测量方法与可视化理论,是本章的理论母本。特别推荐"Observability Tools"一节中关于静态/动态追踪、事件追踪与基于时间的追踪的辨析。
- Brendan Gregg《BPF Performance Tools》(Addison-Wesley, 2019)。eBPF 观测工具的百科全书:800 页覆盖 BCC 全部工具的原理、参数与案例,附大量 bpftrace 脚本。第 1-3 章讲 eBPF 机制与 BCC/bpftrace 使用,后续按子系统(CPU/内存/磁盘/网络/应用)组织。如果说《Systems Performance》是方法论圣经,这本就是 eBPF 工具圣经,两书配合使用效果最佳。
- 火焰图官方网站 https://www.brendangregg.com/flamegraphs.html 。包含火焰图的发明背景、全部变体(CPU/off-CPU/内存/差分/唤醒/热度图)的示例与生成方法、常见渲染问题(颜色含义、搜索语法)的说明。每个变体配真实案例截图,是学习判读的最佳图册。GitHub 的 FlameGraph 仓库 README 也有完整用法。
- bpftrace 官方参考 https://github.com/bpftrace/bpftrace/blob/master/man/adoc/bpftrace.adoc (或 bpftrace --info 与仓库中的 reference guide)。bpftrace 语言的权威文档:全部探针类型、内建变量、聚合函数、map 操作与语法细节。配合仓库中的 tools/ 目录(100+ 现成单行脚本,按场景分类)与 btexamples,是从"会抄脚本"到"会写脚本"的必经之路。
- BCC 工具集文档 https://github.com/iovisor/bcc 。每个工具在 docs/ 下有带示例输出的说明文档(如 biosnoop_example.txt),演示真实场景的判读过程——这些 example 文档本身就是微型案例研究。libbpf-tools 子目录是 CO-RE 重写版,单二进制部署,适合生产。iovisor 组织还有 bcc 的 Python/Lua API 文档,供开发自定义工具。
- perf 官方文档与 Brendan Gregg 的 perf 速查页 https://www.brendangregg.com/perf.html 。perf 是功能最全也最复杂的工具,Gregg 的 perf Examples 页面按场景(CPU 剖析、缓存分析、调度、追踪)给出可直接复制的命令与输出判读,比 man page 友好得多。配合 perf wiki(perf.wiki.kernel.org)的架构文档深入理解 perf_events 子系统。
- Netflix Tech Blog 的 always-on profiling 系列文章,特别是《Java Flame Graphs》与《Netflix Instance Analysis》。Netflix 在十万台服务器上常驻 eBPF 剖析的工程实践:如何控制开销在 1% 以内、如何解决 JIT 符号、如何把火焰图数据做成可检索的时序库。这是"观测平台化"的最佳工业案例,展示了本章工具在极限规模下的形态。
- 《使用 eBPF 观测 Linux 内核》相关的 LWN 文章系列(lwn.net 搜索 BPF)与 Kernel Newbies 的 eBPF 页面。LWN 对验证器演进、CO-RE/BTF、BPF LSM 等关键特性有深度报道,跟踪内核每个版本的 eBPF 新能力(如有界循环、全局变量、ring buffer)。对想在观测之外深入 eBPF 网络/安全用途的读者,这是保持知识更新的渠道。