简单说,strace 就像是程序和操作系统之间的“窃听器”。
你的程序想读文件?申请内存?想发网络请求?还是系统调用?strace 就蹲在中间,把这些对话全给记录下来。
最关键的是:不用修改代码,不用重启程序,不用加日志,直接开查。
就排查问题来说,这玩意简直牛批完了!
先看下面这个例子:
# 追踪运行中的进程,看它在干什么
# [email protected]
$ strace -p 12345
connect(3, {sa_family=AF_INET, sin_port=htons(6379), 
        sin_addr=inet_addr("192.168.1.100")}, 16) = -1 ETIMEDOUT 
        (Connection timed out) <30.002345>
# 结果了然,程序连不上redis了
# 只需要一行命令,这就是 strace 的魅力 —— 不需要看源码,不需要加日志,直接看程序跟操作系统的对话。
# 追踪一个命令的所有系统调用
# 看看 ls 命令到底做了什么
# [email protected] 省略不少返回结果..
$ strace ls
execve("/usr/bin/ls", ["ls"], 0x7ffc...) = 0
brk(NULL)                               = 0x55555576d000
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 3
getdents64(3, /* 10 entries */, 32768)  = 320
write(1, "file1.txt\nfile2.txt\n", 20)  = 20
close(3)                                = 0
exit_group(0)                           = ?
看懂这几行,你就明白了:
# execve - 加载 ls 程序
# openat - 打开当前目录
# getdents64 - 读取目录项
# write - 输出到终端
# close - 关闭文件
# exit_group - 程序退出
# -e trace=network 只看网络相关的调用
# -s 1000 把字符串显示长度设为 1000(默认只有 32)
# [email protected],省略部分输出结果
$ strace -e trace=network -s 1000 curl https://api.opsnot.com/v1/user
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3                       # 创建TCP socket成功
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(443), 
        sin_addr=inet_addr("1.2.3.4")}, 16) = 0                     # TCP 连接建立成功(connect 返回 0)
sendto(3, "GET /v1/user HTTP/1.1\r\nHost: api.opsnot.com\r\n
       User-Agent: curl/7.68.0\r\nAccept: */*\r\n\r\n", 87, MSG_NOSIGNAL, NULL, 0) = 87  # 请求发送成功
recvfrom(3, "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\n
         Content-Length: 52\r\n\r\n{\"error\":\"invalid token\",\"code\":401}", 16384, 0, NULL, NULL) = 145  # 收到服务器响应
最终可以看到,请求发送成功了,并返回了401,大概率是token过期了
某数据处理脚本跑得贼慢,但CPU、内存占用都不高。
# -c 统计模式:显示每个系统调用的次数和总耗时
# 运行完程序后会输出统计表
# [email protected]
$ strace -c python process_data.py
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.87   45.234567      452345       100           fsync
  0.06    0.028901          28      1000           write
  0.03    0.012345          12      1000           read
  0.02    0.009876           9      1100           openat
  0.01    0.004567           4      1000           close
  0.01    0.003210           3      1000           fstat
------ ----------- ----------- --------- --------- ----------------
100.00   45.293466                  5200           total
# 可以看到:脚本 99.87% 的时间花在 fsync 上,只调用了 100 次,但每次耗时 450+ 毫秒,让开发检查代码,发现是每处理一条数据就 fsync 一次(强写)。
# 找到问题就好解决了,把脚本刷盘逻辑改成批量处理,每 1000 或 5000 条数据刷一次磁盘,根据实际场景来定,比如磁盘性能。
# -tt 显示精确时间戳(微秒级)
# -T 显示每个系统调用的耗时
# [email protected]
$ strace -tt -T java -jar app.jar
10:30:15.123456 execve("/usr/bin/java", ["java", "-jar", "app.jar"], ...) = 0
10:30:15.234567 openat(AT_FDCWD, "/etc/hosts", O_RDONLY) = 3 <0.000234>
10:30:15.234567 close(3) = 0 <0.000012>
# JVM 初始化
10:30:15.345678 socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5 <0.000045>
10:30:15.345678 fcntl(5, F_SETFL, O_RDONLY|O_NONBLOCK) = 0 <0.000023>
10:30:15.345678 connect(5, {sa_family=AF_INET, sin_port=htons(3306), 
                 sin_addr=inet_addr("10.0.0.100")}, 16) = -1 EINPROGRESS 
                 (Operation now in progress) <0.000123>
# 非阻塞连接等待
10:30:15.345789 poll([{fd=5, events=POLLOUT}], 1, 30000) = 0 (Timeout) <30.000456>
# 检查连接结果
10:30:45.346245 getsockopt(5, SOL_SOCKET, SO_ERROR, [ETIMEDOUT], [4]) = 0 <0.000034>
10:30:45.346279 close(5) = 0 <0.000015>
# 应用层重试(新建socket)
10:30:45.346294 socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5 <0.000041>
10:30:45.346335 connect(5, {sa_family=AF_INET, sin_port=htons(3306), 
                 sin_addr=inet_addr("10.0.0.100")}, 16) = -1 ETIMEDOUT 
                 (Connection timed out) <30.001234>
10:30:45.347569 close(5) = 0 <0.000013>
# 10:30:45.346245 getsockopt(5, SOL_SOCKET, SO_ERROR, [ETIMEDOUT], [4]) = 0 <0.000034> 这一行信息直击要害:
# getsockopt(...): 获取socket选项
# SO_ERROR: 查询socket错误状态
# [ETIMEDOUT]: 确认连接超时错误
# 这里是确认连接真正失败的关键步骤!
后续,可以综合tcmdump继续排障
# 比如,tcpdump验证网络行为
tcpdump -i any host 10.0.0.100 and port 3306
tcpdump 详细用法,请看我之前的文章 运维火眼金睛之 - tcpdump抓包实操手册
# 基础追踪
# [email protected]
$ strace ./program              # 追踪新启动的程序
$ strace -p 1234                # 追踪运行中的进程(PID=1234)
$ sudo strace -p 1234           # 追踪其他用户的进程需要 root
# 过滤系统调用
$ strace -e trace=file          # 只看文件操作(open, read, write, close...)
$ strace -e trace=network       # 只看网络操作(socket, connect, send, recv...)
$ strace -e trace=process       # 只看进程操作(fork, exec, wait...)
$ strace -e trace=signal        # 只看信号操作(kill, signal...)
$ strace -e trace=open,read     # 只看指定的系统调用
# 时间和性能分析
$ strace -tt                    # 显示时间戳(微秒精度)
$ strace -T                     # 显示每个调用的耗时
$ strace -r                     # 显示相对时间(每个调用距上个调用的时间)
$ strace -c                     # 统计模式(汇总所有调用)
# 输出控制
$ strace -s 1000                # 字符串显示长度(默认 32)
$ strace -o output.txt          # 输出到文件
$ strace -f                     # 追踪子进程(follow forks)
$ strace -ff -o trace           # 每个进程输出到单独文件(trace.PID)
# 组合使用(最常用)
$ strace -tt -T -e trace=file -s 200 ./app
# 显示时间戳 + 耗时 + 只看文件操作 + 字符串长度 200
很多服务会 fork 出多个子进程(比如 Nginx、Gunicorn),用 -f 追踪所有进程:
# -f 追踪所有子进程
# -o 输出到文件(终端会很乱)
# [email protected]
$ strace -f -o trace.log nginx
# 查看输出,每行前面会有 PID
$ head trace.log
1234  execve("/usr/sbin/nginx", ["nginx"], ...) = 0
1234  socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
1234  bind(3, {sa_family=AF_INET, sin_port=htons(80), ...}, 16) = 0
1234  listen(3, 511) = 0
1235  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|...) = 1235
1235  accept4(3, {sa_family=AF_INET, ...}, [16], ...) = 4
如果子进程很多,可以让每个进程输出到单独文件:
# -ff 每个进程一个文件
# 会生成 trace.1234, trace.1235, trace.1236...
# [email protected]
$ strace -ff -o trace gunicorn app:app
strace 的输出通常很多,配合 grep 快速找到想要的信息:
# 只看失败的系统调用(返回值为 -1)
# [email protected]
$ strace ./app 2>&1 | grep "= -1"
# 只看慢的系统调用(耗时超过 1 秒)
$ strace -T ./app 2>&1 | grep "<[1-9]\."
# 看某个文件的所有操作
$ strace -e trace=file ./app 2>&1 | grep "config.json"
# 看所有的网络连接
$ strace -e trace=network ./app 2>&1 | grep "connect"
# 统计每个错误类型的出现次数
$ strace ./app 2>&1 | grep "= -1" | awk '{print $NF}' | sort | uniq -c
strace 会让程序变慢,因为每个系统调用都要拦截和记录。线上使用时:
# ❌ 不好:追踪所有调用,程序会变得很慢
# [email protected]
$ strace -p 1234
# ✅ 好:只追踪需要的调用,减少开销
$ strace -e trace=network -p 1234
# ✅ 好:短时间追踪,看几秒就停止
$ timeout 5 strace -p 1234
# ✅ 好:使用统计模式,开销更小
$ strace -c -p 1234
生产环境使用建议:
# 追踪时间控制在几秒到几十秒
# 用 -e 只追踪需要的系统调用
# 避免追踪高频操作的进程(比如疯狂写日志的)
# 优先使用 -c 统计模式
# 方法一:找到容器内进程在宿主机上的真实 PID
# [email protected]
$ docker top mycontainer
UID     PID     PPID    CMD
root    12345   12344   python opsnot.py
$ sudo strace -p 12345
# 方法二:进入容器的 PID namespace  
$ docker inspect --format '{{.State.Pid}}' mycontainer 
12344
$ nsenter -t 12344 -p -m strace -p 1
# 容器内的进程 1 在容器内的视角追踪
docker inspect 详细用法,请看我之前的文章 Docker Inspect,值得族谱单开一页的命令
# 保存完整的追踪记录
# [email protected]
$ strace -tt -T -f -s 500 -o trace.log ./app
# 分析文件操作:哪些文件被访问了
$ grep "openat" trace.log | awk '{print $3}' | sort | uniq
# 分析耗时:找出最慢的 10 个系统调用
$ grep "<" trace.log | awk '{print $NF}' | sed 's/[<>]//g' | sort -rn | head -10
# 统计系统调用次数
$ awk '{print $2}' trace.log | cut -d'(' -f1 | sort | uniq -c | sort -rn
# 分析错误:统计各类错误
$ grep "= -1" trace.log | awk '{print $NF}' | sort | uniq -c
程序启动慢?
# 看看卡在哪个系统调用上
# [email protected]
$ strace -tt -T -o startup.log ./app
$ grep "<[0-9]\." startup.log | sort -t'<' -k2 -rn | head -10
文件找不到?
# 看程序到底在哪里找文件
# [email protected]
$ strace -e trace=openat ./app 2>&1 | grep "ENOENT"
网络连接失败?
# 看 DNS 解析、TCP 连接、数据收发
# [email protected]
$ strace -e trace=network -s 500 ./app 2>&1 | grep -E "connect|send|recv"
程序卡死?
# 看运行中的进程在等什么
# [email protected]
$ sudo strace -p $(pgrep myapp)
性能问题?
# 统计哪个系统调用最耗时
# [email protected]
$ strace -c ./app
适合的场景:
- 程序行为异常,不知道在干什么
- 启动慢、卡死、hang 住
- 配置文件找不到、读取失败
- 网络连接问题
- 磁盘 I/O 问题
- 快速定位问题,来不及加日志
要注意的坑:
# ❌ 错误:这样只能看到程序输出,看不到 strace
# [email protected]
$ strace ./app > output.txt
# ✅ 正确:重定向 stderr
$ strace ./app 2> trace.txt
# ✅ 正确:或者用 -o 参数
$ strace -o trace.txt ./app
# ❌ 错误:权限不够
# [email protected]
$ strace -p 1234
strace: attach: ptrace(PTRACE_SEIZE, 1234): Operation not permitted
# ✅ 正确:使用 sudo
$ sudo strace -p 1234
# x86_64 上的系统调用
$ strace ./app
openat(AT_FDCWD, "file.txt", O_RDONLY) = 3
# arm64 上可能是
openat2(AT_FDCWD, "file.txt", O_RDONLY) = 3
# 追踪所有子进程
$ strace -f nginx
strace 的输出在 stderr(错误输出),和程序的正常输出混在一起了:
# 方法一:只保存 strace 输出
# [email protected]
$ strace -o trace.log ./app
# 方法二:分离程序输出和 strace 输出
$ ./app > app.log 2> >(strace -o trace.log -p $$)
strace是 Linux 系统排查问题的神器,很多诡异问题都能用它定位。
下次遇到问题,别急着翻源码、加日志、重启服务,先strace一把。
加班哥提醒,strace执行会有性能影响,但短时间使用通常可以接受。
建议:线上使用前先在测试环境验证影响。
另外:strace在容器中使用也较为广泛,后续有机会将专门写一篇strace容器相关用法
本文由 opsnot.com 整理,转载请注明出处,点击下方卡片关注