内核追踪: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_function 或
set_ftrace_filter
做精确过滤。同时,限时追踪(追踪 30
秒后自动关闭)——用
sleep 30 && echo 0 > tracing_on
做安全兜底。
三、tracepoint:内核的稳定 API
tracepoint 是内核开发者显式在代码中埋下的”钩子”。与
kprobe 不同,tracepoint
是稳定的——它不会随内核版本升级而改名或消失,因为它是内核
ABI 的一部分。每个 tracepoint 在
/sys/kernel/debug/tracing/events/<subsystem>/<event>/
下有对应的目录,包含
enable、format 等控制文件。
关键子系统的核心 tracepoint:
- scheduler:
sched_switch(上下文切换,记录 prev_pid → next_pid)、sched_wakeup(进程被唤醒时)、sched_migrate_task(任务在 CPU 间迁移) - block
I/O:
block_rq_issue(块设备请求发出)、block_rq_complete(请求完成——两者时间差 = I/O 延迟)、block_bio_queue(bio 进入队列) - network:
net_dev_queue(包进入网卡队列)、napi_gro_receive(GRO 接收)、tcp_retransmit_skb(TCP 重传——网络问题的黄金指标) - syscall:
sys_enter_*/sys_exit_*(进入/退出系统调用,参数和返回值都可追踪) - file:
ext4_sync_file_enter/exit(fsync 进入和退出)、ext4_da_write_begin/end(延迟分配写入)
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++ 服务(挂 malloc 和 free
分析内存分配模式)、追踪 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、-d、sleep && 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
都无法读取参数)。
八、决策树
- 想知道某类内核事件发生了吗?→
tracepoint(
sched_switch、block_rq_complete、tcp_retransmit_skb) - 想知道某个特定内核函数有多慢?→ kprobe + histogram(挂低频函数,限时追踪)
- 想知道用户态二进制某个函数的行为?→ uprobe(注意容器路径和 Go 1.17+ 调用约定)
- 想快速回答”why is my server slow”?→ bpftrace 一行脚本 + perf top
- 持续内核级性能剖析?→ eBPF + Parca/Pixie(而非手写 bpftrace 脚本)
九、关键概念回顾
- ftrace:内核自带,不需要
eBPF,
function_graphtracer 用于定位内核函数耗时。不加过滤 = 自找 DoS。 - tracepoint:稳定的内核 ABI,开销极低,适合生产常态监控。
- kprobe:动态挂任意内核函数,能力无限但风险对应。单点使用、限时追踪。
- uprobe:用户态 kprobe。容器环境下需要注意二进制文件在宿主机上的路径。
- bpftrace:一行 eBPF 脚本,用类 awk 语法做内核追踪。内置 histogram/count/dist 数据结构。
十、下一步
内核追踪是可观测性工具箱的最后武器。从下一篇文章开始,我们进入可观测性的治理层——SLO 工程如何定义系统的可靠性目标、错误预算如何驱动研发节奏。下一篇 SLO 工程:错误预算、Burn Rate、多窗口告警。
上一篇:Traces 栈与采样:Jaeger、Tempo、Zipkin、SkyWalking
下一篇:SLO 工程:错误预算、Burn Rate、多窗口告警
参考资料
- Linux Kernel, ftrace — Function Tracer, Documentation/trace/ftrace.rst
- B. Gregg, BPF Performance Tools, Addison-Wesley, 2019
- bpftrace, Reference Guide, https://github.com/bpftrace/bpftrace/blob/master/docs/reference_guide.md
- Linux Kernel, tracepoint — documentation, Documentation/trace/tracepoints.rst
- perf, perf-trace, https://perf.wiki.kernel.org/
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【可观测性工程】eBPF 可观测性全景:bcc、bpftrace、libbpf 的工程路径
eBPF 如何实现零侵入、内核级、低开销的可观测性:从 kprobe/uprobe/tracepoint/fentry 钩子机制,到 bcc 工具集、bpftrace 脚本语言、libbpf+CO-RE 可移植编程,再到 Pixie、DeepFlow、Grafana Beyla 等商业化工具,结合内核版本兼容性与生产部署实战。
可观测性工程
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。全 25 篇。
【可观测性工程】网络可观测性:Cilium Hubble、Pixie、DeepFlow、Tetragon
从 L3/L4/L7 三层观测视角出发,讲 eBPF socket filter / tc / XDP 数据采集与 Cilium Hubble 流日志、Tetragon 安全可观测、Pixie 协议自动解析、DeepFlow 架构;展开 bpftrace + kfree_skb_reason 的内核丢包定位、TLS 解密、HTTP/2 解析与服务拓扑自动发现。
【可观测性工程】Traces 栈与采样:Jaeger、Tempo、Zipkin、SkyWalking
拆解 Jaeger、Tempo、SkyWalking 三种开源分布式追踪方案的架构本质与工程取舍:全索引 vs 无索引、采样策略(头部/尾部/自适应)、传播协议(W3C TraceContext)的断裂诊断,以及选型决策框架。