Linux 动态追踪这个话题里ftrace 是我这几年用得最频繁的一个工具。它不像某些重量级框架那样需要额外编译模块、安装依赖、写脚本内核里自带的那套接口就在 tracefs 目录里摆着echo几行字就能开始记录。它解决的问题很具体线上某个服务偶发卡顿、某个系统调用比平时慢了十倍、某个驱动在特定路径上反复重试这些靠日志和监控指标看不出来的东西ftrace 能直接给出函数级别的现场。适合谁来参考一类是刚开始接触内核可观测性的朋友想从最简单的接口摸清调用链另一类是已经在生产环境做性能排查的老手需要一个开销可控、随时能开能关的临时探针。接下来我把这套东西从选型思路、目录结构、四类追踪器的实操一直到开销估算和踩坑经验按实际动手的顺序拆开讲尽量让你看完就能在自己的机器上跑起来。1. 选型思路为什么在工具满天飞的环境里我还留着 ftrace1.1 从“猜问题”到“看现场”一次典型的定位需求很多人排查性能问题的第一反应是看监控、看日志、看 CPU 使用率。这些手段的问题在于粒度太粗。日志告诉你某个请求耗时 800 毫秒监控告诉你那个时间点磁盘 IO 有个尖峰但中间这几百毫秒到底是谁在等谁调用链断在哪一环这些信息是断的。这种时候你需要的不是又一个指标而是一份按时间排序的函数调用流水账。ftrace 提供的正是这份流水账。它记录的是内核函数在什么时间、在哪个 CPU、由哪个进程调用函数之间谁调用了谁、各自消耗了多久。我第一次真正把它用起来是因为一个很典型的场景某台机器上的某个服务在高峰期每隔几分钟就抖一下延迟从个位数毫秒跳到几百毫秒CPU 和内存都正常。后来用 function_graph 抓了 2 秒的调用流一眼就看到某个路径在反复走一个重试分支每次失败都要在函数栈里绕一大圈。换成别的排查方式可能要在代码里加十几个日志点重新发版。这就是 ftrace 的核心价值不需要改代码、不需要重启业务、不需要提前埋点只要内核编译时带了对应选项你随时可以打开它抓一段现场然后关掉。1.2 ftrace 与同类手段的定位差异动手之前先想清楚工具边界比上来就敲命令重要得多。同样是看内核行为ftrace、perf、eBPF、动态打印这几类手段的定位差别很大选错了不是不能做事而是要多花几倍时间。手段典型能力开销量级是否需要改代码适合的场景ftrace 内置 tracer函数流、调用图、延迟、事件低到中取决于 tracer 和过滤范围不需要快速定位调用链、看耗时、确认某函数是否被调用ftrace 的 kprobe 事件在任意函数入口/返回取参数、取返回值中探针越多越高不需要没埋点但想看入参和返回值的函数perf采样剖析、硬件事件、调用栈统计低采样模式不需要找 CPU 热点、看指令级分布自定义 eBPF 程序任意逻辑、聚合、用户态联动低到中可精细控制需要写程序长期在线观测、复杂聚合逻辑动态打印精确输出指定位置的信息中到高取决于频率需要重启或重新加载模块已知问题位置的定点确认我的实际习惯是先用 perf 采样找出可疑的热点函数再用 ftrace 的 function_graph 把热点附近的调用链和耗时展开如果发现参数可疑再挂 kprobe 把入参打出来。这套组合拳的好处是每一步都不需要改动被测系统排查完把开关关掉就恢复原状。eBPF 当然更强但它需要额外的工具链和程序编写在应急排查的场合下ftrace 的“上手即用”属性更值钱。这里有个容易被忽略的点ftrace 的很多能力其实可以和其他工具组合。比如 perf 有一个子命令可以直接驱动 ftrace 采样trace-cmd 这类工具也是把 ftrace 的接口包装了一层做成了录制和报告的形式。理解 ftrace 的原始接口之后这些上层工具用起来会顺手很多因为它们暴露的参数本质上还是那几个文件。1.3 它凭什么开销这么低两套插桩机制的底层账理解底层机制你才知道什么时候可以放心开、什么时候要小心。第一套是编译期插桩。内核在编译时会通过编译选项在每个函数入口插入一个很小的桩传统做法是mcount调用新版本架构上更多用-fpatchable-function-entry在函数开头预留几个字节的指令空间。这个桩在运行时会被动态改写默认情况下一律替换成空操作指令nopCPU 执行到那里相当于什么都没发生代价接近零。当你通过set_ftrace_filter指定了要跟踪的函数后内核只把匹配的那部分函数的桩改写成跳转指令跳进追踪器。这就是所谓的动态 ftrace不是全量记录而是按需激活。这套机制解释了一个常见疑问为什么我打开了 function tracer但只有我指定的那几个函数出现在结果里因为没被选中的函数压根没被激活它们的入口仍然是 nop。这也是为什么在生产环境用 ftrace 时必须写过滤条件全量激活等于把每个函数都变成一次函数调用加一次环形缓冲区写入开销立刻就上去了。第二套是静态 tracepoint。内核开发者在代码的关键位置预先埋好了带参数的钩子比如调度切换、系统调用进出、块设备请求、网络包收发。这些钩子在没有启用时同样是被跳过的启用后才开始往缓冲区写结构化数据。因为参数是开发者预先定义好的不需要你自己去解析寄存器所以用起来比 kprobe 简单得多也稳定得多。注意动态 ftrace 的“低开销”是相对的。它省掉的是未激活函数的成本但一旦激活每次命中仍然要走一次函数调用、一次时间戳读取和一次缓冲区写入。所以在极热的路径上长时间开启依然会改变系统的行为。2. 读懂 ftrace 的骨架tracefs 目录与核心开关2.1 挂载点确认与目录结构梳理现代内核把 ftrace 的接口放在一个独立的伪文件系统里就是 tracefs。绝大多数发行版已经在启动时挂好了常见位置是两个# 新内核推荐位置 ls /sys/kernel/tracing # 兼容位置本质上是同一个东西的软链接或独立挂载 ls /sys/kernel/debug/tracing如果你在容器里、精简系统上或者嵌入式设备上发现这两个目录都不存在可以手动挂载mount -t tracefs nodev /sys/kernel/tracing挂载之后目录里会有几十个文件第一次看确实容易懵。我习惯把它们分成四类来记总控类current_tracer、tracing_on、trace、trace_pipe、trace_options过滤类set_ftrace_filter、set_ftrace_notrace、set_ftrace_pid、set_graph_function、available_filter_functions缓冲与统计类buffer_size_kb、buffer_total_size_kb、per_cpu/、tracing_max_latency子目录events/静态事件、options/开关集合、instances/多实例instances/这个目录值得单独说一句。它允许你创建多个独立的追踪实例每个实例有自己的 tracer、过滤条件、缓冲区。这在多人共用一台机器、或者需要同时抓两种不同类型的数据时特别有用一个人开 function_graph另一个人开事件追踪互不干扰。创建方式很简单mkdir /sys/kernel/tracing/instances/latency_test ls /sys/kernel/tracing/instances/latency_test进去之后是一套完整的、和顶层结构一致的接口但缓冲区和配置都是独立的。用完之后rmdir删除内核会把占用的内存一起释放。2.2 六个必须掌握的控制文件我把最常用的几个文件整理成一张表先建立整体印象后面实操会反复用到。文件作用典型用法available_tracers列出内核支持的追踪器cat available_tracerscurrent_tracer切换当前使用的追踪器echo function current_tracertracing_on追踪总开关1 开 0 停echo 0 tracing_ontrace读取缓冲区快照读完不清空cat tracetrace_pipe流式读取读出即消费会阻塞cat trace_pipetrace_options查看和设置所有输出选项cat trace_options几个容易踩的细节tracing_on写 0 只是停止往缓冲区写入并不会卸载当前追踪器和过滤条件。很多人以为写 0 就万事大吉结果下次别人用机器时看到的还是上次的残留配置还纳闷为什么自己抓的数据不对。trace和trace_pipe的行为差别很关键。读trace拿到的是当前缓冲区里的快照读完之后数据还在可以反复读读trace_pipe是消费式的读出的事件会被删除而且如果缓冲区里没数据它就会一直阻塞在那里等。调试时用trace更安全做实时观察或者数据量太大不适合一次读出来的时候用trace_pipe。trace_options里是一长串开关每行一个选项名前面加no就是关闭。常用的是funcgraph-proc在调用图里显示进程和 CPU、func_stack_trace每次命中函数时打印调用栈、annotate显示各种中断相关的注释、sym-addr同时显示符号名和地址。这组选项直接写trace_options文件就行比如echo norecord-cmd trace_options。还有trace_clock这个文件容易被忽略但很重要。默认是local也就是每个 CPU 用自己的时钟多核环境下不同 CPU 的时间戳可能对不齐。要跨 CPU 比较事件顺序必须切到globalecho global trace_clock切换到 global 会带来一点点额外开销因为要读一个全局的时钟源但这点代价换来的是可比较的时间线排查跨核调度问题时非常值。2.3 trace 输出的字段怎么读第一次看 trace 输出的人基本都会盯着那一串竖线发愣。它其实是一个压缩过的上下文标记格式大致是这样# tracer: function # # entries-in-buffer/entries-written: 1234/5678 #P:8 # # _------ CPU# # / _----- irqs-off # | / _---- need-resched # || / _--- hardirq/softirq # ||| / _-- preempt-depth # |||| / # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | |四个竖线位依次表示irqs-offd表示当前中断被关闭.表示中断是开的。need-reschedN表示内核已经标记了需要重新调度只是还没到调度点.表示没有。hardirq/softirq.表示不在中断上下文h表示在硬中断里s表示在软中断里H表示两者都有。preempt-depth一个数字表示抢占被禁用的嵌套层数0 就是允许抢占。比如你看到某一行是dN.1含义就是这个函数执行时中断是关闭的、ALREADY 有调度请求在等、不在中断上下文、抢占深度为 1。这几个标记组合起来往往能直接解释一些“为什么这个函数卡了这么久”的问题——比如耗时集中在关中断的临界区里那再怎么优化函数本身也没用得去查那次关中断为什么这么长。再看缓冲区那两行统计。entries-in-buffer是当前还在环里的事件数entries-written是从开始追踪到现在一共写了多少条。这两个数字差距越大说明被覆盖掉的历史记录越多也就是说你的缓冲区开小了或者采样时间太长了。3. 四类追踪器实战从函数流到任意插桩3.1 function tracer先把调用流抓出来function tracer 是最基础的一个它记录的是“哪些函数被调用了按时间顺序”。没有耗时只有顺序和上下文但已经能回答很多问题某个函数到底有没有被执行到、某个分支是不是根本没走、一次请求大概经过了哪些环节。一个完整的操作流程我习惯写成这样cd /sys/kernel/tracing # 1. 先关总开关避免边配边写 echo 0 tracing_on # 2. 重置到 nop清掉上一次的残留 echo nop current_tracer # 3. 清空缓冲区 echo trace # 4. 设置过滤只保留关心的函数 echo set_ftrace_filter echo do_sys_openat2 set_ftrace_filter # 5. 切到 function tracer echo function current_tracer # 6. 打开开关执行你的复现操作 echo 1 tracing_on # ... 这里做触发动作 ... echo 0 tracing_on # 7. 读结果 cat trace | head -60顺序很关键。先写过滤条件再切 tracer因为动态 ftrace 是根据过滤器来决定激活哪些函数的。如果你先切到 function 再写过滤那一瞬间可能已经把全量函数都激活了一遍在繁忙的机器上会造成一次明显的抖动。过滤器的语法有几个实用技巧# 通配符 echo vfs_* set_ftrace_filter echo *_open set_ftrace_filter # 排除某些函数 echo !*_lock* set_ftrace_notrace # 一次清空 echo set_ftrace_filter注意和的区别会覆盖整个过滤器是追加。我见过不少人一直用结果每次只跟踪到最后一个函数还以为是内核的问题。如果不知道函数名怎么写先去available_filter_functions里查grep -i open available_filter_functions | head -20这个文件列出的就是可以被动态插桩的函数。查不到的函数通常是内联函数、被编译器优化掉的、或者是标记了不允许追踪的这些后面在问题排查里会细说。还有一个很实用的文件是set_ftrace_pid。当系统上有成百上千个进程在跑而你只关心某个进程时把它的 PID 写进去输出量能降一个数量级echo 12345 set_ftrace_pid需要提醒的是这个过滤是按 PID 精确匹配的子进程默认不会被继承跟踪如果你要知道子进程的行为得另想办法。另外清空要用echo set_ftrace_pid写 0 有时候会被当成一个无效值而不是“清除”。3.2 function_graph看耗时和调用链function tracer 告诉你“谁被调用了”function_graph 告诉你“谁调用了谁各自花了多久”。后者在定位耗时问题上几乎是不可替代的。用法和 function 基本一样只是换个 tracerecho 0 tracing_on echo nop current_tracer echo trace # 只画从某个函数开始往下的调用树 echo set_graph_function echo do_sys_openat2 set_graph_function echo function_graph current_tracer echo 1 tracing_on # ... 触发 ... echo 0 tracing_on cat trace | head -80输出大概长这样1) | do_sys_openat2() { 1) | getname_flags() { 1) 0.512 us | kmem_cache_alloc(); 1) 1.204 us | } 1) | do_filp_open() { 1) | path_openat() { 1) 0.331 us | alloc_empty_file(); ... 1) 45.782 us | } 1) ! 120.336 us | }几个关键点开头的数字是 CPU 号后面的缩进表示调用层级{和}包住的是一整个函数调用区间中间那列是耗时。花括号左侧的标记是有含义的普通空格表示正常和!这类符号用来提示耗时已经超过了某个阈值不同内核版本的具体符号和阈值设置略有差异可以通过options里的相关开关调整。看到这些标记你就知道该往哪一层深挖。set_graph_function这个文件的价值在于聚焦。如果不设你现在运行的这套系统上随便一秒钟的调用树可能就有几十万行根本没法看。设了之后只有指定函数及其所有子调用会被记录输出量直线下降到可读的规模。注意function_graph 的开销明显高于 function因为它在函数返回时也需要一次钩子来回填耗时。在 IO 密集或者系统调用频繁的路径上全量打开可能直接把吞吐拉低。我一般的做法是配合set_graph_function把范围压到一两个入口函数采样时间控制在几秒以内。如果只是想看某个函数的最长耗时还有一个配套的开关是options/funcgraph-duration之类控制耗时显示细节的选项以及tracing_max_latency这个文件它会记录追踪期间观测到的最大延迟值抓偶发问题时很有用——你不需要盯着海量输出跑一段时间后直接读这个数就知道最坏情况有多坏。3.3 静态 tracepoint 与事件过滤events/目录是另一套体系里面是内核预先埋好的静态事件按子系统分类sched、irq、block、net、syscalls、kmem、writeback等等。每个事件都有自己的format文件描述它有哪些字段、每个字段的类型和含义。先看有哪些事件ls events/sched/ cat events/sched/sched_switch/formatformat文件很重要它决定了你能用哪些字段做过滤。比如sched_switch里有prev_comm、prev_pid、prev_prio、next_comm、next_pid这些字段看到这些名字你就知道可以怎么切分数据。启用一个事件echo 0 tracing_on echo trace echo 1 events/sched/sched_switch/enable echo 1 tracing_on # ... 跑一段时间 ... echo 0 tracing_on cat trace | head -40输出会是结构化的文本包含时间戳、CPU、进程名、以及事件定义的各个字段值。想一次启用整个子系统就写events/sched/enable想全开就写events/enable——但后者在繁忙系统上非常危险输出量是灾难级的。过滤器的写法是事件追踪的精髓。举个例子只看某个进程名参与的调度切换echo prev_comm my-service events/sched/sched_switch/filter数值字段支持比较运算符字符串字段支持相等和不等。多个条件可以组合echo prev_pid 1000 prev_prio 120 events/sched/sched_switch/filter这个过滤是在内核里执行的不匹配的事件根本不会写进缓冲区。这比先抓全量再在用户态grep高效得多也不容易因为缓冲区溢出丢数据。syscalls这个子系统特别有用。它把每个系统调用都做成了独立事件能直接看到入参和返回值。比如排查某个程序是不是在反复重试某个调用echo 1 events/syscalls/sys_enter_openat/enable echo sys_enter_openat set_event # 或者用上面的 enable 方式搭配进程过滤你能很快看出调用频率和参数规律。这比挂 kprobe 省事因为参数名都是定义好的。3.4 kprobe 与 kretprobe没有埋点也能看参数静态事件覆盖不到的函数就得靠 kprobe 了。它能在任意内核函数的入口或返回处插入探针并且支持从寄存器或内存里取参数。先确认内核配置支持kprobe_events这个文件存在就说明基本可用。写探针# 在函数入口插探针取第一个参数作为字符串 echo p:my_open do_sys_openat2 dfd%di filename0(%si):string flags%dx kprobe_events # 在函数返回处插探针取返回值 echo r:my_open_ret do_sys_openat2 ret$retval kprobe_events这里涉及架构相关的知识。以 x86_64 为例函数调用的前六个整型参数分别放在%di、%si、%dx、%cx、%r8、%r9里返回值在%ax里。上面的写法就是按这个约定来的。新一点的内核支持更友好的$arg1、$arg2这种写法可以省去记寄存器的麻烦echo p:my_open do_sys_openat2 filename$arg2:string flags$arg3 kprobe_events0(%si):string这个语法要解释一下%si里存的是一个指针0表示从该指针偏移 0 字节的位置开始读:string表示按以空字符结尾的字符串解析。如果参数是结构体指针可以用8(%di):u64这样的形式读某个字段。探针定义好之后它们会出现在events/kprobes/下面启用方式和静态事件一样echo 1 events/kprobes/my_open/enable echo 1 tracing_on用完记得清理否则探针会一直挂在那里echo 0 events/kprobes/my_open/enable echo -:my_open kprobe_events或者一次清空所有动态探针echo kprobe_events注意kprobe 不是万能的。函数被内联、被编译器做了常量折叠、或者符号在kallsyms里没有导出比如被标记为 static 且没保留符号都会导致探针挂不上或者挂上去之后取不到预期的值。挂之前先用grep 函数名 /proc/kallsyms确认符号存在。用户态的函数也能插桩用的是uprobe_events语法类似但要指定可执行文件路径和文件内偏移。实际用起来需要先用工具查出目标函数在二进制里的偏移比内核探针多一步准备工作但原理是一致的。3.5 触发器与 hist让内核自己算统计抓到数据只是第一步很多时候你要的是“统计结果”而不是“原始流水”。ftrace 的触发器机制可以让你在事件命中时自动执行动作而不需要用户态一直读。常用的触发器动作有这么几个触发器作用traceon命中时自动打开追踪traceoff命中时自动关闭追踪stacktrace命中时打印调用栈snapshot命中时拍一份缓冲区快照hist命中时做直方图/分组统计traceon和traceoff的组合特别适合抓偶发问题。比如你想知道某个错误路径最后一次被走到时现场是什么样可以让追踪默认关闭只在关键事件出现时打开echo traceon events/sched/sched_switch/trigger更精准的用法是配合条件判断echo stacktrace if prev_comm my-service events/sched/sched_switch/trigger这样只有当该进程参与调度时才打印调用栈输出量可以控制得非常小。hist触发器更强它直接在核内做聚合。比如统计各进程的调度次数排名echo hist:keysprev_comm:valshitcount:sorthitcount:size20 events/sched/sched_switch/trigger执行完触发动作后读events/sched/sched_switch/hist就能拿到按次数排序的统计表完全不需要用户态处理原始数据。这个能力在数据量大的时候价值极高因为原始事件可能每秒几十万条直接读会撑爆缓冲区而聚合之后只有几十行。snapshot触发器的用法是平时追踪开着但只往一个固定的小缓冲区写命中关键事件时把当前缓冲区的内容复制到快照区。之后读snapshot文件就能看到那一瞬间之前的现场而且不会被后续数据覆盖。抓“事故前一秒发生了什么”这类问题这是最顺手的方式。4. 生产环境落地开销控制与参数计算4.1 缓冲区到底占多少内存算一遍buffer_size_kb这个文件容易让人误解。它写的是每个 CPU的缓冲区大小不是总量。所以实际内存占用大约是这个值乘以 CPU 核数实际占用 ≈ buffer_size_kb × CPU 核数举例一台 8 核机器buffer_size_kb保持默认的 1408那总占用大约是 1408 KB × 8 ≈ 11 MB。这个量级在服务器上基本可以忽略。但如果你为了抓长周期问题把单核设成 32768那就是 32768 KB × 8 256 MB这个数字就得认真对待了尤其是在内存吃紧的边缘设备上。想确认实际值直接读这两个文件cat buffer_total_size_kb cat per_cpu/cpu0/stats后者会给出该 CPU 上实际分配的字节数、当前事件数、被覆盖的事件数等统计。被覆盖的事件数这个指标值得盯如果它一直在涨说明写速度超过了你的读取速度要么加大缓冲区要么缩小追踪范围。我的经验参数是这样短期抓调用链buffer_size_kb设 2048 到 4096 就够采样时间控制在 5 秒以内观察低频事件比如每分钟才发生一次的异常可以把缓冲区设大一些配合traceoff触发器让它在命中后自动停避免无限增长在内存受限的设备上128 到 512 也能用只是必须配合过滤条件否则几秒钟就溢出了。4.2 把开销压到可接受范围的几个开关生产环境用 ftrace核心原则只有一条范围越窄越好时间越短越好。具体落实下来有几个动作第一永远先设过滤器。set_ftrace_filter、set_ftrace_pid、set_graph_function、事件 filter这四者能用的都用上。动态 ftrace 的一大优势是未选中的函数不产生开销这个优势只有在写了过滤器时才成立。第二尽量用function而不是function_graph。后者多了一次返回钩子在深调用栈上开销差距可能是几倍。只有在确实需要看耗时分布时才上 graph看完立刻切回nop。第三优先用静态事件和 hist 聚合。事件追踪的数据结构比函数追踪紧凑而且 hist 在核内就完成了聚合用户态不需要持续读。对于“统计某类事件发生了多少次”这类需求用 hist 比抓原始流水再脚本处理要高效得多。第四采样时间设上限。我习惯在脚本里用timeout包一层timeout 5 bash -c echo 1 tracing_on; sleep 5; echo 0 tracing_on或者更精确地用触发器在命中后自动traceoff避免人工忘记关。第五注意trace_pipe的消费速度。如果追踪输出速率超过你的读取速率缓冲区会不断覆盖旧数据你看到的是残缺的现场。这种情况下宁可缩小过滤范围也不要指望“读快点就行”。第六用完彻底清理。一个干净的收尾脚本长这样cd /sys/kernel/tracing echo 0 tracing_on echo nop current_tracer echo set_ftrace_filter echo set_ftrace_notrace echo set_ftrace_pid echo set_graph_function echo trace echo 0 events/enable echo kprobe_events echo uprobe_events我把这段存成了一个脚本每次排查结束就跑一遍。看起来啰嗦但能避免“下一个人用的时候发现数据不对”这种扯皮。4.3 内核配置与启动参数如果你面对的是自己编译的内核、嵌入式设备或者精简过的系统很可能available_tracers里只有nop。这时候需要回到配置项配置项作用CONFIG_FTRACEftrace 框架总开关CONFIG_FUNCTION_TRACERfunction tracerCONFIG_FUNCTION_GRAPH_TRACERfunction_graph tracerCONFIG_DYNAMIC_FTRACE动态改写桩按需激活CONFIG_FTRACE_SYSCALLS系统调用事件CONFIG_KPROBE_EVENTSkprobe 事件支持CONFIG_UPROBE_EVENTS用户态探针CONFIG_STACK_TRACER栈追踪CONFIG_IRQSOFF_TRACER中断延迟追踪CONFIG_DYNAMIC_EVENTS动态事件与 hist 触发器嵌入式场景下空间紧张常见做法是只开CONFIG_FTRACE、CONFIG_FUNCTION_TRACER和CONFIG_DYNAMIC_FTRACE把 graph 和事件都裁掉能省不少体积。但要注意裁掉之后排查手段就少了一大半做产品前最好评估一下后续的定位需求。也可以在启动参数里预置追踪配置适合那种“开机就要开始记录、来不及手动操作”的场景ftracefunction ftrace_filterdo_sys_openat2 trace_buf_size2M这些参数的具体名称和可用性随内核版本有差异用之前建议查一下手头内核的文档。我在虚拟化和嵌入式环境里用启动参数配置的经验是参数生效早、能抓到启动阶段的问题但写错了不容易发现因为不影响启动。建议配好之后开机第一时间cat trace确认一下。4.4 与 perf、trace-cmd 的衔接ftrace 的原始接口是文件操作优点是透明、可控缺点是不方便做长时间录制和离线分析。这时候可以借力上层工具。trace-cmd是对 ftrace 接口的封装用法接近录制和回放# 录制 3 秒的函数追踪只保留匹配的函数 trace-cmd record -p function -l vfs_* -- sleep 3 # 分析录制结果 trace-cmd report | head -50perf也提供了一条驱动 ftrace 的路径# 用 perf 驱动 function_graph追踪指定函数 perf ftrace -t function_graph --funcs do_sys_openat2 -- sleep 2这些工具的价值在于把“配置、采样、停止、解析”串成了一条命令适合写进自动化脚本。但理解底层文件接口仍然是必要的——工具出问题的时候你还是得回到/sys/kernel/tracing里看看到底哪个开关没设对。另外提一个组合思路先用perf record做采样找到耗时的热点函数然后用perf ftrace或直接操作 tracefs 去看那个函数的调用链和耗时细节。采样的优势是开销低、覆盖面广追踪的优势是细节全、时序准两者互补。5. 常见问题速查与踩坑实录5.1 问题速查表现象大概率原因排查与解决/sys/kernel/tracing不存在内核没开 ftrace 或没挂 tracefs查/proc/mounts手动mount -t tracefs nodev /sys/kernel/tracingavailable_tracers只有nop缺少CONFIG_FUNCTION_TRACER重新配置内核或换用支持的内核版本写set_ftrace_filter报 Invalid argument函数名不存在或不可追踪grep 函数名 available_filter_functions确认切了 tracer 但trace是空的tracing_on为 0或过滤器没匹配先echo 1 tracing_on再检查过滤器只跟踪到最后一个函数用了覆盖而不是追加改用或一次写入多个模式trace_pipe卡住不动这是阻塞读属正常行为用cat trace读快照或配合head加超时事件启用了但没有输出事件本身没触发或 filter 写错先清空 filter 看是否有输出再逐步加条件kprobe 挂载失败符号不存在、被内联或偏移越界grep 函数名 /proc/kallsyms确认检查寄存器用法时间戳跨 CPU 对不上默认时钟是localecho global trace_clock追踪后系统明显变卡全量开启或缓冲区过大加过滤、缩短采样、切回nop输出被覆盖看不到开头缓冲区太小或采样太久加大buffer_size_kb或用 hist 聚合5.2 几条踩过之后才记住的教训第一条符号存在不等于能追踪。有次想追一个内核函数/proc/kallsyms里明明有但就是写不进set_ftrace_filter。后来才发现这个函数在编译时被内联进了调用者符号表里那条记录只是个残留实际代码里根本没有独立的函数入口。判断办法是去available_filter_functions里查这个文件列的是真正可以插桩的位置比 kallsyms 更权威。第二条set_graph_function不等于“只看这个函数”。它的语义是“从这个函数开始往下画整棵调用树”。如果你想排除某几个特别吵的子函数得配合set_graph_notrace来用。我一开始不知道这个抓出来的结果里一半是内存分配相关的调用把真正关心的逻辑淹没了。第三条tracing_on写 0 之后别忘了清缓冲。缓冲区里的数据不会因为关闭而消失下次打开时会接着往后写读出来的就是两次采样混在一起的内容。每次开始新的采样前echo trace这一步不能省。第四条事件 filter 里的字符串比较对空格敏感。进程名后面带空格的话prev_comm my-service是匹配不上的得写成带完整空格的形式。这种问题排查起来很费劲因为 filter 写错了不会报错只是静默地不匹配。第五条多实例能救命。有一次在一台共享机器上排查问题我开了 function_graph同事同时在用事件追踪两边的配置互相覆盖数据全是乱的。后来改用instances/各开一个问题立刻消失。现在我只要在共享环境里干活第一件事就是建实例。第六条kprobe 的字符串读取要小心越界。0(%si):string会从指针开始一直读到遇到空字符为止。如果那个指针指向的不是合法的以空字符结尾的字符串可能读到一大片乱码甚至触发内核的地址检查。稳妥的做法是加上长度限制比如0(%si):string配合%si的合法性判断或者改用0(%si):ustring并确认缓冲区大小。第七条别在容器里假设 tracefs 可用。容器默认往往不挂载 tracefs即使宿主机有容器内也看不到。要么在宿主机层面操作要么把 tracefs 以合适的权限挂进容器。这个坑在一次排查服务抖动时踩得很实在容器里ls /sys/kernel/tracing空空如也折腾了半小时才发现是挂载问题。第八条长时间追踪前先想清楚数据往哪去。ftrace 的缓冲区是环形的写满就覆盖。如果你打算跑十几分钟要么把缓冲区开到很大内存代价要么用 hist 做聚合要么用trace_pipe配合外部程序持续消费。指望“开半小时再来看”基本一定丢数据。再补一个实操上的小技巧把常用的追踪操作写成几个小脚本参数化函数名和采样时长。ftrace 的命令本身不复杂但顺序和清理步骤容易出错脚本化之后能省掉大量重复劳动也避免了“忘了关”导致的线上抖动。我自己维护了几个这样的脚本其中使用频率最高的是“给一个函数名抓 3 秒调用图并输出耗时最长的前 20 个调用区间”这个几乎每次排查都会先用它过一遍。工具本身不高级但胜在确定性——每次得到的都是同一套格式的数据横向对比起来非常方便。