土法炼钢兴趣小组的算法知识备份

【可观测性工程】内核追踪:ftrace、kprobe、uprobe、tracepoint 生产实战

文章导航

分类入口
architectureobservability
标签入口
#kernel#ftrace#kprobe#uprobe#tracepoint#bpftrace#perf#ebpf#linux#tracing

目录

内核追踪:ftrace、kprobe、uprobe、tracepoint 生产实战

某次生产故障:一个 Go 服务偶尔会出现 200ms 的 fsync 延迟,但 prometheus-node-exporter 显示磁盘 I/O 正常,iostat 显示 await 在 2ms 以内,strace 挂了 30 分钟没有捕捉到任何异常。最终通过 bpftrace 在 vfs_fsync_range 上挂了一个 kprobe,发现 fsync 延迟确实在某个特定时间点飙升了——进一步分析发现是一个冷数据页 writeback(回写)被内核在 fsync 之前强制触发了。

这个案例说明了一个基本事实:用户态的可观测性工具能看到系统 80% 的问题,但剩下 20% 最深层次的问题——内核调度延迟、文件系统写回、网络栈丢包、内存回收——只能在”内核算账”的层面被捕获。内核追踪不是日常工具,但当你被一个”所有用户态指标都正常但用户就是报障”的问题卡住时,它是最后的武器。

一、Linux 内核追踪技术栈全景

Linux 内核追踪的主线演进分为四个阶段。第一阶段(2008 年前后):ftrace 进入内核主线——/sys/kernel/debug/tracing/ 这个伪文件系统成为所有后续内核追踪的基础设施。第二阶段(2010–2015):perf_event 子系统成熟,将硬件性能计数器(PMU)和软件事件统一为 perf_event_open() 系统调用。第三阶段(2015–2020):eBPF 从简单的包过滤扩展到通用内核虚拟机,kprobe/uprobe 通过 eBPF 程序获得了安全的动态追踪能力。第四阶段(2020 至今):CO-RE(Compile Once, Run Everywhere)使得 eBPF 程序可以在不同内核版本间移植,BTF(BPF Type Format)使得 bpftrace 可以”理解”内核数据结构。

当前(2026 年)的内核追踪技术栈可以这样分层理解:

组件 适合谁 代价
前端工具 bpftrace, perf, ftrace, trace-cmd SRE / 性能工程师 取决于 attach 点
内核接口 tracepoint, kprobe, uprobe, perf_event 维护者 / 平台团队 tracepoint 轻量,kprobe 中,uprobe 重
数据管道 eBPF ring buffer, ftrace ring buffer 内核开发者 ring buffer overhead ~1–5%
后端消费 bpftrace maps, perf.data, trace.dat SRE 取决于数据量

二、ftrace:内核自带的函数追踪器

ftrace 是内核内置的最古老但最稳定追踪器。它的接口是 /sys/kernel/debug/tracing/ 下的一系列伪文件——不需要安装任何工具,只要内核编译时启用了 CONFIG_FTRACE=y,挂载 debugfs 后就能用。

function_graph tracer 是 ftrace 最有用的功能之一:它追踪每个内核函数的进入和退出,记录每个函数的执行时间和调用关系。例如,定位某个 write() 系统调用的内核路径上哪一步最慢:

cd /sys/kernel/debug/tracing
echo __x64_sys_write > set_graph_function
echo function_graph > current_tracer
echo 1 > tracing_on
# 等待几秒
echo 0 > tracing_on
cat trace > /tmp/ftrace_output.txt

输出会显示 vfs_write → ext4_file_write_iter → ext4_dio_write_iter → ... 每一步的耗时(单位为微秒)。你可以直接看到哪个子函数占用了大部分时间。

ftrace 的最大优势是不需要 eBPF——它不依赖 eBPF 子系统、不需要 BTF、不需要任何用户态工具。在 eBPF 被安全策略禁止、或者内核太老不支持 eBPF 的环境中,ftrace 是唯一的选择。trace-cmd 是 ftrace 的 CLI 包装器,KernelShark 提供了 GUI 时间线可视化。

ftrace 在生产环境中的安全边界:function_graph tracer 如果不加过滤,会追踪内核中的每一个函数调用(几十万个/秒)。在生产节点上做全量 function_graph 追踪,系统性能会下降 10–50 倍——等同于一次自找的 DoS。永远在用 function_graph 之前设置 set_graph_functionset_ftrace_filter 做精确过滤。同时,限时追踪(追踪 30 秒后自动关闭)——用 sleep 30 && echo 0 > tracing_on 做安全兜底。

三、tracepoint:内核的稳定 API

tracepoint 是内核开发者显式在代码中埋下的”钩子”。与 kprobe 不同,tracepoint 是稳定的——它不会随内核版本升级而改名或消失,因为它是内核 ABI 的一部分。每个 tracepoint 在 /sys/kernel/debug/tracing/events/<subsystem>/<event>/ 下有对应的目录,包含 enableformat 等控制文件。

关键子系统的核心 tracepoint:

tracepoint 的开销极低(每个事件约 50–200 ns),且触发频率可控(由内核代码调用频率决定),生产环境完全可以持续启用特定 tracepoint 做常态监控。一个常见模式是用 bpftrace 挂 tracepoint 并计算延迟 histogram:

bpftrace -e 'tracepoint:block:block_rq_issue { @start[args->dev] = nsecs; }
             tracepoint:block:block_rq_complete { $t = nsecs - @start[args->dev]; @lat_us = hist($t / 1000); }'

这在生产中是安全的——tracepoint 是被设计为可被频繁调用的。

四、kprobe:挂内核函数的瑞士军刀

kprobe 可以在任何内核函数的入口或出口处动态插入追踪点——不需要内核重新编译、不需要重启。工作原理是:用 int3 指令(x86)替换目标函数的前几个指令,当 CPU 执行到这个位置时触发中断 → 执行你注入的 eBPF 程序 → 恢复原指令继续执行。

kprobe 的能力是”无所不能”——你可以追踪任何内核函数。但这也是它的风险来源:kprobe 挂的函数可能在不同内核版本间改名、消失或被内联(inline)。kprobe 挂在高频路径上(如 tcp_rcv_established__schedule_raw_spin_lock)的开销可能达到几个百分点。

生产安全边界: - 优先用 tracepoint(稳定且安全),只在 tracepoint 覆盖不到的时候才用 kprobe。 - 只挂低频路径——不要挂调度器热路径、网络数据面热路径、内存分配器热路径。 - 用 kprobe 的 kernel 返回(kretprobe)时小心——每个函数调用都会触发,在高频函数的返回路径上挂 kretprobe 可能导致显著的性能劣化。 - 限时追踪——用 timeout 或 bpftrace 的 -d 选项限制追踪持续时间。

五、uprobe:用户态函数的动态追踪

uprobe 是 kprobe 的用户态对应——它可以在用户态可执行文件的任意符号(函数)上设置断点并执行 eBPF 程序。典型的适用场景:追踪一个没有内置 metrics 的遗留 C/C++ 服务(挂 mallocfree 分析内存分配模式)、追踪 Python 或 Node.js 解释器的内部函数(挂 PyEval_EvalFrameEx 分析 Python 代码执行时间)。

uprobe 有一个常被忽略的限制:在容器环境中,uprobe 挂的是宿主机的二进制文件路径,不是容器内的。如果你的容器有独立的文件系统,容器内的 /usr/bin/myapp 在宿主机上的路径是 /var/lib/docker/overlay2/<hash>/merged/usr/bin/myapp——你需要用宿主机的绝对路径来挂 uprobe。这就意味着 uprobe 在 K8s/Docker 环境中的可用性受限于你对容器运行时的了解程度。

Go 程序从 1.17 开始使用寄存器调用约定(register-based calling convention),这意味着 uprobe 的参数读取方式在 Go 1.16 和 1.17+ 之间不同。bpftrace 的 uprobe:/path/to/go-binary:runtime.mallocgc 在 Go 1.17+ 下需要额外指定参数读取规则——这是 Go 用户态追踪的一个常见坑。

六、bpftrace:内核追踪的高级语言

bpftrace 是一行脚本到完整追踪程序之间的完美中间态。它把 eBPF 的复杂性封装在类 awk 的语法中,让 SRE 能够用一行命令完成原本需要几百行 C 代码才能做到的追踪:

# 统计进程被 CPU 调度出去的延迟分布
bpftrace -e 'kprobe:finish_task_switch { @lat_ns = hist(nsecs - @start[tid]); }'

# 按系统调用统计延迟
bpftrace -e 'tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
             tracepoint:syscalls:sys_exit_read /@start[tid]/ { @read_lat = hist(nsecs - @start[tid]); }'

# 统计 TCP 重传次数(网络故障的首选指标)
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { @retrans[comm] = count(); }'

bpftrace 内置的数据结构——@count[key](计数器)、@dist[key](直方图)、@hist(value)(分布直方图)——可以让你在不写任何存储代码的情况下积累和聚合追踪数据。它的 -d 选项(--duration)允许你设置”运行 N 秒后自动退出并打印结果”——这是生产安全的重要保障。

七、内核追踪的生产安全

内核追踪最大的敌人不是技术复杂性,是忘记关闭追踪。管理一个 K8s 集群,早上十点挂了一个 function_graph 追踪忘记关闭,下午两点发现整个集群的 API Server 超时——所有节点的 CPU 都被 function_graph 的追踪开销吃掉了——这种事情在 SRE 圈内不是笑话,是真实发生过多次的事故。

安全清单: - 永远限时追踪(timeout-dsleep && echo 0 > tracing_on)。 - 永远在 attach 前用量化估计过滤:目标函数的调用频率是多少?如果 > 10000 次/秒,在追踪前加入额外过滤条件。 - K8s 环境中用非 privileged 权限运行 bpftrace——CAP_BPF + CAP_PERFMON + CAP_SYS_ADMIN 替代 privileged: true。 - 确认内核编译选项:CONFIG_DEBUG_INFO_BTF=y(bpftrace 解析内核结构体的前提,缺失则所有 kprobe 都无法读取参数)。

八、决策树

九、关键概念回顾

十、下一步

内核追踪是可观测性工具箱的最后武器。从下一篇文章开始,我们进入可观测性的治理层——SLO 工程如何定义系统的可靠性目标、错误预算如何驱动研发节奏。下一篇 SLO 工程:错误预算、Burn Rate、多窗口告警


上一篇Traces 栈与采样:Jaeger、Tempo、Zipkin、SkyWalking

下一篇SLO 工程:错误预算、Burn Rate、多窗口告警

参考资料

  1. Linux Kernel, ftrace — Function Tracer, Documentation/trace/ftrace.rst
  2. B. Gregg, BPF Performance Tools, Addison-Wesley, 2019
  3. bpftrace, Reference Guide, https://github.com/bpftrace/bpftrace/blob/master/docs/reference_guide.md
  4. Linux Kernel, tracepoint — documentation, Documentation/trace/tracepoints.rst
  5. perf, perf-trace, https://perf.wiki.kernel.org/

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。全 25 篇。


By .