排查一个“启动就挂、日志什么都没有”的服务时,能给出答案的往往不是日志,而是系统调用。strace命令完整详解这件事,本质上讲的是怎么把进程和内核之间的每一次对话录下来:它开了哪个文件、向谁发了包、卡在哪个调用上、返回了什么错误码。这套东西最适合三类人——写后端服务和守护进程的开发者、需要在一台陌生机器上定位问题的运维、以及想搞清楚“程序到底在干什么”的底层爱好者。它不要求你会写 C,但要求你愿意读一读系统调用名。下面这份内容按“原理—参数—场景—取舍—踩坑”的顺序铺开,参数部分会给出取值理由和取舍逻辑,场景部分会给出完整命令和输出解读,能直接抄去用。
1. strace 到底在跟踪什么
1.1 用户态与内核之间的那道门
一个普通程序想读写文件、建立连接、申请内存、创建线程,都不能自己动手,必须向内核发起系统调用(syscall)。这是用户态和内核态之间唯一的正规入口:程序把调用号放进寄存器,触发一条特殊指令,CPU 切到内核态,内核按调用号查表执行,再把返回值写回寄存器,切回用户态。整个过程对程序来说就是一次普通的函数调用,glibc 之类的运行库帮你把参数和寄存器摆好,你感受到的只是fopen、read、socket这些名字。
关键在于:这一层是透明但可观测的。程序崩了、卡了、读不到文件,绝大部分原因最终都会落在这道门上——要么参数传错了,要么权限不对,要么对方没响应。而应用层日志往往只记“我失败了”,不记“我怎么失败的”。strace干的事就是站在这道门旁边,把每一次进出都打印出来,包括调用名、参数、返回值、错误码、耗时。它不关心你的业务逻辑,只关心你和内核说了什么。
我常用一个类比:应用日志是餐厅服务员告诉你“菜没上”,strace是直接蹲在后厨门口,看厨师到底有没有接到单、食材有没有、灶有没有点着。前者告诉你结果,后者告诉你卡在哪一环。
1.2 ptrace 的代价与 strace 的三种工作方式
strace的实现依赖内核提供的ptrace机制。基本流程是:strace作为父进程启动目标程序(或者 attach 到一个已有进程),通过PTRACE_SYSCALL让目标在每次系统调用进入和返回时各暂停一次,暂停时读取寄存器拿到调用号和参数,打印出来,再放行。所以一次read在strace眼里是两次上下文切换加两次进程调度,这就是它慢的根本原因,跟日志写多少没关系,是机制决定的。
除了最基础的fork+ptrace模式,较新的strace还有几种工作方式值得知道:
- attach 模式:
-p PID挂到一个已经在跑的进程上,不需要重启服务。适合线上临时抓一把,缺点是会漏掉 attach 之前已经发生的调用。 - seccomp-bpf 加速模式:
-b参数指定某些系统调用走 seccomp 过滤器而不是每次都停,能显著降低开销,典型用法是-b execve,因为execve会重置进程状态,ptrace处理起来有坑,走 seccomp 更稳。 - 独立进程模式:
-D让strace不再作为目标进程的父进程,而是以兄弟进程的方式运行,减少对目标进程父子关系、信号处理的干扰。代价是启动阶段的少数调用可能抓不到。
理解这层实现,很多现象就顺了:为什么被跟踪的程序变慢了、为什么setuid程序跟踪不了、为什么容器里strace报Operation not permitted——都不是strace本身的 bug,是ptrace的权限模型和机制成本。
1.3 什么时候该想到 strace,什么时候别用
strace最擅长的场景有个共同特征:你知道进程有问题,但不知道问题在哪一层。具体说,下面这些情况我第一反应就是上strace:
- 程序启动即退出,日志只打了一行,甚至连一行都没有。
- 配置文件明明改了,程序行为没变,怀疑读的是另一个路径。
- 服务跑着跑着不响应了,进程还在,
top看着 CPU 不高,不知道卡在哪。 - 报“权限不足”,但你
ls -l看权限是对的。 - 网络连接失败,
ping通但业务不通,想确认到底连的哪个地址、报的什么错。
反过来,有些场景不要硬上strace:
- 性能剖析:想看函数级热点,用
perf更合适。strace的开销会把你本来想测的东西完全扭曲。 - 长时间在线监控:
strace的日志量和开销都不适合常驻。真要做持续观测,用基于内核 tracepoint 的方案,开销低两个数量级。 - 高频交易、实时性要求高的进程:一次
ptrace停顿就可能让业务超时,宁可复现问题在测试环境抓。 - 多线程重度程序的全量跟踪:
-f加全量输出,日志能用 GB 计,读起来也痛苦,得先过滤。
2. 装好它,然后跑通第一个跟踪
2.1 安装与版本差异
绝大多数发行版仓库里都有,包管理器直接装:apt 系apt install strace,dnf 系dnf install strace,最小化镜像里通常没预装,值得提前确认。装完先看版本:
strace -V版本差异比你想象的重要。-k打印调用栈需要编译时链接了 libunwind,很多发行版包默认没开,跑起来会提示不支持;-e inject是较新版本才有的故障注入能力,老版本只有-e fault;-y打印文件描述符对应路径在 4.x 之后才完善;-b加速参数在 5.x 之后语义有调整。如果你在排查一个和老系统相关的问题,先确认版本,别拿着新参数在老版本上试半天。
一个实用技巧:把strace -V和uname -r一起记下来。有些行为和内核版本相关,比如open在某些架构上被openat取代,你在输出里根本看不到open,不是程序没调用,是运行库走了新接口。
2.2 权限:ptrace_scope、capability 与容器
第一次用strace最容易撞的墙是权限。现象是:
strace -p 1234 strace: attach: ptrace(PTRACE_SEIZE, 1234): Operation not permitted原因通常有三个层面。第一层是Yama 的 ptrace_scope,路径在/proc/sys/kernel/yama/ptrace_scope,取值含义大致是:0 允许任意同用户 attach,1 只允许 attach 自己的子进程(很多发行版默认是这个),2 仅特权用户,3 完全禁止。临时放开:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope注意这是全局的、临时的,重启失效,生产机器上改完记得改回去。
第二层是用户身份。目标进程属主和你当前用户不一致,普通用户 attach 不了,得有相应特权。sudo strace -p能解决大部分情况,但要清楚这等于给了你读该进程全部内存的能力,操作上要谨慎。
第三层是容器。默认的容器安全配置会拦掉ptrace,需要在启动时显式加能力,大致是--cap-add=SYS_PTRACE这一类;如果容器还叠加了更严格的系统调用过滤,可能还需要放宽。这是容器里跑诊断工具的通用问题,不是strace独有。我的建议是:诊断镜像单独做一个,别在生产容器里长期挂这些能力。
注意:临时打开
ptrace_scope属于降低系统防护等级的操作,做完立刻恢复原值,别让它一直留着。
2.3 从一条最简单的命令看懂输出格式
跑一个最小的例子,输出结构就清楚了:
strace -f -s 128 -o /tmp/trace.log /bin/ls /tmp打开日志,前面几行长这样(路径和地址每次不同):
execve("/bin/ls", ["ls", "/tmp"], 0x7ffd... /* 24 vars */) = 0 brk(NULL) = 0x55d1c0e2a000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=38142, ...}) = 0 mmap(NULL, 38142, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8c... close(3) = 0 ... write(1, "a.txt b.log\n", 13) = 13 exit_group(0) = ?读这种输出有几个固定套路:
= 0是成功,= -1是失败,后面跟errno名字和人类可读解释。= ?表示调用没有返回,通常是进程退出或者被信号杀掉,exit_group就是典型。ENOENT不一定是错误,上面access("/etc/ld.so.preload")返回ENOENT是正常的,运行库在探测这个可选文件。判断“是不是真问题”要看上下文。- 参数里的指针用十六进制显示,
0x7ffd...这种你不用管,它是指向内存的地址;-v能让结构体不被缩写。 -f之后每行前面会带[pid 1234]前缀,多进程混杂的时候注意看 pid 变化。-y之后 fd 后面会挂路径,比如= 3</etc/ld.so.cache>,这个对排查“到底打开了哪个文件”极其有用。
3. 参数全景:把常用参数分成六组来记
strace参数很多,硬背没意义。我习惯按“跟谁、看什么、什么时候、怎么过滤、怎么统计、怎么兜底”六组来记,用的时候按组去找,比翻 man 快。
3.1 选择跟踪对象:-p、-f、-ff、-o
这一组决定“跟踪谁、输出到哪”。
-p PID是 attach 已有进程,最常用的线上手法。可以跟多个-p,也可以-p $(pgrep -d, 名字)一次挂一窝。它的局限是只能看到 attach 之后的调用,程序启动阶段的动态库加载、配置读取全部错过。所以如果是“启动失败”这类问题,宁可重启一次用strace 命令直接拉起。
-f跟踪子进程和线程。没有它,主进程fork出来的 worker 你一个都看不见,而恰恰是 worker 在干活。加-f之后输出会混在一起,靠 pid 前缀区分。
-ff是-f的加强版,每个进程单独写一个文件,文件名是-o指定的路径加.PID后缀。多线程程序用这个,比在一个文件里翻找 pid 高效得多。典型命令:
strace -f -ff -o /tmp/tr -s 256 ./service # 生成 /tmp/tr.1001 /tmp/tr.1002 ...-o 文件把输出写文件而不是 stderr。别小看这个,全量跟踪的输出可能几十上百 MB,直接刷屏会把你的终端卡住,也会因为终端缓冲区满而反压到被跟踪进程,让它更慢。
3.2 控制输出详略:-s、-v、-x/-xx、-y/-yy、-a、-q
这一组决定“每行打多细”。
-s控制字符串最大打印长度,默认只有 32 字节,这是新手最常踩的坑:明明抓到了openat,路径却被截断成/etc/very/long/path/conf…,看不出是哪个文件。常用取值 128 到 1024,看你的路径和协议缓冲区多长。全量跟踪加-s 4096会让日志膨胀得很快,第 5 章会算这笔账。
-v让结构体不缩写。默认stat结构体会打成{st_mode=S_IFREG|0644, st_size=38142, ...},最后那个...省略了一堆字段,-v全部展开。排查时间戳、inode、设备号相关问题时需要它。
-x把非 ASCII 字符串按十六进制打印,-xx连 ASCII 也十六进制。处理二进制协议、带非 UTF-8 字符的路径(比如中文文件名乱码)时很有用,能看出字节到底是什么。
-y在文件描述符后打印对应路径,-yy进一步打印 socket 的对端地址、设备号等。这条我强烈建议常用,它把“read(3)到底读的哪个文件”这个最常见的问题直接回答了:
strace -y -e trace=read,write -p 1234 read(3</var/log/app/current.log>, "2024-05-11 ...", 8192) = 1024-a 列数调整对齐位置。默认对齐在 40 列,参数特别长的时候会把=挤到很后面,看着难受,可以调大。
-q抑制 attach、detach 这类提示行,-qq连进程退出的提示也压掉。写脚本解析输出时用得上,纯粹的样例输出里这些行容易干扰。
3.3 时间维度:-t、-tt、-ttt、-r、-T、-w
时间参数是我认为strace最有价值的部分之一,比很多人以为的重要。
-t每行加秒级时间戳,-tt加微秒,-ttt用 Unix 时间戳(机器好解析)。-r打相对时间戳,显示距上次调用过了多久,排查“这段卡了多久”时极其直观:
strace -r -e trace=openat,read ./app 0.000000 execve("./app", ["./app"], ...) = 0 0.000412 openat(AT_FDCWD, "/etc/app.conf", O_RDONLY) = 3 2.531004 read(3, "timeout=30\n", 8192) = 11两秒半才返回,问题就很明显了。
-T在行尾加<耗时>,单次调用耗时一目了然,适合找哪一次调用慢。
-c统计模式下再加-w,统计的是墙钟时间而不是默认的时间口径。这点特别容易被忽略:一个read阻塞了 30 秒等数据,默认统计里它可能只占很小的时间份额(因为它没消耗 CPU),但-w会把它算出来。排查“CPU 不高但很慢”的问题,-c -w组合是首选。
提醒:不同版本的统计口径实现有细微差别,拿它做横向对比时,尽量用同一台机器、同一个版本,避免结论被口径差异带偏。
3.4 过滤:-e trace=、-P、-e read/write、-e inject
过滤是让strace从“玩具”变成“工具”的关键。不过滤的输出里 80% 是mmap、mprotect、brk这些内存管理调用,噪音极大。
-e trace=是最常用的。可以写具体的系统调用名,用逗号分隔;也可以用分类通配:
| 分类写法 | 覆盖范围 | 典型用途 |
|---|---|---|
-e trace=%file | 所有文件名参数的系统调用 | 查文件路径、配置加载 |
-e trace=%desc | 文件描述符操作(read/write/close/dup 等) | 查 IO 行为 |
-e trace=%network | socket/connect/sendto/recvfrom 等 | 查网络连接 |
-e trace=%process | fork/clone/execve/wait 等 | 查进程创建与退出 |
-e trace=%signal | 信号相关 | 查被谁发的信号打断 |
-e trace=%ipc | 消息队列、共享内存、信号量 | 查进程间通信 |
-e trace=%memory | mmap/brk/munmap | 查内存映射与段错误关联 |
-e trace=%stat | stat 一族 | 查文件状态检查 |
支持取反,比如-e trace=!read,write,mmap排除这三类,或者-e trace=%file,!openat在文件类里再排除一个。
-P 路径只跟踪访问指定路径的系统调用,写完整路径,多个-P叠加。追查“到底谁在动这个文件”时,-P /etc/app.conf比全量过滤省事得多,输出可以少到十几行。
-e read=fd和-e write=fd会把指定描述符上读写的数据按十六进制 dump 出来。这个能力很少人用,但排查协议问题非常香:配合-y先确认 fd 是哪个 socket,再 dump 数据,能把“发出去的东西和我以为的不一样”这类问题钉死。
-e inject=是故障注入,可以让某个系统调用在指定次数时返回指定错误,用来验证程序的错误处理分支:
strace -e inject=openat:error=ENOENT:when=3 -o /tmp/inj.log ./app意思是第 3 次openat时伪造ENOENT。这比改代码打包测试靠谱得多,测“配置文件读失败时程序会不会崩”这种分支尤其方便。老版本对应的是-e fault=,语法略有差异,注意区分。
3.5 统计模式:-c、-C、-S、-U
-c不打印每次调用,只在结束时给一张汇总表:
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 62.31 0.482100 40 12040 read 21.05 0.162900 27 6032 write 8.44 0.065300 65 1004 12 openat 4.20 0.032500 32 1008 close ------ ----------- ----------- --------- --------- ---------------- 100.00 0.773800 20084 12 total这张表要会读。calls是调用次数,errors是失败次数,seconds是总耗时,usecs/call是单次均耗时。几个判读要点:
read/write占比高很常见,未必是问题,先看usecs/call是不是异常。openat的errors数量是很好的健康度指标,正常程序会有一些ENOENT(探测可选路径),但几百次失败通常意味着路径配置有问题。futex次数极高往往说明锁竞争激烈,多线程服务里这条值得盯。poll/epoll_wait的usecs/call很高说明在等 IO,属于阻塞型等待,-w模式下才能看清真实占比。
-C是-c的补充,统计表和完整日志同时输出。适合既要概览又要细节的场景,代价是日志体积翻倍。
-S 排序字段改排序方式,默认按时间排,可以改成-S calls按次数排,或者-S name按名字排。找“哪个调用最频繁”时按次数排更直接。
-U 列名在老版本里控制汇总表显示哪些列,新版本表结构固定了,用得少。
3.6 几个容易被忽略但很好用的:-k、-z/-Z、-i、-E、-u、-D、-b、-d
-k打印每个系统调用的用户态调用栈,能直接告诉你这次openat是代码里哪一行触发的。前提是编译时支持,跑不通会提示不支持。对有符号的二进制,输出大致是:
openat(AT_FDCWD, "/etc/app.conf", O_RDONLY) = 3 > /usr/lib/libc.so.6(__libc_open64+0x1b) [0x7f...] > ./app(load_config+0x3a) [0x55...] > ./app(main+0x1a) [0x55...]有它定位能省一大截时间,缺点是开销进一步上升,且对静态链接、strip 过的二进制基本没用。
-z只打印成功的调用,-Z只打印失败的。排查“为什么失败”的时候,-Z直接把噪音砍掉九成:
strace -Z -e trace=%file ./app-i打印系统调用指令指针地址,做崩溃分析时能对上具体位置。-E VAR=VAL给目标进程设置环境变量,等价于在命令行前加VAR=VAL,但更灵活,因为可以对 attach 的场景生效(部分版本支持)。-u 用户名以指定用户身份运行命令,测权限相关问题时不用先切用户。
-D让strace不作为目标的父进程,减少干扰,前面提过。-b指定走 seccomp 加速的系统调用。-d打印strace自身的调试信息,一般用不上,但遇到“strace本身行为异常”时可以开一下看看它在干什么。
4. 五类真实排障场景的完整走法
4.1 启动即退出:从 execve 开始倒着看
服务启动就退出、日志空白,是最典型的strace场景。命令这样写:
strace -f -s 256 -o /tmp/start.log ./service --config /etc/service.conf echo "exit=$?"然后从日志最后往前看,因为退出原因通常出现在末尾:
tail -50 /tmp/start.log几种常见结尾和解读:
- 结尾是
openat(... "/etc/service.conf", ...) = -1 ENOENT:配置文件路径不对,或者程序用了相对路径而工作目录和你以为的不一样。后面通常会跟write(2, "config not found\n", ...),日志就这一行,对上了。 - 结尾是
exit_group(1),前面没有任何write(2, ...):程序自己判断某条件不满足直接退了,连日志都没打。往前找最后一个非常规调用,往往是stat、access、getuid之类的检查。 - 结尾是
--- SIGSEGV {si_signo=SIGSEGV, ...} ---:段错误。结合-k或者直接上 gdb 看栈。 - 结尾是
execve("/usr/bin/env", ...) = -1 EACCES:包装脚本的权限或 shebang 有问题。 - 压根没有
execve:说明命令本身没被执行,检查路径、权限、shell 层是否拦截。
我一般还会补一条-e trace=%process,%file的过滤版命令,输出短,肉眼能扫完:
strace -f -e trace=%process,%file -s 256 ./service 2>&1 | tail -404.2 权限与文件:EACCES、ENOENT 的细微区别
报“权限不足”但ls -l看着对,这时候区分EACCES和ENOENT特别重要。
ENOENT是“文件不存在”,包括路径中间某一级目录不存在。EACCES是“存在但没权限”,包括目录没有执行权限(x),因为访问目录下的文件需要对目录有x权限,这个点很多人会忽略。还有一种情况是路径上某一级目录不可穿越,返回的也是EACCES。
排查命令:
strace -f -Z -e trace=%file -s 512 -y ./app 2>&1 | head -60-Z只看失败,-y看 fd,-s 512保证路径完整。看到失败的那一行之后,直接拿路径去namei -l /path/to/file逐级检查权限,比盲猜快得多。
还有一类隐蔽问题:程序读到了配置文件,但读的是另一个路径下的同名文件。典型原因是工作目录不同、容器挂载覆盖、或者环境变量指向了别处。用-e trace=%file加-y抓一遍,把实际打开的路径列出来去重,问题立刻现形:
strace -f -e trace=%file -y ./app 2>&1 | grep -o '"[^"]*"' | sort -u4.3 卡死不动:看它在哪个系统调用上睡觉
进程还在,CPU 不高,请求不返回。这种先确认它当前停在哪:
cat /proc/PID/syscall cat /proc/PID/wchan cat /proc/PID/status | grep -E 'State|Threads'/proc/PID/syscall第一列是系统调用号,后面是参数,如果显示的是read、futex、epoll_wait、accept这类阻塞调用,基本能定性。不过这只是快照,要连续观察用strace -p:
strace -p PID -T -tt -e trace=%desc,%network,%ipc几种典型的“卡住”及其含义:
| 卡在 | 可能含义 | 下一步 |
|---|---|---|
futex(...) | 等锁、等条件变量、线程池空闲 | 看是否有其他线程持锁不放,结合-f全局看 |
read(0, ...) | 等标准输入 | 程序交互式等待,检查启动脚本是否漏了重定向 |
read(fd)某个 socket | 等对端数据 | 用-y/-yy看对端地址,确认服务端是否回了 |
epoll_wait | 事件循环空闲 | 正常状态,问题可能在别的线程 |
accept | 等新连接 | 正常,说明前面没请求进来 |
connect | 正在建连 | 看目标地址是否可达、是否被防火墙丢包 |
nanosleep | 主动睡眠 | 看代码里的重试逻辑,可能是退避等待 |
wait4 | 等子进程 | 子进程挂了或卡了,顺着 pid 往下追 |
poll带超时 | 等 IO 事件 | 结合-T看单次等待时长 |
有个细节:straceattach 上去之后,目标进程会短暂停顿,对超时敏感的服务要小心。attach 前后各看一眼/proc/PID/status的voluntary_ctxt_switches,能大致判断它是不是频繁被唤醒。
4.4 性能问题:先用 -c 找热点,再定点放大
“慢”这个问题,strace的正确用法是两段式:先用统计模式定位方向,再用过滤模式看细节。
第一段,统计:
strace -c -w -f -o /tmp/stat.log ./app cat /tmp/stat.log重点看三样:usecs/call特别大的、calls特别多的、errors特别多的。有了方向再进第二段。
第二段,定点:
# 假设统计显示 openat 次数异常多 strace -f -e trace=%file -c ./app # 假设显示某个 read 单次耗时很长 strace -f -e trace=read -T -tt -p PID举个我实际遇到过的形态:统计表里futex的calls占了总量的四成,usecs/call也不低。这说明大量时间花在锁上,继续用-f看是哪个线程在抢,再结合代码里的锁粒度调整。另一常见形态是openat每秒几千次,多半是配置或模板文件被反复打开,加个缓存就能降一个量级。
要提醒的是,strace的统计不能当性能剖析的最终结论。它的采样本身有偏差,开销也会放大阻塞型调用。它的定位是“指方向”,真要量化还得靠内核态采样工具。
4.5 多线程与多进程:-f 之后的输出怎么读
多进程场景不加-f等于白抓,加了之后输出会乱,得有方法读。
单文件模式(-f -o)下,每行前缀是[pid 1234]。读的时候先按 pid 分组:
grep -o '^\[pid *[0-9]*\]' /tmp/trace.log | sort | uniq -c | sort -rn哪个 pid 行数最多,它就是主力,重点看它。如果主进程行数很少,说明活都在子进程,问题不在主进程。
多文件模式(-f -ff -o /tmp/tr)更清爽,直接按文件看:
ls -S /tmp/tr.* | head # 最大的那个文件就是话最多的进程追一条完整链路时,进程创建是起点:
strace -f -e trace=%process -s 256 ./app 2>&1 | grep -E 'clone|fork|execve|exit'从中能看到主进程clone出几个 worker、每个 worker 的 pid、execve了什么、什么时候exit。如果某个 worker 起来没几秒就退出,看它退出前最后的系统调用,基本能定位到初始化失败的那一步。线程卡住的情况,配合-T看哪个线程的futex等待时间最长。
5. 参数怎么算:输出体积、性能开销与取值的取舍
这一节讲讲“为什么这么取参数”,因为很多人抄了命令但不知道为什么,换个场景就不灵了。
5.1 -s 的取值与日志体积估算
-s默认 32 字节,这个默认值在几十年前够用,现在明显偏小,一个稍长的路径就截断了。但也不能无脑设 4096。
日志体积可以用一个粗算公式估:
总字节 ≈ 调用总次数 × (行固定开销 + 参数平均字节数)行固定开销包括时间戳、系统调用名、返回值、对齐空格,大约 60 到 100 字节。参数部分取决于调用类型。
举个例子:某进程跑 60 秒,-c统计到 200 万次系统调用。假设平均每行 110 字节,总日志约 220MB。如果其中有 20% 是带缓冲区的调用(read/write/sendto),-s从 32 提到 256,每次多打印 224 字节,增量就是:
200万 × 20% × 224 字节 ≈ 90MB加上去就是 300MB 往上。如果再加-f多线程,行数还要乘。所以在磁盘紧张或者要长时间抓的场景,-s的取值要算一下,不要顺手写个 4096。
我的经验取值:
| 场景 | 建议 -s | 理由 |
|---|---|---|
| 只看调用序列和返回值 | 32 到 64 | 不关心内容,越小越好 |
| 查文件路径 | 256 | 覆盖绝大多数路径长度 |
| 查配置内容、HTTP 头 | 1024 | 头部和配置行通常在几百字节内 |
| 查二进制协议 | 1024 到 4096,配合-x | 需要看完整载荷 |
| 长时间抓取 | 64 到 128,配合-e trace=过滤 | 用过滤换体积,别靠截断 |
5.2 开销从哪儿来,怎么压到可接受
strace的开销来源有四个,逐条说:
第一,ptrace 停顿。每次系统调用停两次,进程调度、上下文切换都要钱。这是主要成本,量级上会让 IO 密集型程序慢 10 到 100 倍,具体倍数看系统调用密度。一个每秒几万次调用的服务,挂上strace之后吞吐可能掉到十分之一。
第二,输出格式化。把参数转成字符串、写文件,都是strace进程自己在做,会占一个核。用-o写本地磁盘比写管道到终端好,写终端会因为终端刷新慢而反压。
第三,日志 IO。高频输出本身消耗磁盘带宽。写到 tmpfs 上能缓解,但要注意内存占用。
第四,缓存失效。被跟踪进程的执行被打断,CPU 缓存局部性变差,这部分很难量化,但实际存在。
压缩开销的优先级排序,我的建议是这样:
- 先过滤,
-e trace=和-P把范围缩到最小,这一步收益最大。 - 能用统计就不用全量,
-c只记计数不打印每行,开销显著低于全量。 - 能用
-b就用,指定 seccomp 加速的系统调用,减少停顿次数。 - 缩短抓取窗口,抓 10 秒比抓 10 分钟安全得多,先抓短窗口看趋势。
- 避开业务高峰,必要时在低峰复现。
- 别在生产主节点上长时间挂,宁可摘一台流量抓。
一个底线原则:任何 attach 操作前,先想清楚“如果不小心挂住 30 秒会不会出事”。会出事就别在线上做,去测试环境复现。
5.3 一次规范的生产环境抓取流程
把上面的东西串成一个可复用的流程,我在实际环境里大致是这么走的:
第一步,确认对象和权限。ps -ef | grep 名字拿到 pid,确认属主,确认ptrace_scope和容器能力是否允许。
第二步,先做无害观察。不 attach,先看/proc/PID/syscall、/proc/PID/status、/proc/PID/wchan,几十毫秒的事,能排除掉一半可能性。
第三步,短窗口统计。用timeout包一层,强制限时:
timeout 10 strace -c -w -f -p PID -o /tmp/stat.log10 秒后自动 detach,不影响业务。
第四步,按方向过滤抓全量。
timeout 15 strace -f -ff -tt -T -s 256 -y \ -e trace=%file,%network -p PID -o /tmp/tr第五步,离线分析。把/tmp/tr.*拉回本地,按 pid 分组、按错误码筛、按耗时排序,慢慢看。分析在本地做,不占生产资源。
第六步,清理。删日志,恢复ptrace_scope,移除临时的容器能力。
这套流程的核心思想是:把“观察”和“分析”分开。抓的时候尽量短、尽量窄,分析的时候再展开。很多人反过来,抓的时候全量挂十分钟,分析的时候发现日志几百 MB 打不开,两头都难受。
6. 常见问题与坑位实录
6.1 问题速查表
下面这张表是我这些年遇到频率最高的问题,按现象查:
| 现象 | 常见原因 | 处理手法 |
|---|---|---|
Operation not permitted | ptrace_scope、用户不匹配、容器缺能力 | 见 2.2,临时调 scope 或加能力 |
| 完全没输出,命令立刻返回 | 目标程序没被执行(路径/权限),或execve就失败了 | 加-f,看第一条是不是execve |
| 只有几行就结束 | 程序很快退出,真正的问题在退出前 | 从日志末尾往前读,-Z只看失败 |
路径被截断成... | -s默认 32 太小 | -s 256起步 |
| 看不到子进程 | 少了-f | 加-f,多进程用-ff |
输出里完全没有open | 运行库走的是openat | 用-e trace=%file而不是指定open |
| 日志把磁盘写满 | 全量 + 大-s+ 长时间 | 见 5.1 估算,加过滤和timeout |
| attach 后目标卡住不动 | 目标在等某个锁或 IO,strace只是放大了 | -T -tt看是不是某次调用超长 |
| 输出乱序、看不出因果 | 多线程混杂 | -ff分文件,或先 grep 出单个 pid |
-k报不支持 | 编译时没链接 libunwind | 换发行版的包,或放弃栈信息 |
日志里ENOENT一大堆 | 探测可选路径属正常,也可能是配置错 | 看路径是否重复出现且是业务相关文件 |
EMFILE出现在openat | 文件描述符泄漏 | -c看openat和close次数是否失衡 |
connect返回EINPROGRESS后无下文 | 非阻塞连接,后续poll超时 | 用-y看目标地址,确认网络可达 |
被EINTR打断 | 有信号进来 | -e trace=%signal看是哪个信号 |
| 统计表里耗时都很小但业务慢 | 默认口径不含阻塞等待 | 加-w用墙钟时间统计 |
表格里最后一条值得单独强调。很多次有人拿着-c的结果说“系统调用都很正常”,但业务就是慢。原因是默认统计口径对阻塞型调用的体现不直观,read阻塞 5 秒可能只显示 0.001 秒。加上-w之后数字完全不一样,问题立刻现形。这是我认为strace里最容易被忽略的一个参数。
6.2 和 ltrace、perf、bpftrace、gdb 的边界
工具选错,时间翻倍。这几个的定位我列一下:
| 工具 | 观察层次 | 开销 | 最适合的问题 |
|---|---|---|---|
strace | 系统调用 + 信号 | 高 | 文件、网络、权限、阻塞、启动失败 |
ltrace | 动态库函数调用 | 中到高 | 看调了哪个库函数、参数是什么 |
perf | CPU 采样、调用栈 | 低 | 性能热点、火焰图、CPU 占用高 |
| 内核 tracepoint 方案 | 内核事件 | 很低 | 生产环境长时观测、聚合统计 |
gdb | 指令级、单步 | 极高 | 需要精确复现、改内存、看变量 |
/proc快照 | 当前状态 | 极低 | 快速确认“现在停在哪” |
几个边界判断的经验:
- 不知道问题在库层还是系统调用层,先
strace,因为系统调用层更靠下,覆盖面更广。 - CPU 高但系统调用很少,别用
strace,用perf看用户态热点。 - 已经定位到具体系统调用,想看是谁调的,
strace -k或者上 gdb。 - 要在生产长期观测,别用
strace,用基于 tracepoint 的方案,开销差两个数量级。 strace输出里看到大量mmap/brk,说明你的过滤没做好,先用-e trace=收窄,别硬读。
另外一个常被忽略的入口是/proc/PID/syscall和/proc/PID/stack(后者需要特权)。前者给你“此刻正在执行哪个系统调用”,后者给你内核态栈。两个都不用 attach,零开销,遇到“进程不动了”先用它们探一下,再决定要不要上strace。
6.3 我踩过的那几个坑
坑一:以为strace慢是日志写多了。早期抓一个服务的问题,输出重定向到文件,还是慢得离谱。后来才想明白,慢的是 ptrace 停顿,跟打印多少无关。所以“少打点日志就能快”这个直觉是错的,能减开销的是减少停顿次数,也就是用-b、用-c、缩小过滤范围。
坑二:拿-c的耗时当性能结论。前面说过,默认口径下阻塞等待体现不明显。我吃过一次亏,统计表显示read总耗时 0.02 秒,判断“读文件没问题”,结果加了-w之后是 40 多秒,全卡在等网络数据上。从那以后,凡是排查“慢”,-c必带-w。
坑三:attach 上去把服务挂超时了。一次在业务节点上直接strace -f -p,没加timeout,输出还打到了终端。终端刷新慢,管道缓冲区一满,被跟踪进程跟着停,上游直接超时告警。教训很直接:永远用-o写文件,永远用timeout限时,永远先想清楚最坏情况。
坑四:-e trace=open什么都抓不到。折腾半天,最后发现现代运行库用的是openat。系统调用名在不同架构、不同 libc 版本上会变,别写死单个名字,用%file这类分类,兼容性最好。
坑五:多线程程序只抓了主进程。一个多进程服务,主进程负责监听,worker 干活。只 attach 主进程,看到的就是一堆epoll_wait,什么问题都没有。加上-f之后才看到某个 worker 卡在futex上。所以看到主进程很“安静”时,第一反应应该是“活是不是在别人身上”,而不是“没抓到”。
坑六:以为ENOENT一定是错误。大量access、openat返回ENOENT是运行库在探测可选路径,属于正常预热。判断标准是:这个路径是否与你的业务相关、是否每次启动都出现固定次数、后面是否跟着成功打开。没有这三个特征,直接忽略。
最后分享一个我现在的习惯。凡是第一次接触的服务出问题,先跑这么一条:
timeout 10 strace -f -c -w -p $(pgrep -f 服务名 | head -1) -o /tmp/s.log; cat /tmp/s.log十秒钟,一张表,能看出这个进程主要在用哪一类系统调用、错误率高不高、有没有明显的锁竞争。这个信息量比翻十页日志都大,而且几乎不会对业务造成影响。后面要不要深入、往哪个方向深入,看这张表就够了。真正需要长时间挂strace的场合其实很少,大多数问题在头十秒里就已经露出马脚了。