Linux性能排查利器:从strace到bpftrace,一文讲透trace工具家族
2026/9/24 21:11:01 网站建设 项目流程

线上服务P99抖动到心慌,CPU、内存、IO看着都正常,这时候你会怎么办?如果第一反应只是打开top再盯一遍,那大概率还会盯着屏幕怀疑人生。我第一次遇到这个场景时,盯着监控面板看了一下午,最后是靠Linux trace性能分析工具才把问题挖出来——所谓trace,就是让系统自己把运行轨迹记录下来,精确到每一次系统调用、每一次内核函数调用、每一次线程被唤醒和抢占。它不是告诉你系统是否繁忙,而是告诉你系统到底在忙什么、哪些等待不合理。

这篇文章写给三类人:刚接触Linux性能排查、分不清strace和perf trace区别的入门者;已经在用top、vmstat、pidstat,但遇到疑难杂症总感觉隔靴搔痒的进阶用户;以及想系统了解trace工具家族、知道什么场景该掏出哪把刀的运维和开发同学。我会把几个主力工具的原理、用法、实测开销和踩坑经验一起讲透,最后用一个我实际排查过的线上案例串起来。

1. 用trace之前,先把工具家族谱系认全

1.1 “trace到底在trace什么”:一个理发店排队的故事

很多性能问题卡在“不知道程序在等什么”。常规性能工具看的是结果:CPU高不高、内存够不够、磁盘忙不忙。但“高”和“不够”背后是什么原因,这类工具给不出答案。

我习惯用一个理发店的例子解释trace。想象你观察一家理发店一整天的运营情况,top帮你统计出“下午3点客流最旺、店里平均5个人排队”,这是profiling,采样后算平均值和分布;但要搞清楚“为什么下午3点会堵”,你需要的是另一个角度的数据:每个顾客几点进门、几点坐下、洗完头几点起来、中间有没有因为等吹风机干等、有没有人插队——把每个时刻发生的事件按时间顺序记下来,这就是tracing。

Linux trace工具家族就是一套“事件记录设备”,它钩在用户态和内核态的各个关键位置,把程序运行过程中的系统调用、内核函数、信号、调度事件、用户态函数调用等统统记录下来。有了这份轨迹档案,你才能回答那些常规监控回答不了的问题:为什么一次写入耗时100ms?为什么线程明明可运行了却等了300ms才上CPU?为什么同一个函数调用,有时1us有时10ms?

1.2 六大主力工具分工:一张表认清谁管哪一段

Linux下叫trace的工具不少,但它们追踪的对象和场景差异很大,很多人一上来就被弄晕。我按自己实际使用的频率,把它们分成六类:

工具追踪对象典型场景侵入性上手成本
strace用户态程序发起的系统调用排查文件读写、网络、权限相关延迟很高(ptrace打断)
ltrace动态库函数调用定位用户态库函数级别问题
ftrace内核函数调用和内核事件内核时延、调度延迟、函数调用链极低
perf trace系统调用、内核事件、调度事件聚合统计+事件关联分析
bpftrace内核/用户态动态探针、tracepoint自定义指标、快速验证假设中高
LTTng内核+用户态事件的高精度记录长时间全量追踪、后置分析中低

这个表有个容易忽略的点:strace和perf trace追踪的对象部分重叠,但机制完全不同,这也是为什么很多人觉得“strace慢得没法用,perf trace就轻快很多”。后面的章节我会详细拆开讲。

另外提醒一句,其实“trace”这个词在不同领域意思差很多。前端常说的trace组件、分布式链路追踪,说的是跨服务的调用链追踪,跟Linux内核的trace工具完全是两码事。这篇文章只聚焦后者——在单机Linux上,追踪内核和进程状态的工具。

2. strace:排查用户态与内核态交互的第一步,但别拿它硬扛

2.1 一条命令看到程序的“一举一动”

strace可能是大多数人和trace工具的第一次相遇,因为它上手门槛最低:一句话就能把进程所有系统调用打出来。

strace -f -e trace=file,network,process -p 12345

-f表示跟踪子线程,-e trace=file,network,process只过滤文件、网络、进程相关调用,-p指定进程号。输出格式大概是这样的:

[pid 12345] 14:30:01.002345 openat(AT_FDCWD, "/etc/app/config.yaml", O_RDONLY) = 4 [pid 12345] 14:30:01.002356 read(4, "server:\n port: 8080\n", 512) = 22 [pid 12345] 14:30:01.002902 connect(3, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("10.0.0.5")}, 16) = -1 ETIMEDOUT (Connection timed out)

每一行都是一个系统调用的完整记录:调用名、参数、返回值。= -1 ETIMEDOUT直接告诉你连接超时了,= 4说明打开文件成功、fd是4。排查权限问题、配置文件找不到、网络连接失败这类问题,strace基本是首选。

有一点很多人不知道:strace不仅能看系统调用,还能看信号。比如进程被莫名kill掉,strace -e trace=signal -p PID能抓到信号来源(虽然是发送方进程,不是发起信号的用户级调用栈)。

2.2 strace为什么会让程序慢一个数量级

这里必须说清楚一个反直觉的事实:strace对性能的影响非常大,尤其是系统调用密集的程序。我实测过一个大量pread的Java进程,正常1秒能完成的批处理操作,挂上strace直接8秒以上,某些场景吞吐掉到十分之一。

原因在于strace的底层机制。它依赖ptrace系统调用,每次目标进程发起系统调用,内核都会把进程停下来,通知跟踪者“调用了XX”,跟踪者确认处理后放行;等系统调用结束返回,又得通知一次。进一次出一次,两次打断,程序在用户态和内核态之间反复横跳,开销可想而知。有同事跟我形容过:strace跑起来像给运动员脚上绑了两个沙袋,每一步都在跟空气搏斗。

所以在生产环境用strace,要记住三条经验:

  • -c做统计模式,而不是逐条记录。strace -c -p PID跑几十秒,最终只输出每种系统调用的次数、耗时总和、平均耗时,轻量很多。
  • 必须逐条跟踪时,用timeout 10 strace -o /tmp/trace.log -f -e trace=read,write -p PID这样的形式限时执行,输出重定向到文件,避免糊在终端上把shell卡死。
  • 永远不要长时间挂在一个繁忙进程上。strace适合“秒级取证”,不适合“长期监控”。

2.3 strace常用参数速查与组合技巧

参数作用我的惯用玩法
-f/-ff跟踪子进程 / 每个子进程单独输出到文件排查多进程服务必加-f
-tt显示微秒级时间戳看一次调用的精确耗时
-T显示每个系统调用的耗时-tt配合定位慢调用
-e trace=file,network,process,signal按类别过滤只跟踪关心的类别,降低干扰
-e trace=%file只跟踪文件相关调用查“找不到配置文件”最快
-s 200显示最多200字节的字符串内容默认32字节经常不够看
-y打印fd对应的路径看到read(4, ...)时直接显示fd 4是哪个文件
-o file输出到文件必用
-p PID附加到运行中进程线上排查主要靠它
-c汇总系统调用统计先看统计,再决定要不要细看

我排查慢IO的固定组合长这样:

strace -f -tt -T -y -e trace=read,write,fsync,fdatasync,openat -p <PID> -o /tmp/trace.log

跑完以后,用awk按耗时排序,几行就能找出最慢的系统调用:

awk '{print $NF, $0}' /tmp/trace.log | sort -rn | head -20

-T输出的耗时在每行末尾,直接按它排序就能定位“哪个调用慢得离谱”。这个思路比对着几千行输出肉眼翻高效得多。

3. ftrace:内核自带、零依赖,适合查调度和内核函数延迟

3.1 先找到ftrace的“总控台”

strace管的是用户态到内核态的边界,但很多时候问题发生在内核内部:一个内核函数调用慢,或者线程状态切换的时机不对。这种场景strace就使不上劲了,得请出ftrace。

ftrace最朴实的特点是不会骗人:它就在内核里,通过tracefs文件系统暴露控制接口,不需要装任何额外软件,很多容器镜像里它也能用(前提是host没有限制你的权限)。老版本路径在/sys/kernel/debug/tracing,新内核通常在/sys/kernel/tracing,两个路径可以兼容处理:

mount -t tracefs tracefs /sys/kernel/tracing 2>/dev/null || mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null cd /sys/kernel/tracing

关键文件就这几个,记住它们就掌握了ftrace的操作逻辑:

  • available_tracers:当前内核支持的跟踪器列表,常见的有functionfunction_graphsched_switch等。
  • current_tracer:当前启用哪个跟踪器,echo function > current_tracer即可切换。
  • available_filter_functions:所有可跟踪的内核函数列表(大几千个)。
  • set_ftrace_filter:设置要跟踪的函数,支持*通配符。
  • tracing_on:总开关,写1开启记录,写0停止。
  • tracetrace_pipe:读取跟踪结果,前者读完还在,后者边读边清空。

3.2 完整操作:抓一个内核函数的调用轨迹

要说清楚ftrace怎么用,我直接给一个我实际抓过多次的流程——跟踪文件打开的内核函数调用:

cd /sys/kernel/tracing # 先关掉记录,避免历史数据干扰 echo 0 > tracing_on # 清空已有缓冲 echo > trace # 启用function跟踪器 echo function > current_tracer # 只跟踪do_sys_openat2这个函数,其余不记录 echo 'do_sys_openat2' > set_ftrace_filter # 打开记录开关 echo 1 > tracing_on # 触发一次文件操作,比如在其他终端执行 cat /etc/hostname # 停掉记录 echo 0 > tracing_on # 查看结果 head -50 trace

输出大致长这样:

# tracer: function # TASK-PID CPU# TIMESTAMP FUNCTION # | | | | | cat-12345 [003] .... 20231.204082: do_sys_openat2 <- __x64_sys_openat

<‑前面的函数是被调用的目标,后面是调用它的上一级,合起来就是一条调用关系。如果需要看更完整的调用链,把跟踪器切成function_graph

echo function_graph > current_tracer echo 'do_sys_openat2' > set_ftrace_filter echo 3 > max_graph_depth # 只展开3层,防止输出爆炸

输出会变成缩进的函数调用树,每层函数进出都有时间戳,括号里的微秒数就是函数执行耗时:

1) | do_sys_openat2() { 1) 0.320 us | getname_flags(); 1) 0.130 us | get_unused_fd_flags(); 1) 2.510 us | do_filp_open(); 1) 3.000 us | }

这套东西最妙的点在于:它不依赖任何用户态调试工具,在一个最小化安装的Linux上也能用,非常适合做内核层问题的初判。

3.3 关键用法:ftrace查调度延迟和函数耗时分布

ftrace除了function tracer,还有一个我常用的wakeupsched_switch跟踪器。前者记录进程从被唤醒到真正上CPU的最长延迟,后者记录每个CPU上的线程切换序列。排查“线程明明ready了却迟迟不运行”这类调度问题,它们比perf更直观。

举一个实际用法。我想知道某个线程的调度延迟基本盘:

cd /sys/kernel/tracing echo wakeup > current_tracer echo 12345 > set_ftrace_pid # 只跟踪这个PID echo 1 > tracing_on sleep 5 cat trace

输出的wakeup_lat字段直接给出该进程最大唤醒延迟。我在有些共享型云主机上跑过,这个数字能到几十毫秒,瞬间就知道机器负载高、CPU争抢严重。

另一个容易踩的坑是:ftrace的function跟踪器会把每个匹配函数的每次调用都记下来,如果filter设置得太宽,比如echo 'schedule*' > set_ftrace_filter,记录量能在几秒内把缓冲区塞满,然后trace文件里全是截断的数据。我建议先点查几个可疑函数,或者配合set_ftrace_pid只跟踪目标进程,别一上来就全量捕获。

4. perf trace和bpftrace:把追踪能力提升到“事件级编程”

4.1 perf trace为什么比strace“轻”

perf trace和strace看到的东西有一大半重叠,它也能列出系统调用、时间戳、返回值,但实现机制完全不同。perf trace基于perf_event_open和内核跟踪点(tracepoint),不会像ptrace那样每次系统调用打断进程两次,所以开销低很多,适合做略微长一点的采样观察。

基本用法很接近:

perf trace -e openat,read,write,close -p 12345 -- sleep 5

它还能直接显示线程切换、signal、页错误等strace不太方便追踪的事件。我比较喜欢它的--summary模式,跑完自动汇总每种调用的次数和耗时,相当于strace-c的增强版:

perf trace -p 12345 --summary sleep 5

实际体验下来,perf trace的开销比strace低一个量级。对一个每秒几万次系统调用的进程,strace可能拖慢5到10倍,perf trace一般能控制在1.5倍以内。不过话说回来,perf trace对内核版本要求高一些,老内核上有些tracepoint可能不存在,建议在4.4+内核上使用。

4.2 bpftrace:一行脚本打天下

如果说ftrace是内窥镜、perf trace是显微镜,那bpftrace就是一把可编程的手术刀。它基于BPF技术,允许你用类awk语法编写小段脚本,挂载在内核跟踪点、kprobe、uprobe上,安全地在内核态做过滤、统计、聚合,然后只把精简结果返回用户态。

先感受一下它的一行脚本能做什么:

# 统计每个进程打开文件次数,按进程名聚合 bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }' # 实时打印新进程创建事件 bpftrace -e 'tracepoint:sched:sched_process_fork { printf("%s -> %s\n", comm, args->child_comm); }' # 统计各进程实际占用的CPU运行时间(ns),输出直方图 bpftrace -e 'tracepoint:sched:sched_stat_runtime { @[comm] = lhist(args->runtime, 1000, 100000, 10000); }'

第一个脚本的@[comm]表示用一个以进程名(comm)为键的map做累加,count()是计数函数。第二个脚本直接读args->child_comm,从tracepoint参数里取新进程名。第三个用lhist输出线性直方图,能看到每个进程的运行时间分布——这在分析CPU调度不均的时候特别直观。

对于“某个函数调用慢”这类假设验证,bpftrace更是利器。比如怀疑openat慢,可以用kprobe和kretprobe配对计算每次调用耗时:

bpftrace -e ' kprobe:do_sys_openat2 { @start[tid] = nsecs; } kretprobe:do_sys_openat2 /@start[tid]/ { @open_lat_ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'

这里的逻辑是:进函数时记下时间戳nsecs,出函数时算差值,hist输出耗时直方图。跑几百次操作后,就能看到耗时的中位数、尾延迟分布,而不是只能拍脑袋猜“好像有点慢”。

4.3 bpftrace的适用边界和我踩过的坑

bpftrace能做的远不止这些,它还能挂用户态uprobe、跟踪内核函数栈、打印数据结构里的指定字段。但也有几个现实约束:

  • 内核必须开启BPF支持,且部分老内核或云厂商裁剪内核上探针可能不全。用前先跑bpftrace -l | head看看探测点列表是否正常。
  • kprobe是基于函数名的,内核升级后函数名可能失效;tracepoint相对稳定,优先使用tracepoint是更稳妥的工程选择。
  • bpftrace脚本在繁忙系统上长时间跑也会带来额外开销,虽然比strace低很多,但不建议当成常驻监控进程。它更适合“发现问题后做定点取证”。

我现在的习惯是:先用perf trace --summary快速判断系统调用层有没有异常,再用bpftrace写一段针对性的统计脚本,把问题按进程、函数、耗时分布精确切出来。这套组合拳比单纯盯着strace日志高效太多。

5. 一个真实案例复盘:接口偶发超时,我是怎么用trace层层定位

5.1 现象:业务侧P99抖动,常规指标全部正常

某天线上服务反馈接口P99从正常20ms漂移到500ms,而且不是持续高,是偶发毛刺。我第一时间打开监控面板:CPU利用率30%多,内存充足,磁盘IO等待很低,网络流量也没有异常。top、vmstat、pidstat看了一圈,全是“正常”两个字。

这种场景最折磨人。常规监控告诉你一切正常,但业务端明明在超时。我当时的判断是:问题大概率不出在“资源不够”,而是出在“某个等待环节”。

5.2 第一层:strace确认业务自身没有阻塞

先用strace采样一下业务线程,目的不是看全部调用,而是验证“这个接口慢”到底是不是发生在read/write这些IO环节。

timeout 10 strace -f -tt -T -e trace=read,write,epoll_wait,futex -p <业务PID> -o /tmp/strace.log

结果出乎意料:在报慢的时间窗口内,线程从epoll_wait返回后,马上就能read到数据,read本身耗时不到0.1ms。也就是说,网络数据早就到了,但线程没有及时被唤醒——问题在更底层,业务系统调用本身并没有慢。

strace看的是“进程做了什么”,但它看不到“进程没被调度时发生了什么”。这一步只能排除业务阻塞,下一个怀疑对象变成了调度器。

5.3 第二层:perf sched还原调度时序

接下来用perf sched抓一段调度事件,看线程从“被唤醒”到“真正在CPU上运行”之间花了多少时间:

perf sched record -g -p <业务PID> -- sleep 10 perf sched latency --sort max

输出里有一列wakeup latency,代表任务从唤醒到获得CPU的平均/最大等待时间。我一看最大等待时间,直接飙到400多毫秒。这个数字对应上业务报的P99超时,基本对上了。

这说明一个问题:线程不是没就绪,而是就绪了却一直没有被调度到。为什么会这样?要么CPU被别的进程抢占,要么cgroup配额限了流,要么机器本身CPU steal严重。

5.4 第三层:bpftrace量化runqueue delay,锁定cgroup配额

perf sched已经定位到“调度延迟”,但还没回答“为什么延迟这么高”。这个阶段继续用perf sched也能查,但信息太杂。我改用bpftrace,在调度器相关探针上直接统计排队时间:

bpftrace -e ' tracepoint:sched:sched_wakeup { @wake_ts[args->pid] = nsecs; } tracepoint:sched:sched_switch { if (@wake_ts[args->prev_pid] != 0) { @runq_delay_us[args->prev_comm] = hist((nsecs - @wake_ts[args->prev_pid]) / 1000); delete(@wake_ts[args->prev_pid]); } }'

这段脚本的意思是:记录每个进程被唤醒的时间点,在下一次切换进来时,用当前时间减去唤醒时间,得到它在运行队列里排队等待的时长,并按进程名输出直方图。

跑了几分钟后,直方图显示大部分排队的进程都集中在某个容器组内,延迟中位数在几十毫秒,尾部在几百毫秒。我再去查容器cgroup的CPU配额,确认是cpu.max设置过小,业务突发流量把配额打满了,线程只能在runqueue里等着。问题根因不是代码、不是锁、更不是单机性能,而是共享计算资源的配额配置不合理。

5.5 修复与验证

把cgroup的CPU配额从2核扩到4核,同时和业务方沟通错峰任务时间,再跑同样的监控,P99回落到了25ms左右。整个排查过程用时一个下午,如果用穷举法调配置、看代码,可能几天都不一定有结果。trace工具在这里扮演的角色不是“锦上添花”,而是把问题从“业务层”一步步逼到“调度层”再到“资源配额层”,每一层都有数据支撑。

这个案例也让我形成了一条固定的排查路径:先strace排除用户态问题,再perf sched看调度时序,最后bpftrace量化关键路径指标。三级递进,很少落空。

6. 工具选型决策指南,和三条我踩出来的铁律

6.1 五步决策:现场到底该掏哪把刀

很多读者可能会问:strace、ftrace、perf trace、bpftrace各有擅长,到了故障现场怎么快速选?我总结了一个五步判断法,基本能覆盖大部分情况:

  1. 判断问题发生在用户态还是内核态。如果是“打开文件失败”“连接被拒”“读写超时”,先用strace;如果是“线程不执行”“进程D状态”“内核函数时延异常”,直接跳到ftrace或perf。
  2. 判断你需要“逐条事件”还是“聚合统计”。逐条事件看strace或perf trace;要按pid、函数、耗时做聚合分布,bpftrace更合适。
  3. 判断环境能不能装工具。装不了就跑ftrace,内核自带;能装就优先perf trace和bpftrace,查询能力上一个台阶。
  4. 判断时间窗口长短。strace适合秒级取证;perf trace可以撑几十分钟;要跑几小时全量记录,还是LTTng这类带缓冲和落盘的方案更稳。
  5. 判断要不要跟业务变量联动。比如“按请求URL统计打开文件次数”,这种关联用户态业务数据的场景,bpftrace动态追踪是唯一解。

下面这张表把常见场景对应到推荐工具,方便收藏:

故障现象第一选择备选
文件打开失败、权限错误straceperf trace
网络连接超时、read/write慢strace -e trace=networkperf trace
进程不响应、注册不上strace + gdbftrace sched_switch
内核函数时延异常ftrace function_graphperf ftrace
CPU调度延迟高perf schedbpftrace sched统计
用户态库函数调用可疑ltraceuprobe(bpftrace)
自定义指标统计、长尾耗时分布bpftraceperf record + script

6.2 生产环境使用trace的三条铁律

踩过几次坑之后,我给自己定了三条规矩,分享出来供大家参考。

第一,限时限量。线上环境任何trace操作都要带时限,timeout 10 strace ...-- sleep 5,目的就是防止忘记关导致服务被拖垮。ftrace的buffer也要控制大小,我通常先把buffer_size_kb调小到几百KB,够用就行。

第二,输出必须落文件。trace命令的原始输出量远超想象,不重定向时终端会被几十万行日志刷爆,造成额外负载。所有trace输出统一写到/tmp下,用完清理。

第三,先猜后证,别盲目抓包。trace工具是验证假设的,不是漫无目的扫描仪。上工具前先想清楚“我怀疑是哪个环节慢”“这个工具能不能证明或证伪这个假设”。带着问题抓trace,效率是瞎抓的十倍不止。

6.3 trace工具和线上监控体系的分工协作

最后说一个经常被误解的点:trace工具不是用来代替监控体系的,它们解决的是不同时间尺度的问题。

监控告警负责“发现在先”,7x24小时盯着CPU、内存、延迟、错误率,一旦超阈值就告警。trace工具负责“定性在后”,在收到告警后的几分钟内,上机器快速还原现场、定位根因。一个是体温计,一个是手术刀,完全可以共存。

我比较推荐的做法是:把trace工具接到一个便捷入口,比如在跳板机上放一套整理好的脚本集(strace的几个固定组合、perf sched采集、bpftrace统计模板),故障发生时一键执行,输出统一落盘。这样即使不是资深内核专家,也能按图索骥完成第一轮取证,大幅缩短平均恢复时间。

从我个人的实践经验看,trace工具学的不是“命令怎么敲”,而是一种排查思维:任何性能问题都不是凭空出现的,它一定对应着某个环节的等待或争抢,trace只是帮你把这个“等待”具象化。把这套思维练熟,再复杂的疑难杂症也有章可循。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询