strace 与 lsof:看不见的东西怎么观察

strace 与 lsof:看不见的东西怎么观察
隔壁老康难度 ★★★☆☆ · 前置:信号:kill -9 到底做了什么 | 一句话结论:
strace用ptrace把目标进程在每次系统调用前后停下来,所以它慢十倍、还会改变时序;lsof靠读/proc/*/fd,几乎零开销但只看得到当下。两个工具回答的是不同的问题。
八秒和两百毫秒,差在哪
线上有个服务,偶尔启动要 8 秒,偶尔 200 毫秒,重启一万次也复现不出规律,代码看完了,日志翻遍了,配置也比对了三遍,一个字不差,剩下的可能性就只有一种,它不是卡在自己的代码里,而是卡在跟内核说话的路上,而这条路在用户态完全不可见(越到这个阶段我越会怀疑自己前面的所有判断,因为你越是确信代码没问题,越说明问题在代码之外)。
这时候能做的只剩一件事,看它到底在跟内核说什么。
1 | strace -f -T -e trace=openat,connect,read,write -o /tmp/trace.log ./service |
然后 grep 一下,按耗时倒序取前五。
1 | awk -F'<' '{print $2}' /tmp/trace.log | sort -rn | head -5 |
1 | 3.901832> |
两个 4 秒级、2 秒级的调用,抓出来看是什么。
1 | grep -E '<(3\.9|2\.1)[0-9]+>' /tmp/trace.log | head -4 |
1 | [pid 8123] connect(3, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("10.0.3.12")}, 16 <3.901832> |
数据库连接。两个地址,第一个超时 4 秒,第二个 2 秒。配置里写了两个 MySQL 地址做故障转移,但客户端没设 connect_timeout,两个 connect 各自在 TCP 握手阶段从头等到尾才放弃。注意这不是内核的默认值,tcp_syn_retries 默认是 6,指的是 SYN 重传 6 次,退避 1+2+4+8+16+32+64 秒,单地址就要一分多钟。这里量到 4 秒和 2 秒,说明这台机器的 tcp_syn_retries 被调过,或者客户端库有自己的短超时,6 秒是实测结果,不是内核默认(我当时专门去线上机器敲了一遍 sysctl net.ipv4.tcp_syn_retries,值确实是 2,跟推算对得上,那一刻才敢确定自己没看错)。
这个问题光看代码永远看不出来。connect 在内核里等着,用户态的日志一点感觉都没有,你甚至会觉得进程根本没在干活,而它其实正在等一个永远等不到的 SYN-ACK,等的那几秒里 CPU 占用是零,线程栈上什么都看不出来,top 里那个进程甚至都不显示消耗,只有把边界上的耗时打出来,你才知道时间花在了一个你没有写的函数里,这也解释了为什么这类问题在代码评审里永远看不出来,评审看的是你写的东西,而它慢在你没写的东西上。
收费站这个比喻,只对了一半
我以前把 strace 理解成在系统调用边界上架了个收费站,车过就记一笔。这个说法对 strace 是准的,但把 lsof 也放进来就错得离谱,错到什么程度呢,差不多等于把温度计和空调当成同一类东西,都能影响室温,一个只是在看,另一个真的在改(这个比喻是我自己想的,想完还挺得意,用了小半年才发现它只能解释一半,另外一半得靠 ptrace 这个词本身去理解)。
strace 用的是 ptrace,内核提供的调试接口。它能让一个进程(跟踪者)控制另一个进程(被跟踪者)的执行,在系统调用入口和出口处停下来,读参数、读返回值,还能读目标进程的内存把指针参数解成字符串,所以它看到的是执行过程,而不是某个时刻的快照。这个区别听起来抽象,落到实际就是,它能在调用还没返回的时候告诉你进去了,而任何读状态的工具都只能告诉你现在是什么样,这一点决定了它所有的脾气和所有的开销,也决定了它为什么必须慢。
lsof 完全不用 ptrace。它干的事很朴素,遍历 /proc/*/fd,把每个符号链接的目标读出来,再读 /proc/*/maps 拿 mmap 打开的文件,拼成一张表,整个过程不打断任何人,也不被任何人打断,代价是你只能看它读的那一刻,早一秒晚一秒都不是同一张表。也正因为如此,它对被观察的进程是完全透明的,进程自己不会知道自己被看过,这也意味着它没能力告诉你刚刚发生过什么,想知道刚才那个 open 到底打开了哪个文件,只能事先挂上去看,事后补是不可能的。
这个区别比参数表重要得多。
差别就在这里。
strace |
lsof |
|
|---|---|---|
| 机制 | ptrace(拦截执行) |
读 /proc(事后看状态) |
| 能不能看历史 | 不能,只能看从现在开始 | 只能看现在 |
| 性能影响 | 十倍级 | 几乎没有 |
| 需要权限 | 目标同 uid 或 root | 同 uid 或 root(别人的 fd 看不到) |
所以用法完全不同,strace 用在怀疑它卡在哪的时候盯着,lsof 用在怀疑它开着什么的时候查一遍,一个看过程,一个看结果,而且两个都没法事后补。
lsof 那个输出,很长一段时间没看全
1 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME |
FD 那一列不只是数字,它描述的是这个进程和这个文件之间的关系类型。
六种。
| FD 值 | 意思 |
|---|---|
cwd |
当前工作目录 |
rtd |
根目录(chroot 之后会不一样) |
txt |
可执行文件本体 |
mem |
通过 mmap 打开的内存映射文件(共享库) |
3u / 4w / 5r |
数字加模式:r 读、w 写、u 读写 |
| 数字后面不带字母 | 等同于 u |
TYPE 列常见值。
七个值。
| TYPE | 是什么 |
|---|---|
REG |
普通文件 |
DIR |
目录 |
CHR / BLK |
字符设备 / 块设备 |
IPv4 / IPv6 |
网络 socket |
unix |
unix domain socket |
FIFO |
命名管道 |
a_inode |
内核内部用的匿名 inode(信号量等) |
(LISTEN) 出现在 NAME 里,这是 lsof -i 才有的信息。加上 -i 之后,lsof 会交叉比对 /proc/net/tcp 拿到网络状态,所以那一列不是从 fd 里读出来的,是另外拼的,这也解释了为什么 lsof -i 比 lsof -p 慢一截(这一条我翻了两遍 man 才确认,之前一直以为 fd 那一列自己能给出监听状态)。
strace 慢十倍,是因为你让它停三次
ptrace 的工作方式是,被跟踪的进程每次进出系统调用都要停下来,等跟踪者读参数、记日志、再放行。
1 | 正常: 用户态 → 系统调用 → 内核 → 返回 → 用户态 |
一共三次上下文切换,每次都要调度。
这是它绕不过去的账。而系统调用本来就贵,现在是贵十几倍,注意这还只是空转的开销,如果你的服务每秒几千次系统调用,这十几倍会直接体现成 QPS 上的一个数量级,而且丢的不只是吞吐,时序也一起变了,本来能复现的竞态可能就复现不出来,本来能压测出来的边界也压不出来,你以为自己在观测它,其实是换了一个程序在跑,这一点在排查并发问题时尤其致命,因为那些问题本来就依赖时序,你一挂上去它就不出现了。
所以真正该记住的是下面这三条,它们比任何参数都重要。
三条,都不长。
- 不能在生产上对高流量服务长跑
strace -p,它能把 QPS 从 5000 拖到 200。 - 测性能绝不能在
strace下测。 - 想低开销地观测,用
perf、bpftrace,或者strace -c(只统计不打印)。
strace -c 为什么快一些,因为它不解析参数、不打印,只累计计数。但停下来那一下还是躲不掉,所以 -c 只能用来估量级,不能用来测性能。
不收窄范围,等于没看
不加 -e 的话,一个正常程序一次启动能产生几万行,绝大多数是噪声,翻起来跟看十六进制转储差不多。我第一次这么干的时候在终端里滚了十分钟,最后什么也没找着,当时还怪这个工具不好用(错的是用法,不是工具,这一点我承认得比较晚)。
-e trace= 是必须用的,下面这七行基本覆盖日常九成的需求。
七行,抄下来就够用。
1 | strace -e trace=openat,open,stat,newfstatat ./app # 只看文件操作 |
-e trace= 后面跟的是只显示这些,跟 -e signal= 完全不是一回事,后者是对这些信号做特殊处理,两个选项差一个词,行为差出去很远。我第一次把这两行抄错成同一条,盯着一份干干净净的输出看了半小时,还以为是那个程序真的一次网络都没碰过,后来才发现是选项写错了,这种错很难自查,因为输出本身看不出任何异常,它只是少了一大块。
另一类常见的浪费是过滤写得太细,只留 openat,结果连接超时这种纯网络问题根本不会出现在输出里,你翻到天亮也翻不到,然后开始怀疑 strace 是不是漏了事件,其实是你自己把那一类事件关掉了,所以过滤的粒度要跟着问题走,先宽后窄,别一上来就锁死在某一个调用上,那样等于带着眼罩找东西。
先宽后窄,这四个字值一篇的篇幅。
常用的组合。
| 参数 | 作用 |
|---|---|
-f |
跟踪子进程(fork、exec 出去的),必须加 |
-p PID |
附到已存在进程 |
-o file |
输出到文件,别刷屏幕 |
-T |
每个调用后面标耗时,最有用 |
-t / -tt |
加时间戳 |
-s 256 |
字符串最大长度,默认 32,太短会截断 |
-c |
只统计,最后给汇总 |
-y |
把 fd 数字翻译成路径(很慢但很有用) |
-yy |
连 socket 的四元组(源 IP:端口 + 目标 IP:端口)都翻译出来 |
-k |
每次系统调用后打栈回溯(-kk 连源码一起打,极慢) |
-f 最容易忘。一个 shell 脚本、一个 Python 程序起子进程、一个用 system() 的 C 程序,不加 -f 就只看到父进程,子进程干的事全丢,而现在的服务几乎没有不起子进程的。我第一次给一个 Python 服务做 trace,输出干净得可疑,只有十几行。折腾了二十分钟才想起来它所有实际工作都在 multiprocessing 起的子进程里,加上 -f 之后刷了几千行,问题三秒就看见了(那二十分钟我一直坚信是那个服务真的没在干活,还顺手怀疑了一下是不是内核参数不对)。
lsof 报不全的时候,先想权限
它的局限来自只读 /proc 这一件事,下面四条后果都是从这一条推出来的。
四条。
一,看不到已删除的文件。名字栏会写 (deleted),这恰好是找泄漏的利器,一个日志文件被 rm 了但进程还开着,磁盘空间就不会回来,df 说满了,du 说没占多少,差的那一块就在这儿。
1 | lsof +L1 |
二,需要权限。别人的进程,普通用户看不到 mem 和 txt 这些,因为 /proc/PID/maps 有权限检查。所以 lsof 报不全的时候先想是不是权限问题,别先怀疑工具坏了。
三,读不到 /proc 就没有。容器里 /proc 被遮了、或者进程在另一个 namespace 里,就看不到,这时候要先 nsenter 进去。
四,它可能是你的 IO 延迟来源。
最后一条最容易忽略。在进程数上万的机器上,lsof 一次能跑十几秒,因为它要 readlink 每一个 fd。别在故障排查时随手 lsof 全系统,加 -p 或 -i 收窄(我有次在压测时顺手跑了一下,机器直接卡了半分钟,那一下才知道这个工具不是免费的)。
什么时候该用哪个。
还是那张表更省事。
| 症状 | 先用什么 |
|---|---|
| 启动慢,不知道卡在哪 | strace -f -T -e trace=file |
| 端口占用了,不知道是谁 | lsof -i :8080 或 ss -ltnp |
| 磁盘满了但删不掉 | lsof +L1 |
| 某个文件被哪个进程写着 | lsof /var/log/foo.log |
| 进程卡着不动 | /proc/PID/wchan + /proc/PID/stack |
| 想看历史调用 | 都不行,只能复现 |
先建立一个数量级的直觉
strace -c 建立的是数量级的直觉,一个命令到底调了多少次系统调用。
1 | strace -c ls > /dev/null |
1 | % time seconds usecs/call calls errors syscall |
ls 一个空目录,一千多次调用。这就是第一篇里说系统调用不便宜的意思,不是单次贵,是次数多,一次 ls 背后是 mmap、newfstatat、close、execve 一长串,每一项都要过一次边界。然后看每次调用花多久。
1 | strace -T -e trace=openat,newfstatat,write,close ls /tmp 2>&1 | head -12 |
每行末尾的 <0.000012> 就是耗时。绝大多数是微秒级,偶尔有一个几百微秒,那就是异常点,-T 的精度到微秒,这也是它能用来定位慢调用的原因。现在抓一个慢的现场,用 nc 连一个不存在的地址。
1 | time strace -T -e trace=connect nc -w 5 10.0.3.99 3306 |
-T 会在 connect 那行标出耗时。如果那台机器把包丢了(不是拒绝),你会看到超时,这两种失败的耗时差着一个数量级,包被丢的时候你得等到 connect 自己放弃。对比一下 DNS 解析的代价。
1 | strace -T -e trace=connect ssh -o ConnectTimeout=2 -o BatchMode=yes nowhere.invalid |
connect 之前会有一次 sendto、recvfrom,那就是 DNS。很多连接慢其实是 DNS 慢,而 DNS 慢又常常是因为 /etc/resolv.conf 里配了两个不可达的 nameserver。
这一条我自己吃过两次。
DNS 这个坑还有一层,/etc/resolv.conf 里默认的 options timeout 是 5 秒、attempts 是 2,两个不可达的 nameserver 叠起来就是 20 秒起步,而这个等待发生在 connect 之前,strace -e trace=network 只会显示 sendto 和 recvfrom 各一次,看起来干干净净,耗时全藏在中间的空白里,不加 -T 根本看不出来。我第一次定位这种问题用的是 time,只看到整条命令慢了二十秒,以为是网络问题,让人去查防火墙,查了两个小时才发现是 resolver 配置写错了一个 IP。
然后是 lsof。
1 | lsof -p $$ | head -12 |
1 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME |
$$ 是 shell 自己。看 txt 那一行是 /usr/bin/bash,它自己。0u、1u、2u 全指向 /dev/pts/0,三个标准流都在同一个终端(这个观察很简单,但它顺手解释了一个经典问题,为什么重定向只改一个数字就能生效)。查端口。
1 | lsof -i :22 |
-n 和 -P 一定要加,否则 lsof 会去做 DNS 反查和 services 查询,能把一个 0.1 秒的命令拖成 10 秒。这个坑踩过的人很多。最后是找删除未释放。
1 | lsof +L1 2>/dev/null | awk '$7 ~ /^[0-9]+$/ && $7+0 > 1048576 {print}' |
只打印链接数为 0 且大小超过 1MB 的,前面那些 (deleted) 的小文件是噪声,真正吃空间的是几个 GB 的日志。
strace 测不出性能
strace 能不能用来做性能分析。它的开销是十倍级,测出来的性能毫无意义,要看性能用 perf,要让它进生产用 bpftrace,三个工具的分工不能混。
strace -p 安不安全。对高流量服务可能是致命的,先用 -c 估一下,或者干脆换 bpftrace,只在预发和故障现场用 -p(这条是被人当场拦住的,我之前一直在线上随手 -p,还觉得自己很熟练)。
不加 -f 也能看到全部,这是错的。fork 出去的子进程全部丢,脚本、Python、任何用 system() 的程序都要 -f。
lsof 会不会拖慢系统。它自己不拖,但它在进程数多的机器上要跑很久,排查时可能变成新问题,lsof -i :80 记得加 -n,快十倍。
strace 能看到历史调用,只能看到从附加那一刻开始的,要历史只能靠日志或 bpftrace 的 ring buffer;strace 能看内核里的执行,只能看边界,内核内部发生了什么它一无所知,那是 perf、ftrace、bpftrace 的地盘。
lsof 报 (deleted) 是不是文件坏了。不是,是名字没了但还开着,这恰恰是找泄漏最关键的证据。
两种看法,两种代价
strace 靠 ptrace 在系统调用边界上拦截,开销十倍级,绝不能用于性能测试。-f 跟子进程、-e trace= 收窄范围、-T 看耗时,这三个是必加的,缺一个就会得到一份自己看不懂的输出。lsof 靠读 /proc,几乎零开销,但只能看当下状态,加 -n -P 快十倍,+L1 找删除未释放。
两个都受权限限制,看到的可能不全,不全的时候先怀疑权限,别先怀疑工具。到这里已经能回答进程在干什么、它在等什么、它开着什么这三个问题了,也知道它们的边界在哪里,一个是过程视角但有代价,一个是状态视角但只有一瞬,两个都不能事后补,也都不能在生产上空跑。这套东西我用了三年,到现在还是每次排查先跑一遍 strace -c 估个量级,再决定要不要挂 -p,这个顺序反过来的话,往往是把故障现场又搅了一遍。下一个问题是,nohup ./server & 起的服务,关掉终端就没了,而 nohup 明明装了、也确实生效了,为什么。









