第4章 BPF 可观测性工具
BCC 与 bpftrace 全景
章节导读
前面三章铺好了地基,从本章开始我们进入本书的主体:工具。Brendan Gregg 在这本书里介绍了超过一百个 BPF 性能工具,本章是全景导览——工具的两大来源(BCC 工具集与 bpftrace 脚本)、按问题域的组织方式、几个最核心工具的深入剖析、以及如何解读工具输出并控制生产环境开销。后续第 5 到第 13 章将按子系统(应用、CPU、内存、磁盘、网络、容器)逐一展开,本章建立的工具使用心法贯穿全书。
先讲工具的组织哲学。Gregg 反对"按工具名记忆"的学习方式——一百多个工具名谁也记不住,记住的也会忘。他主张按"你要回答的问题"来组织:想知道谁在执行进程?execsnoop。想知道文件打开失败?opensnoop。想知道磁盘 I/O 延迟分布?biolatency。想知道 CPU 调度延迟?runqlat。想知道谁在占用 CPU、热点函数是什么?profile。想知道线程为什么阻塞?offcputime。想知道 TCP 连接的生命周期?tcplife。这套"问题→工具"的映射,加上 USE 方法论(对每种资源查利用率、饱和度、错误),构成了完整的排查地图。书中附录有一张大表:按资源(CPU、内存、磁盘、网络)× 指标类型(利用率/饱和度/错误/延迟)组织全部工具,值得反复翻阅直到内化。
BCC 工具集是 IO Visor 社区维护的"标准库",一百多个工具全部遵循统一的设计范式:Python 前端负责参数解析和输出格式化,内嵌的 C 程序负责内核态的过滤与聚合,perf buffer 或 ring buffer 负责事件传输。工具命名有清晰的规律:snoop 系列追踪离散事件(execsnoop、opensnoop、tcplife 类),latency/lat 系列输出延迟直方图(biolatency、runqlat、syscount 类),top 系列输出周期性排行榜(biotop、tcptop、memleak 类),profile/offcputime 是剖析器。掌握命名规律后,遇到新问题你可以合理猜测"应该存在一个什么名字的工具",然后去 BCC 仓库验证——大多数时候它真的存在。
bpftrace 则是另一条路线:不安装工具集,直接用高级语言现场写脚本。它的语法直接继承自 DTrace——探针描述符(kprobe:vfs_read)、谓词过滤(/pid == 1234/)、动作块({ @ = count(); })、内置聚合函数(count、sum、avg、hist、lhist、stats、min、max、quantize)。一个 DTrace 老手可以在一小时内上手 bpftrace;一个新手可以在一天内写出解决真实问题的脚本。bpftrace 的定位是"交互式诊断的瑞士军刀":BCC 工具覆盖不了的长尾问题,用 bpftrace 现场写十行脚本解决。书中给出了数十个 bpftrace 单行脚本(one-liners),本章精选最典型的几个精讲。
工具输出解读是本章的另一个重点。以延迟直方图为例:横轴是 2 的幂次分桶的延迟区间,纵轴是事件数。解读要点:(1) 看形状——单峰且紧凑说明系统行为一致;双峰说明存在两种路径(如页缓存命中 vs 磁盘读取);长尾(右侧稀疏但存在的桶)说明有偶发慢操作,往往是问题的根源。(2) 看绝对值——磁盘 I/O 的主峰应该在几十微秒(SSD)或几毫秒(HDD);如果主峰在几百毫秒,说明存储栈有严重问题。(3) 看变化——调优前后、故障发生前后的直方图对比是最有说服力的证据。Gregg 有句名言:平均值是给管理者看的,直方图才是给工程师看的。
生产环境使用 BPF 工具的注意事项贯穿全书,这里先立规矩:(1) 优先选择内核态聚合的工具(所有 BCC 工具都是),避免逐事件输出的脚本跑在高频事件上;(2) 注意工具自身的开销——profile 99Hz 采样开销 <1%,但 execsnoop 在进程创建风暴(每秒数千 fork)下会有可观输出量;(3) 长时间运行的监控要注意 map 增长——以 PID 为键的 map 在进程频繁创建销毁时可能膨胀(现代工具用 LRU map 缓解);(4) 内核版本兼容——BCC 工具随内核演进不断修复兼容性问题,保持工具和内核的版本匹配;(5) 权限——BPF 需要 root 或 CAP_BPF+CAP_PERFMON(5.8+),云环境的容器默认没有这些权限,需要特权配置。
本章的最后部分讨论一个进阶话题:如何从工具输出定位到根因。工具给你数据,但数据到结论之间需要推理链。例如 runqlat 显示调度延迟 P99 达到 50ms——这只是症状,下一步要问:是所有 CPU 都饱和(runqlen 看队列长度)还是个别 CPU(-P 按 CPU 分解)?是被谁抢占(cpudist 看运行时长分布)?有没有 CPU 被中断/软中断占用(hardirqs/softirqs)?是不是 cgroup CPU 配额限流(第 10 章)?每个工具的输出都是下一个问题的起点,这种"工具链推理"能力才是 BPF 专家的真正标志。
核心概念
- BCC 工具集
- bpftrace 语法
- execsnoop
- biolatency
- runqlat
- profile
- 延迟直方图
要点精析
【BCC 工具集全景:按问题域分类】BCC 的一百多个工具可以按问题域分为六类。进程与执行:execsnoop(新进程执行)、exitsnoop(进程退出及退出码/信号)、oomkill(OOM 事件)、shmsnoop(共享内存操作)。文件与 I/O:opensnoop(文件打开)、fileslower(慢文件操作)、biosnoop(逐 I/O 明细)、biolatency(I/O 延迟直方图)、biotop(I/O 排行榜)、cachestat(页缓存命中率)、filetop、xfsslower/ext4slower/btrfsslower(文件系统慢操作)。CPU 与调度:profile(CPU 剖析)、runqlat(调度延迟)、runqlen(运行队列长度)、cpudist(进程单次运行时长)、offcputime(阻塞分析)、wakeuptime(谁唤醒了谁)、hardirqs/softirqs(中断耗时)。内存:memleak(泄漏追踪)、stackcount(按栈计数任意函数)、pageunclaimed(未回收页)。网络:tcplife(TCP 连接生命周期)、tcpconnect/tcpaccept(主动/被动连接)、tcpretrans(重传)、tcptop(连接流量排行)、tcplife 的字节统计、udplife、dns 相关工具、xdp 系列。语言运行时:javacalls、javathreads、pythonstat、rubystat、nodejsstat 等基于 USDT 的工具。每类工具的详细用法在对应章节展开,本章要求建立"索引级"熟悉度:遇到问题时能在 30 秒内想起候选工具,然后 man 一下确认参数。
【bpftrace 语法速成:探针、谓词、动作】bpftrace 程序的完整语法:probe:target /predicate/ { action }。探针类型:kprobe/kretprobe(内核函数入口/返回)、uprobe/uretprobe(用户态函数)、tracepoint:子系统:事件、usdt:路径:提供者:探针、profile:hz:99(定时采样所有 CPU)、interval:s:1(定时器,常用于打印 map)、BEGIN/END(脚本起止)、software/hardware(perf 软件/硬件事件)。谓词是过滤条件:/pid == 1234/、/comm == "nginx"/、/arg2 > 1024/,在内核态求值,不满足则跳过动作——这是控制开销的第一道闸门。动作块中:内置变量 pid、tid、comm、cpu、nsecs、uid、gid、kstack、ustack、arg0-argN、retval、curtask;map 操作 @name[key] = value、@ = count()、delete()、clear();聚合函数 count()、sum()、avg()、min()、max()、stats()(一次给出 count/avg/total)、hist()(log2 直方图)、lhist(x, min, max, step)(线性直方图);输出 printf()、print(@map);流程控制 if/else、while(受限)、unroll。多探针脚本用逗号或分号分隔,可写入 .bt 文件执行。这套语法的表达力覆盖 90% 的日常诊断需求,剩下 10%(复杂数据结构、跨事件状态机)才需要 BCC/libbpf。
【核心工具精讲:execsnoop——看见短生命周期进程】execsnoop 追踪 execve 系统调用,输出每个新进程的执行事件:时间、PID、PPID、参数(-t 时间戳、-x 显示失败、-U 只显示 UID 变化、--name 过滤)。它解决的核心问题是"top 看不见的进程":存活时间小于采样间隔的进程在 ps/top 中完全隐形,而 cron 任务、监控脚本、构建系统、恶意软件往往正是这类进程。真实案例:某服务器 CPU 周期性尖峰,execsnoop 揭示是遗留 agent 每 10 秒 fork 一批短命进程;另一起安全事件中,execsnoop 的持续记录成为入侵者执行命令的完整审计日志。实现原理:挂载 tracepoint:syscalls:sys_enter_execve(读取参数)和 sys_exit_execve(判断成败),把事件通过 perf buffer 发到用户态。开销:每个 execve 一次事件输出,正常系统每秒几次到几十次,可忽略;fork 炸弹场景下输出量大,用 -t 加时间戳观察速率。execsnoop 还是"进程谱系分析"的起点:配合 exitsnoop 和 PPID 字段可以重建完整的进程树生命周期。
【核心工具精讲:opensnoop——文件访问审计与排错】opensnoop 追踪 open/openat 系统调用,输出 PID、COMM、FD、ERR、PATH。两个杀手级用法:(1) 排错——FD 为 -1 时 ERR 列显示 errno(2=ENOENT 文件不存在,13=EACCES 权限拒绝),应用日志里"配置文件加载失败"的模糊报错,opensnoop 直接告诉你在找哪个路径、为什么失败;(2) 审计——谁在读敏感文件?-n 按进程名过滤、-p 按 PID 过滤、-e 只显示失败、-T 显示时间戳、-x 只显示失败。实现:sys_enter_openat 抓路径参数(用户态字符串需 bpf_probe_read_user_str),sys_exit_openat 抓返回值,用 map 在 enter/exit 间关联。注意事项:路径是打开时刻的字符串,符号链接解析后的真实路径看不到;容器内进程的打开事件同样可见(BPF 在宿主机内核层追踪,天然跨命名空间);高频文件访问的服务(如日志密集写入)会产生大量输出,务必用过滤参数。opensnoop 与 fatrace(基于 fanotify)的对比:fanotify 只覆盖文件访问通知且需要挂载点配置,opensnoop 覆盖所有 open 且零配置。
【核心工具精讲:biosnoop——逐 I/O 明细的显微镜】biosnoop 输出每一个块设备 I/O 的明细:时间戳、进程(COMM/PID,注意:异步 I/O 的完成上下文可能显示为内核线程)、磁盘、类型(R/W)、扇区、大小、延迟(Q2C 总时间)、Q2D(队列时间)、D2C(设备时间)。与 biolatency(聚合直方图)互补:biolatency 告诉你"延迟分布异常",biosnoop 告诉你"具体哪些 I/O 异常、多大、什么类型、谁发的"。典型排查流程:biolatency 发现长尾 → biosnoop -Q 观察长尾 I/O 的特征(是不是都是大 I/O?集中在某个设备?Q2D 大说明内核队列拥堵,D2C 大说明设备慢)→ 结合 iostat 确认设备级利用率。实现原理:挂载 block 子系统的 tracepoint(block_rq_insert/block_rq_issue/block_rq_complete,不同内核版本挂载点不同),用 request 指针为键在 map 中记录各阶段时间戳。开销:每个 I/O 一条事件,高 IOPS 设备(NVMe 每秒数十万 I/O)下输出量巨大,生产环境应短时间运行或用 -m 毫秒过滤只看慢 I/O。书中强调:biosnoop 显示的 PID 是 I/O 完成时的上下文,回写 I/O(writeback)会归属到 kworker 线程而非真正的应用进程——这是块层异步性的体现,解读时的重要陷阱。
【核心工具精讲:runqlat 与调度延迟】runqlat 测量任务从"可运行"(被唤醒进入运行队列)到"真正获得 CPU"之间的延迟,输出直方图,支持 -P 按 PID、-L 按线程 ID、-p 过滤进程、--cgroup 按 cgroup 分解。调度延迟是 CPU 饱和度的最灵敏指标:CPU 利用率 100% 只说明忙,runqlat 的长尾才说明"忙到让任务等不起"。健康系统的调度延迟主峰应在微秒级;出现毫秒级长尾说明 CPU 供给不足或调度异常。排查链条:runqlat 发现延迟 → runqlen 看是哪些 CPU 的队列积压(-P 按 CPU 分解可发现中断亲和性不均导致的单核饱和)→ cpudist 看任务单次占用 CPU 的时长分布(时长极短说明切换频繁,极长说明有 CPU 霸占者)→ pidstat/进程分析找到霸占者。虚拟机场景的特殊性:runqlat 测的是 guest 内核的调度延迟,hypervisor 层的 CPU 争抢(steal time)表现为 guest 调度延迟异常但 guest 内看不到原因——需要结合 /proc/stat 的 steal 列和宿主机监控。容器场景:cgroup CPU 配额(quota/period)限流造成的延迟在 runqlat 中同样可见,第 10 章详述。
【延迟直方图工具家族:biolatency、syscount 与解读方法】biolatency 是块 I/O 延迟直方图,选项丰富:-Q 包含队列时间、-D 仅设备时间、-F 按 I/O 标志分解、-m 毫秒单位、-O 按磁盘分解。syscount 统计系统调用次数和耗时(-L 按延迟排序、-p 过滤、-i 间隔输出),是"这个进程到底在干什么"的第一工具——替代 strace -c 但开销低两个数量级。直方图解读方法论(适用于所有 lat/latency 工具):第一步看主峰位置——它代表系统的"正常延迟",SSD 随机读主峰应在 100μs 附近,HDD 在 10ms 附近,网络往返在同机房 100μs-1ms;主峰位置不对说明基础路径有问题。第二步看长尾——主峰右侧 3 个数量级外仍有计数的桶就是长尾,长尾占比哪怕 0.1% 对 P99.9 敏感的业务也是灾难。第三步看多峰——双峰通常对应两条路径(缓存命中/未命中、快盘/慢盘、本地/远程)。第四步做对比——变更前后、故障节点与正常节点、压力测试不同阶段。把直方图当"系统行为的指纹",指纹变化即线索。
【剖析工具:profile 与 offcputime 的双剑合璧】profile 按固定频率(默认 99Hz)在所有 CPU 上采样调用栈,聚合后输出——这就是 CPU 火焰图的数据来源。频率选择有讲究:99Hz 意味着每秒每 CPU 99 个样本,10 秒采集在 32 核机器上约 3 万个样本,足以定位主要热点,CPU 开销 <1%;频率提高到 999Hz 分辨率更高但开销线性增长,且 99 这种"非整"频率是故意的——避免与系统中其他周期性活动(100Hz 的定时器)产生锁频共振,采样偏差更小。offcputime 是 profile 的镜像:追踪 sched_switch,测量线程"不在 CPU 上"的时间及其阻塞栈——线程被换出时记录栈和时间戳,换入时计算差值,聚合出"阻塞在哪里、阻塞了多久"的直方图。两者合璧覆盖线程时间的全部:on-CPU 时间(profile)+ off-CPU 时间(offcputime)= 墙钟时间。应用慢但 CPU 利用率低?offcputime 直接告诉你线程在等锁、等 I/O 还是等网络。wakeuptime 是第三个视角:谁唤醒了阻塞的线程——追踪"唤醒者"的栈,用于分析阻塞链条的上游。
【工具输出解读实战:从数据到结论的推理链】举一个完整的推理案例:告警显示服务 P99 延迟从 20ms 恶化到 200ms。排查过程:(1) mpstat 看 CPU 利用率 60%,不高,但 %sys 从 5% 升到 25%——内核态时间异常;(2) syscount -L 看系统调用耗时排行,发现 futex 调用总耗时暴涨——大量锁等待;(3) profile 抓 on-CPU 火焰图,内核栈显示自旋锁竞争激烈;(4) offcputime 抓 off-CPU 火焰图,应用线程阻塞在 futex_wait,栈顶指向某个应用层互斥锁;(5) 用 bpftrace 追踪该锁的持有时间分布,发现持有者偶尔长达 100ms——持锁期间做了一次同步磁盘写;(6) biosnoop 确认该写入的延迟长尾与存储后端抖动吻合。结论:存储抖动 → 持锁写盘变慢 → 锁竞争放大 → P99 恶化。整个链条用了六个工具,每一步的输出决定下一步查什么——这就是"工具链推理"。反模式是漫无目的地把所有工具跑一遍:数据淹没线索,且高频工具本身会扰动系统。正确姿势:假设驱动,每次运行工具前明确"我要验证什么"。
【工具开销与生产环境守则】BCC 工具的开销模型:内核态每事件成本(tracepoint 约 0.1-0.5μs,kprobe 约 1-2μs)+ 聚合成本(map 操作约 0.1μs)+ 输出成本(perf buffer 每事件约 0.5-1μs,仅事件型工具有)。据此分级:(A) 常态运行级——profile(99Hz)、runqlat、biolatency 等聚合型工具,开销 <1%,Netflix 等在全部服务器 7×24 运行;(B) 诊断短时运行级——execsnoop、opensnoop、biosnoop 等事件型工具,开销取决于事件频率,正常运行可忽略,风暴场景(fork 炸弹、百万 IOPS)下输出通道成为瓶颈,应限时运行并加过滤;(C) 谨慎使用级——stackcount 全量抓栈、对超高频函数(malloc、页错误处理)的 uprobe/kprobe 全量追踪,每事件成本数微秒,高频下开销可达百分之几十,必须配合过滤和采样。通用守则:先看事件频率(跑 1 秒 count() 估算)再决定策略;工具运行期间监控被测应用的关键指标,发现扰动立即停止;生产环境优先用 -i 间隔输出观察趋势而非一次性大量输出;容器环境注意 BPF 权限(CAP_BPF、CAP_PERFMON、/sys/kernel/debug 挂载)与内核版本。
【bpftrace 单行脚本库:现场诊断的弹药库】书中整理了大量 bpftrace one-liners,精选最常用的模式:(1) 按进程统计系统调用:tracepoint:raw_syscalls:sys_enter { @[comm] = count(); };(2) 读请求大小分布:tracepoint:block:block_rq_issue { @ = hist(args->bytes); };(3) vfs_read 耗时:kprobe:vfs_read 配对 kretprobe 的 hist 模式;(4) 调度延迟:tracepoint:sched:sched_wakeup 记时间、sched_switch 算差的直方图;(5) 按栈计数某函数调用:kprobe:tcp_retransmit_skb { @[kstack] = count(); }——重传的内核路径分析;(6) 用户态函数参数追踪:uprobe:/path/to/app:process_request { printf("%d\n", arg0); };(7) 每秒打印:interval:s:1 { print(@); clear(@); }。这些模式的组合可以回答绝大多数临时问题。编写心法:先 count() 确认事件频率和过滤条件有效性,再上 hist/printf 等重逻辑;谓词过滤放在最前面(内核态过滤零传输成本);用 clear() 配合 interval 做周期性报表。bpftrace 的 -d 选项可以输出生成的 BPF 字节码信息,--info 查看特性支持,都是调试利器。
【工具缺失时:从 BCC 源码到自定义脚本的进阶路径】即使 100+ 工具也覆盖不了所有问题,进阶路径分三级:第一级,改造现有工具——BCC 工具是 Python 脚本,直接复制修改(如给 opensnoop 加一个按文件扩展名过滤的参数),大多数定制需求在这级解决;第二级,bpftrace 组合脚本——用 .bt 文件写多探针脚本,实现工具没有的关联逻辑(如"追踪持有锁超过 10ms 的线程并打印其栈"),书中第 12 章有大量范例;第三级,BCC/libbpf 完整开发——需要复杂用户态逻辑(符号解析、火焰图折叠、多 map 关联)时写完整工具。判断标准:一次性问题用 bpftrace;重复使用、需要参数化和友好输出的用 BCC;产品化集成、追求启动速度和零依赖的用 libbpf+CO-RE。学习方法的建议:挑三个你最常用的工具(如 execsnoop、biolatency、profile)通读源码——每个工具三四百行,C 部分展示挂载点和聚合逻辑,Python 部分展示输出处理,读完三个你就掌握了 BCC 工具的全部套路。BCC 仓库的 examples/tracing 目录还有十几个教学级示例,从 hello world 到完整工具循序渐进。
代码示例
/* 示例1:execsnoop 与 exitsnoop 组合——进程生命周期审计 */
sudo /usr/share/bcc/tools/execsnoop -t -x
/* TIME PID PPID RET ARGS
10:23:01 4521 4520 0 /usr/bin/curl http://internal/api
10:23:02 4522 4520 -2 /opt/backup.sh
*/
sudo /usr/share/bcc/tools/exitsnoop -t
/* TIME PID PPID EXIT_CODE SIGNAL ARGS
10:23:05 4521 4520 0 curl
*/execsnoop 的 RET 列是 execve 的返回值:0 成功,负数是失败的 errno(-2 = ENOENT,脚本路径不存在)。上面的输出讲了一个完整的故事:某个父进程(PPID 4520)先成功执行了 curl,然后尝试运行 /opt/backup.sh 但文件不存在——备份脚本被误删或路径配置错误。exitsnoop 补上生命周期终点:EXIT_CODE 是退出码,SIGNAL 列非零表示进程被信号杀死(9=SIGKILL,常见于 OOM killer 或超时机制)。两个工具组合就是完整的进程审计系统:谁启动了什么、成功与否、活了多久、怎么死的。这在容器环境尤其有价值——容器内进程崩溃重启,日志可能丢失,但宿主机上的 exitsnoop 记录了一切。实践建议:把 execsnoop -t 的输出接入日志系统做长期留存(注意脱敏),安全团队会感谢你的——它是检测横向移动和持久化行为的低成本手段。/* 示例2:biolatency 的多维分解 */
sudo /usr/share/bcc/tools/biolatency -D 10 5 /* 仅设备时间,10秒×5次 */
sudo /usr/share/bcc/tools/biolatency -Q -O 10 1 /* 含队列时间,按磁盘分解 */
/* 输出示例(-O 按磁盘):
disk = sda
msecs : count distribution
0 -> 1 : 1204 |****************************************|
2 -> 3 : 45 |* |
disk = nvme0n1
usecs : count distribution
64 -> 127 : 8932 |****************************************|
*/biolatency 的选项组合是定位存储问题的利器。-D(device time)只测设备处理时间,-Q(queue time)加上内核排队时间,两者之差揭示拥堵位置:Q2C 大而 D2C 小 → 内核队列拥堵(I/O 调度器积压、设备队列深度不足);两者都大 → 设备本身慢。-O 按磁盘分解的输出一眼看出异构存储的性能差异:上例中 nvme0n1 的主峰在 64-127 微秒(典型 NVMe 随机读),sda 主峰在 0-1 毫秒(SATA SSD 或缓存命中的 HDD),sda 还有少量 2-3ms 的长尾。-F 按 I/O 标志分解(读/写/同步/元数据)能进一步区分:写延迟长尾可能是回写风暴,读长尾可能是缓存失效。-m/-u 切换毫秒/微秒显示单位。诊断心法:先跑默认模式看总体,发现异常后用 -Q/-D 分离队列与设备,再用 -O/-F 定位到具体磁盘和 I/O 类型,最后用 biosnoop 抓具体慢 I/O 的明细。这个"聚合→分解→明细"的三步法是所有延迟问题的通用排查模式。/* 示例3:bpftrace 现场脚本——追踪超过 10ms 的 vfs_fsync 调用 */
sudo bpftrace -e '
kprobe:vfs_fsync_range { @start[tid] = nsecs; }
kretprobe:vfs_fsync_range /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000000;
if ($dur > 10) {
printf("%s (pid %d) fsync took %d ms\n", comm, pid, $dur);
print(kstack);
}
delete(@start[tid]);
}'
/* 输出:
mysqld (pid 2233) fsync took 45 ms
vfs_fsync_range+0
ext4_sync_file+0x8a
...
*/这个脚本解决一个 BCC 工具没有直接覆盖的问题:"哪些 fsync 特别慢、慢的时候内核在干什么?"fsync 是数据库和日志系统性能的关键路径——它强制把脏页刷到持久存储,慢 fsync 直接拖垮事务提交延迟。脚本逻辑:入口记时间戳,出口算差值,超过 10ms 阈值才输出进程信息 + 内核栈。谓词 /@start[tid]/ 过滤孤儿返回,阈值判断在内核态完成——不满足条件的调用零输出成本,这就是"内核态过滤"的威力。输出的 kstack 显示慢 fsync 的内核路径(ext4_sync_file → 块层提交 → 等待设备),如果栈里出现 jbd2(ext4 日志线程)说明卡在日志提交,出现 blk_mq 说明卡在块层。扩展练习:把阈值做成命令行参数(bpftrace 支持位置参数 $1);把 printf 换成 @slow[comm] = hist($dur) 做聚合统计;同时追踪 fdatasync(kprobe 支持通配符 vfs_fsync* 一次挂两个)。这个十行脚本展示了 bpftrace 的核心价值:问题出现时现场写、立即跑、用完扔。/* 示例4:syscount——系统调用画像 */
sudo /usr/share/bcc/tools/syscount -p 2233 -i 5
/* 每5秒输出 PID 2233 的系统调用统计:
TIME(s) SYSCALL COUNT
5.0 futex 12043
5.0 epoll_wait 3021
5.0 read 2890
5.0 write 2755
*/
sudo /usr/share/bcc/tools/syscount -L -p 2233 -i 5 /* 按总耗时排序 */
/* TIME(s) SYSCALL COUNT TIME(us)
5.0 futex 12043 892341 <- 锁等待耗时巨大
*/syscount 是给进程做"系统调用画像"的工具,回答"这个进程到底在跟内核交互什么"。上例揭示了一个典型的锁竞争模式:futex 调用次数最多且总耗时占绝对大头(5 秒内 892ms 花在 futex 上)——futex 是用户态锁陷入内核的路径,高频 futex 意味着应用层互斥锁竞争激烈。对比一个健康的事件驱动服务:epoll_wait 占主导(等待事件)、read/write 次之、futex 很少。-L 按耗时排序是发现"时间去哪了"的关键选项;-i 间隔输出观察随时间的变化;-P 按进程分解全系统。实现原理:挂载 raw_syscalls 的 enter/exit tracepoint,内核态按 syscall 号聚合计数和耗时。开销:raw_syscalls tracepoint 每次系统调用触发两次(enter+exit),系统调用密集的应用(每秒数十万次)下开销约 1-3%,可接受但要注意。syscount 与 strace -c 的输出等价,但 strace 会让应用慢数倍而 syscount 几乎无感——这是"同样的信息、不同的架构"的最好例证。/* 示例5:profile 生成火焰图数据 */
sudo /usr/share/bcc/tools/profile -F 99 -p 2233 -f 30 > out.folded 30秒
/* -f 输出折叠栈格式,用 FlameGraph 工具渲染: */
git clone https://github.com/brendangregg/FlameGraph
./FlameGraph/flamegraph.pl out.folded > cpu.svg
/* profile 直接输出示例(非折叠模式):
采样命中排行:
12043 mysqld`row_search_mvcc
8921 mysqld`buf_page_get_gen
3011 [unknown] (缺失符号)
*/profile 是 CPU 热点分析的黄金工具:-F 99 设定采样频率,-p 限定进程,-f 输出折叠栈(每行一个唯一栈 + 命中数),30 是采集秒数。折叠栈喂给 flamegraph.pl 生成火焰图——x 轴宽度代表样本占比(不是时间顺序),y 轴是栈深度,平顶的宽塔就是热点函数。火焰图解读三要点:(1) 找最宽的"平台"——顶部宽平的帧是消耗 CPU 的叶子函数;(2) 忽略窄塔——占比 <1% 的栈通常不值得优化;(3) 对照代码——帧名直接对应函数,点击 SVG 可缩放。"[unknown]" 帧表示符号缺失:应用编译时 strip 了符号,需要 debuginfo 包或 perf 的 --call-graph dwarf 模式。Java 应用需要 perf-map-agent 生成符号映射(或直接用 async-profiler)。采样频率的权衡:99Hz 是默认平衡点;分析短暂现象可临时提到 999Hz(开销 <2%);长期常态监控可降到 49Hz。profile 的实现:BPF 程序挂在 perf_event 定时器上,触发时调用 bpf_get_stackid 把栈存入栈表 map 并计数——聚合在内核态完成,输出量极小,这是它能 7×24 常态运行的原因。/* 示例6:stackcount——按调用栈计数任意内核函数 */
sudo /usr/share/bcc/tools/stackcount -p 2233 'c:malloc' /* 用户态 malloc 的调用栈 */
sudo /usr/share/bcc/tools/stackcount 't:block:block_rq_issue' /* 块 I/O 发起栈 */
sudo /usr/share/bcc/tools/stackcount -i 5 'vfs_write' /* 每5秒输出 */
/* 输出示例:
mysqld`ha_innobase::write_row
mysqld`row_insert_for_mysql
...
vfs_write
ksys_write
4521
*/stackcount 是"谁调用了这个函数"的通用答案:对任意内核函数、tracepoint、用户态函数(c: 前缀)或 USDT 探针,按完整调用栈聚合计数。三个典型用法:(1) c:malloc 追踪内存分配来源——哪个代码路径在分配内存,配合第 7 章的 memleak 定位泄漏;(2) t:block:block_rq_issue 看磁盘 I/O 是谁发起的——当 iostat 显示 IOPS 异常时,这个命令直接给出应用调用栈;(3) 追踪 tcp_retransmit_skb 的栈分析重传路径。输出按栈聚合,末尾数字是命中次数,栈的顺序是叶子在上(与火焰图一致)。选项:-P 按进程分解、-i 间隔输出、-f 折叠格式(可转火焰图)、-K/-U 只要内核栈/用户栈、-m 百万分之一采样率(高频函数降采样)。stackcount 本质上是"带栈聚合的 kprobe/uprobe 计数器",是 bpftrace @[kstack] = count() 模式的产品化版本。开销注意:抓栈成本(每次数微秒)远高于普通计数,对高频函数(如页错误处理)务必用 -m 采样或严格过滤。实验笔记
本实验系统演练 BCC 工具集与 bpftrace 的核心用法,建立"问题→工具→解读"的完整工作流。预计耗时 3-4 小时。环境:内核 4.9+(推荐 5.4+),安装 bcc-tools、bpftrace、sysstat、fio、stress-ng。建议用虚拟机,方便制造可控负载。
【实验一:工具普查与分类】列出你系统上安装的全部 BCC 工具(ls /usr/share/bcc/tools/),按本章的六类分类法给每个工具归类(不确定的 man 一下)。挑五个名字猜测其功能,再读 man 验证猜测。目标:建立工具集的"索引级"熟悉度。
【实验二:制造负载并观察】用 stress-ng 和 fio 制造四类负载,每类负载下运行对应工具并记录输出:(1) CPU 负载(stress-ng --cpu 4 --timeout 60)下运行 profile -F 99 10,观察热点栈(会看到 stress-ng 的忙循环函数);再运行 runqlat 10 1 观察调度延迟变化。(2) I/O 负载(fio 随机读)下运行 biolatency -D 10 1 和 biosnoop 各 10 秒,对比聚合视图与明细视图的信息差异。(3) 进程风暴(while true; do /bin/true; done)下运行 execsnoop -t,估算每秒 exec 速率,然后停止负载。(4) 内存分配负载(stress-ng --vm 2 --vm-bytes 256M)下运行 syscount -L 观察缺页相关系统调用。每个场景写三行结论:工具显示了什么、说明了什么、下一步该查什么。
【实验三:bpftrace 现场脚本三连】(1) 写脚本统计每个进程读写的字节数(提示:tracepoint:syscalls:sys_exit_read,retval 是读取字节数,@bytes[comm] = sum(retval))。(2) 写脚本追踪所有超过 5ms 的块 I/O 并打印进程和大小(提示:block tracepoint 配对,参考示例3的阈值过滤模式)。(3) 写脚本统计 TCP 重传按内核栈聚合(kprobe:tcp_retransmit_skb { @[kstack] = count(); }),用 ping 洪水或 tc netem 制造丢包触发重传。三个脚本各运行 30 秒,记录输出并解读。
【实验四:直方图解读训练】构造三种存储场景并采集 biolatency 直方图:(1) 纯页缓存读(先 dd 一个大文件进缓存,再重复读);(2) 直接 I/O 随机读(fio --direct=1 --rw=randread);(3) 混合负载。对比三张直方图的形状差异:缓存场景主峰应在微秒级,直接 I/O 在设备延迟量级。然后人为制造长尾:在虚拟机上用 tc 或 cgroup io 限制制造偶发慢 I/O,观察直方图右侧的变化。写出你的"直方图指纹库":什么形状对应什么系统状态。
【实验五:完整推理链演练】模拟一个"服务变慢"场景:启动一个 Python 脚本,内部有锁竞争(多线程递增共享计数器不加优化)加偶发的 time.sleep(0.05) 模拟慢 I/O。只允许使用本章工具(syscount、profile、offcputime、runqlat、stackcount),写出完整排查报告:每一步用什么工具、看到什么、推断什么、下一步查什么,最终定位到锁竞争和 sleep 两个根因。这个演练的价值在于建立"假设驱动"的排查习惯——每次运行工具前写下你要验证的假设,运行后写下结论。
【实验报告要求】五个实验的命令、关键输出摘录、解读结论。特别要求:实验四必须包含三张直方图的对比分析;实验五必须是推理链形式而非工具输出堆砌。把实验中所有"工具输出与预期不符"的时刻记录下来——这些困惑点正是深入理解的机会,带着它们进入第 5 章。
延伸阅读
- Brendan Gregg, "BPF Performance Tools" 第 4 章及工具索引附录——原书第 4 章有全部工具的速查表和每个工具的一页纸说明(含示例输出),附录按 USE 方法组织工具矩阵,是案头必备。
- BCC 仓库 examples 目录 (github.com/iovisor/bcc/tree/master/examples)——除工具外还有 tracing 和 cpp 两个教学目录,hello_world.py 到完整工具的渐进示例,配 tutorial.md 讲解开发套路。
- bpftrace 官方 one-liner 教程 (github.com/iovisor/bpftrace/blob/master/docs/tutorial_one_liners.md)——官方维护的单行脚本教程,从基础到高级二十余个实战脚本,每个都有解释,适合逐条敲一遍。
- Brendan Gregg, "Systems Performance" 第 2 版 (2020)——方法论圣经,USE 方法、60 秒检查法、火焰图方法论的完整阐述,第 2 章的工具论与本书第 4 章互为表里,建议对照阅读。
- FlameGraph 项目文档 (github.com/brendangregg/FlameGraph)——火焰图生成工具的完整文档,含差分火焰图、off-CPU 火焰图、内存火焰图的生成方法,Gregg 的 flamegraph 网站有数百张真实案例图可研习。
- man 手册:execsnoop(8)、biolatency(8)、runqlat(8)、profile(8) 等——BCC 安装自带全部工具的 man 页,包含每个选项的精确语义和示例,遇到输出疑惑先查 man 的 EXAMPLES 和 FIELDS 节。
- Netflix Tech Blog 的 BPF 系列文章——"Netflix Instance Analysis"、"Open Connect at the Edge" 等文章展示了 BPF 工具在超大规模生产环境的真实应用案例,是理解"工具如何融入运维体系"的最佳材料。
- Learn BPF (learnbpf.org) 与 ebpf.io 的教程区——社区维护的 BPF 学习资源聚合站,包含各工具的在线示例和常见问题的 cookbook,适合碎片时间查阅。