上周线上有个服务接口偶发超时,日志里只能看到“上游组件超时”,CPU、内存全正常,看监控也找不到异常。我挂上 strace 抓了不到 10 分钟,就从系统调用时间戳里找到了真正的等待点。群里一个同事问了一句:“strace 还能这么用?”——我才意识到,不少人对于 strace 的认知还停留在strace -p PID然后盯着一屏刷屏输出发呆的阶段,根本不知道它还有过滤、统计、场景化组合这些高级玩法。
这一篇是第 17 篇的续篇。上一次已经把 strace 的基础参数和输出格式讲了一遍,今天不再重复基础知识,直接聊高级技巧和生产环境实战。内容会覆盖:怎么用过滤组合拳把噪音降到最低、怎么用统计模式快速排错,以及我在真实线上问题里用 strace 定位过的四类典型场景。适合已经接触过 strace 基础命令、想把排查效率真正提上去的读者。
1. 想用好 strace,先清楚它到底在进程上做了什么
1.1 ptrace 与系统调用插桩
strace 不是黑魔法,它基于 Linux 的 ptrace 系统调用实现。strace 启动后,会通过 PTRACE_SYSCALL 请求让内核在目标进程每次系统调用进入和退出时,把进程暂停下来,strace 趁机读取寄存器里的系统调用号、参数,记录返回值,然后让进程继续跑。
这意味着什么?意味着目标进程每调用一次系统调用,就要被“戳”两下。理解这一点非常重要,因为它直接解释了为什么 strace 的侵入性很强、为什么不能长时间挂在高频进程上。
做个生活化类比:strace 就像给快递站门口装了一个登记员,每个快递进出都要拦下来记录包裹信息、耗时、收件结果。对于快递量小的站点,登记员影响不大;但面对一天几万件包裹的仓库,这个登记员就成了瓶颈本身。
好消息是,绝大多数线上性能问题,最后都会落到某个具体系统调用上。read、write、open、connect、poll、futex,这些调用是用户态请求内核干活的唯一入口。应用层再怎么封装,慢的问题最终要体现在这些调用上。所以 strace 的价值,就是帮你把“接口很慢”这个大问题,翻译成“哪个系统调用、阻塞了多久、返回了什么错误”这样具体可查的小问题。
1.2 三种工作模式,对应三类不同问题
我平时用 strace 基本只有三种起手式,每种对应不同的排查场景。
第一种,跟踪一个执行中的命令。适合启动阶段的问题排查,比如服务启动时找不到配置文件、初始化连接失败、启动后立刻崩溃。用法很简单:
strace -f -o /tmp/boot.trace ./app服务启动完成或崩溃后,停掉 strace 翻日志就行。
第二种,附着到一个已经运行的进程。这是生产环境里最常用的方式,适合线上已经跑着的服务出现异常:
strace -p PID需要注意 attach 时机。如果问题已经发生完了再挂上去,很可能什么也抓不到——strace 只能看到挂上之后发生的系统调用。所以这种模式更适合问题还在持续发生、或者可以压测复现的场景。
第三种,跟踪全部线程和子进程。绝大多数服务都是多线程或多进程架构,只跟踪主进程往往看不到真正的业务线程在干什么。必须加-f参数,让 strace 跟随 fork、vfork、clone 出来的所有子线程和子进程:
strace -f -p PID如果线程实在太多,可以用-ff参数,让 strace 为每个进程/线程分别输出独立文件,避免所有线程的输出混在一个文件里没法看。
选择依据很简单:怀疑启动阶段问题就从头跟命令,怀疑运行期偶发问题就用-p附着,服务有大量 worker 子进程时千万记得加-f。
2. 真正提高效率的是过滤组合拳
2.1 用 -e 参数把跟踪范围缩到最小
很多人 strace 一挂上,屏幕上瞬间刷出几百行,然后就开始迷茫。问题的根源在于没有做范围收缩。strace 默认记录所有系统调用,但你在排查网络问题时根本不需要看 brk、mmap、close 这些噪音。
-e参数就是用来指定跟踪范围的。常用写法有几类:
# 只看文件相关操作 strace -e trace=file -p PID # 只看网络相关操作 strace -e trace=network -p PID # 只看描述符操作(read/write/close/lseek 等) strace -e trace=desc -p PID # 只看内存分配相关 strace -e trace=memory -p PID # 精确指定某几个系统调用 strace -e trace=openat,connect,read,write -p PID实际使用中,精确指定系统调用最常见。因为trace=file可能还是太宽,而trace=openat,connect,read,write这样点名道姓,输出量通常能降一个数量级。
还有一个小技巧:-e trace=%network,%desc这种写法可以一次性组合多个类别。在怀疑“网络相关调用 + IO 相关调用”同时有问题时非常方便。
2.2 用几个关键参数把输出变成“既短又有价值”
过滤掉调用类型之后,还有几个参数能进一步压缩输出、提升信息密度。
-P path只看指定路径相关的调用。比如我怀疑程序在反复读写某个日志文件,可以这样:
strace -e trace=openat,read,write -P /data/app.log -p PID配合-Z或-z只看成功或失败调用。这是一个容易被忽略但极其好用的参数:-Z只看失败的系统调用,-z只看成功的。排查“为什么连接失败”“为什么文件不存在”这类问题时,用-Z能把所有成功的噪音全部过滤掉,屏幕上剩下的全是错误点的errno返回码。
# 只看失败的系统调用,输出量瞬间大幅降低 strace -Z -e trace=network -p PID-y和-yy参数也很实用。它们会让 strace 打印文件描述符对应的具体路径或 socket 地址。比如看到read(12, ...)时你可能不知道 fd 12 是什么,加了-y就会显示为read(12</data/app.log>, ...),加了-yy还会显示 socket 对端 IP 和端口。
字符串截断问题也必须处理。strace 默认只显示参数中的前 32 个字节,遇到长路径、长参数、写 SQL 内容时经常截断到没法看。生产排查建议至少设置成 128:
strace -s 128 -p PID最后是输出重定向。不要直接在终端跑 strace,输出刷屏后很难回溯。用-o参数写入文件,需要时再 grep:
strace -f -tt -T -s 128 -o /tmp/trace.log -e trace=network,desc -p PID其中-tt打印带微秒的绝对时间戳,-T打印每次系统调用的耗时。这两个参数是定位“慢在哪一次调用”的关键,后面实战案例里还会用到。
2.3 统计模式:不关心过程,只想要“排行”
有时候你不想看每个调用的细节,只想快速知道这进程的系统调用画像。比如压测时 IO 高,你想知道是 read 多还是 write 多,还是 fsync 拖了后腿。这时候用统计模式:
timeout 30 strace -c -f -p PID跑 30 秒后,strace 会输出一张汇总表,统计每个系统调用被调用了多少次、总耗时多少秒、错误数多少、平均耗时多少。配合-S time可以按耗时排序,而不是默认按调用次数排序:
timeout 30 strace -c -S time -f -p PID我习惯这样来做第一轮排查。先用统计模式看“量”和“耗时排行”,锁定可疑调用,再换细节模式配合-e trace=具体调用抓单个调用现场。
之前遇到过一个压测场景,CPU 不高但磁盘 IO 跑满。我先跑了一轮strace -c -S time,结果清楚地显示 fsync 调用次数和总耗时高得惊人,而 read、write 反而占比不大。后续顺着 fsync 查下去,才发现是业务代码里每写几百字节就同步刷盘一次。这个结论如果不用统计模式,光靠肉眼翻输出根本不可能快速发现。
3. 生产环境最常见的四类实战场景
下面四个案例,都是以我在真实线上环境里排查过的经历为原型,进程名和组件名都做了脱敏处理,但操作步骤和判断思路完全可以照搬。
3.1 场景一:接口偶发超时,日志指向“上游超时”
当时某服务 A 每几分钟出现一次接口延迟,上游服务 B 的依赖方收到的响应时间飙到 5 秒,但服务 B 自己的监控面板看过去一切正常。
我先用统计模式跑了 1 分钟:
timeout 60 strace -c -f -S time -p `pgrep app_a | head -1`结果显示 poll 和 futex 两项耗时占比最高。poll 是网络等待的典型指标,futex 则可能和锁竞争有关。继续细化,只抓网络和线程同步相关调用:
timeout 120 strace -f -tt -T -s 128 -o /tmp/a.trace -e trace=poll,futex,connect,recvfrom,sendto -p PID从 trace 文件里看到,绝大多数请求的 poll 都在 1 毫秒内返回,但偶发的几个请求中,poll 等待事件长达 3 秒多。再结合-tt打出的时间戳,对比业务日志里的请求时间点,确认是服务 A 的所有线程都被某个慢请求占满后,后续请求在连接池里排队等待空闲连接。这个“排队等待”在系统调用层面表现为 poll 长时间阻塞。
这个案例里 strace 的核心价值是:把笼统的“上游超时”细化成了具体的“poll 等待事件”。如果只盯着业务日志,你永远不知道超时时间花在了连接池等待上。
3.2 场景二:连接被重置,服务端报 Connection reset
监控群里刷屏,服务 B 连外部网关时频繁报 Connection reset,业务方第一反应就是“中间链路有问题、网关故意断连”。
我直接用失败模式抓:
timeout 60 strace -f -Z -y -yy -e trace=connect,poll,recvfrom,read -o /tmp/b.z -p PID只看失败调用后,输出量少了很多。关键行大概是这样:
[pid 1234] connect(7</target:10.0.0.5:443>, ...) = -1 EINPROGRESS (Operation now in progress) [pid 1234] poll([{fd=7, events=POLLOUT}], 1, 5000) = 1 ([{fd=7, revents=POLLERR|POLLHUP}]) [pid 1234] recvfrom(7</target:10.0.0.5:443>, ...) = -1 ECONNRESET (Connection reset by peer)这里有几个关键判断点。connect 返回 EINPROGRESS 不代表失败,非阻塞 socket 的 connect 正常就是先返回 EINPROGRESS,然后靠 poll 等待结果。但 poll 返回的 revents 是 POLLERR|POLLHUP,说明对端状态异常。随后 recvfrom 返回 ECONNRESET,errno 是 104。
结合-yy显示的远端地址,再配合 tcpdump 抓包,最终定位到:服务 B 的连接池长期保持空闲连接,而网关侧的 idle timeout 较短,网关早就静默关闭了这条连接,服务 B 并不知道,继续拿着失效连接去发请求,于是触发 RST。
常见 errno 速查表:
| errno | 数值 | 常见含义 |
|---|---|---|
| EINPROGRESS | 115 | 非阻塞操作正在进行中,不算错误 |
| ECONNRESET | 104 | 对端主动关闭连接或链路被重置 |
| ETIMEDOUT | 110 | 连接或 IO 超时,通常对端不可达或防火墙丢弃 |
| EAGAIN | 11 | 资源暂时不可用,非阻塞 IO 下常见 |
| EADDRINUSE | 98 | 本地端口被占用,服务重启时常见 |
生产环境里遇到网络报错,别只盯着 ECONNRESET 两个字,配合-Z和-y把完整调用链拉出来,才能分清是发起方的问题还是对端的问题。
3.3 场景三:频繁小写与 fsync 导致的写放大
某服务正在压测,CPU 使用率不高,但磁盘 IO 占用率持续在 90% 以上。一开始怀疑磁盘有问题。
我先做了一轮统计:
timeout 30 strace -c -f -p PID汇总结果里,write 和 fsync 两个调用占据了绝大部分耗时。继续细化看每个请求的写模式:
timeout 60 strace -f -tt -T -e trace=write,fsync,fdatasync,openat -o /tmp/fs.trace -p PID从 trace 日志里能看到非常典型的写放大模式:业务代码每处理一条请求,就执行一次几百字节的 write,紧接着调用一次 fsync,单次 fsync 平均耗时 10-20 毫秒。在并发压测下,大量线程同时做这件事,磁盘 IO 自然瞬间被打爆。
证明问题的逻辑很简单:write 本身很快,但 fsync 要求数据真实落盘,每次都要等磁盘完成持久化,代价远高于 write。如果业务场景允许轻微丢数据(比如日志、非关键状态),就应该去掉 fsync,改成批量写入或定期刷盘。
strace 在这里的作用是给出铁证:先用量级统计证明 fsync 次数异常,再用细节模式证明每次写入量很小而 sync 开销很大。有了这些数据,跟开发团队沟通调优方向就有了充分的依据。
3.4 场景四:启动时读了“错误”的配置
某服务升级后行为异常,配置明明已经改了,程序却不生效。怀疑编译打包过程有问题,但一直没找到线索。
这种情况下,直接用 strace 从头跟踪启动过程最干净:
strace -f -e trace=openat,access,stat,read -o /tmp/boot.trace ./app启动完成后,从日志里过滤 openat 相关的调用:
grep openat /tmp/boot.trace很快看到了关键路径链:程序先尝试打开/etc/app/main.yaml,返回 ENOENT;再尝试./conf/app.yaml,也返回 ENOENT;最后成功读取了/opt/app/default.yaml。也就是说,程序走的是一套“找不到 A 就找 B,找不到 B 就回退到默认配置”的逻辑。配置没生效的原因,是环境变量里指定的配置路径不对,程序一直在用兜底配置运行。
这种配置回退问题,单看代码不一定能立刻发现问题,因为代码逻辑会告诉你“应该有默认值”。但 strace 会诚实地把每次尝试打开的真实路径和结果列出来,一眼就能看出程序实际读的是哪个文件。配合-Z只看失败,也可以快速列出哪些路径缺失:
grep ENOENT /tmp/boot.trace有时候排查问题的路径,比读代码更快。
4. 我踩过的坑和一句话避坑清单
4.1 几个真实“事故”
这些年我见过不少人在 strace 上栽跟头,自己也踩过坑,列出来给大家避雷。
第一个坑:挂太久。strace 是插桩式跟踪,不是采样式监控,它会对每个系统调用进行拦截和记录。挂在频繁系统调用的进程上,服务吞吐可能直接掉一半以上。有人把 strace 挂在线上进程上一跑就是一整天,业务方投诉性能下降,回头一看才发现是这个工具没停。现在我都习惯用timeout 30 strace ...这种方式,强制限制执行时长。
第二个坑:日志文件爆炸。多线程服务加-f后,输出量会指数级增长,-s 256再叠加-tt,每秒可能产生几十 MB 日志。曾经有个同事在 /tmp 下直接 nohup 跑 strace,回来发现磁盘被写满。记住三点:必须用-o指定文件、先df -h看磁盘剩余空间、能加-Z只看失败就优先加。
第三个坑:attach 权限问题。生产环境经常跑在容器里,并且 host 上开着 Yama LSM 保护,非父子进程之间 attach 会被拒绝,报错类似ptrace: Operation not permitted。这不是 strace 的问题,是权限限制。解决方式要么用 root 运行,要么调整 ptrace 相关内核参数。
第四个坑:忘记加-f。排查多进程服务时只 attach 到主进程,主进程 fork 出来的 worker 子进程一个都看不到,关键问题全漏掉了。我的习惯是:不确定服务是不是多线程/多进程时,直接加-f;输出太乱再用-ff拆文件。
第五个坑:只看调用名,不看返回值和 errno。strace 输出的精髓是对每一行调用后面的返回值和错误码做判断。比如 connect 返回 EINPROGRESS 不代表失败,read 返回 -1 时到底 errno 是 EAGAIN 还是 ECONNRESET,含义完全不同。养成查 errno 的习惯,排查速度能快一倍。
4.2 一套可复制的生产排查路径
我现在在线上做排查,基本遵循一条固定路径,分享出来供参考:
- 先做外围判断。用日志、监控、
perf top、iostat、sar等确认问题大致在哪个层面:磁盘 IO、网络、锁、还是进程异常。 - 尽量复现问题。偶发问题可以在压测环境先复现,复现不了的,准备好命令等待下一个问题窗口。
- 先跑一轮统计模式。
timeout 30 strace -c -S time -f -p PID拿系统调用排行,锁定可疑目标。 - 换定向细节模式。用
-e trace=具体调用类型加-Z(只看失败)或正常模式单独抓可疑点,带上-tt -T看时间戳和耗时。 - 把 strace 时间戳和业务日志时间点对齐。这一步能确认问题是否集中在特定请求上。
- 强制定时停止。不管有没有收获,到时间就停,恢复系统状态。
模板命令:
# 统计模式:先看排行 timeout 30 strace -c -f -p PID # 细节模式:只看失败 + 网络类调用,带时间和耗时 timeout 60 strace -f -tt -T -s 128 -Z -o /tmp/trace.err -e trace=network,desc -p PID这套流程的核心逻辑是先粗后细、先少后多、主动设限。不要一上来就动细节跟踪,那样容易被海量输出淹没。
5. strace 不够用的时候,你还可以试试这些
5.1 ltrace 和 gdb,负责更细的层次
strace 只能看到系统调用,但有些问题发生在用户态的库函数层面,比如 malloc、free、pthread_mutex_lock 这些调用是库函数,不是系统调用。这种时候 ltrace 更合适:
ltrace -p PIDltrace 跟踪的是动态库函数调用。排查锁问题、内存分配问题时,它的信息量比 strace 直接很多。
gdb 则可以进一步深入。catch syscall可以让你在某次指定系统调用发生时断下来,然后查看完整的用户态调用栈,搞清楚是代码里哪一行触发了这次调用。不过 gdb 会真正停住进程,不适合直接挂在线上高并发服务上,更适合线下复现问题后精细调试。
5.2 perf trace 和 bpftrace,低开销的现代方案
straces 最大的问题就是开销高。如果你很在意性能影响,或者问题发生在高频系统调用场景,可以换成基于采样的perf trace:
perf trace -p PID它的输出格式和 strace 类似,但基于 perf_event 采样机制,不会逐个拦截系统调用,开销小很多,适合长时间观察。
更进一步,bpftrace 可以针对内核 tracepoint 做定制统计。比如统计进程打开了哪些文件:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s\n", str(args->filename)); }'bpftrace 写起来比 strace 复杂,但它能做聚合统计、分布直方图,而且不会像 strace 那样显著影响业务,是生产环境长时间观测的进阶选择。
5.3 把它当成“排查链路”的一环
strace 不是万能的,它只是链路中的一环。我的实际使用感受是:问题排查需要把多个工具串起来,而不是寄希望于一个工具解决所有问题。
比如 CPU 占用高,应该先看 perf top 找热点函数,而不是 strace;磁盘 IO 高,先看 iostat 确认是读还是写,再用strace -c确认是哪个进程哪种调用模式;网络连接异常,先看 tcpdump 确认报文有没有到本机,再用 strace 看进程视角的连接状态。
strace 的强项是告诉你“进程在哪个系统调用上花了时间、返回了什么错误”,但需要进一步定位代码位置时,要配合 gdb 看用户态栈;需要确认报文是否到达时,要配合 tcpdump。工具之间互相印证,结论才站得住脚。
我现在工作里的使用习惯是:strace 一般不是第一个排查工具,而是做“确认”的工具。先把监控、日志、perf、iostat 过一遍,确定怀疑到某个进程的某个 IO,再短时间小范围 strace 一把。抓的时候尽量用-o输出到文件,带上-tt -T,并且记录业务日志时间点用于对齐。
最后分享一个小技巧:抓 trace 的过程中,在业务侧主动触发一次可复现的请求,然后以请求时间点为中心看 strace 前后 2 秒的调用序列,比漫无目的翻全部输出要高效得多。这个方法帮我快速定位过好几次偶发超时问题。还有一个小建议:如果你怀疑 DNS 或者配置文件读取有关的问题,优先用-e trace=openat,connect,sendto,recvfrom -Z,先把失败路径列出来再深挖。用完记得停,线上稳定比排查本身更重要。