☰
系统调用追踪实战:用strace、perf trace、bpftrace定位性能瓶颈
2026/10/3 18:02:23 网站建设 项目流程

有一次线上网关服务的接口耗时突然从 40ms 涨到 800ms,top 看下来 CPU 只有 25%,内存、磁盘都正常,GC 日志也没有异常。日志翻了一圈,看不出所以然。后来我不猜了,直接挂上系统调用追踪,几分钟就锁定了问题:线程大量卡在futex等待上,顺着锁信息再查代码,改完立刻恢复。

这套排查方式就是我平时处理 Linux 性能问题的主要思路:不要只盯着 CPU 和内存,先搞清楚进程到底在内核态里等什么。今天把系统调用追踪和性能分析的实战方法完整写一遍,从strace到perf trace再到bpftrace,覆盖日常排查中最常用的手段。运维、后端、SRE 和做性能优化的同学都可以参考。

1. 为什么盯系统调用是最快的定位切口

1.1 应用卡住时,CPU 数据很可能“骗人”

大多数性能问题的第一反应是看 CPU。CPU 打满说明在“干活”,但实际线上场景更常见的是:CPU 不高,接口却慢得离谱。这说明进程把时间消耗在了等待上,而这种等待恰恰发生在内核里。

比如线程在等锁、等网络包、等磁盘 IO、等另一个进程响应,这些状态在 top 里看不出来。虽然可以看wa、si这些指标,但无法告诉你是哪个进程、哪一类操作在等待。系统调用追踪能直接看到线程正在调用什么内核功能,等的是read还是futex,等了多少时间。

1.2 系统调用是用户态和内核态之间唯一的口子

用户程序要做文件读写、网络收发、内存分配、线程同步,都必须通过系统调用进入内核。每一次系统调用都有清晰的“进入”和“返回”两个时间点,这两个时间点之间的差值,就是这次操作真正消耗的内核时间。

这句话值得多念几遍:只要程序慢,不是用户态在算,就是内核态在等。用户态在算,CPU 会高;内核态在等,CPU 往往不高。追踪系统调用,等于给进程装了一个“门禁监控”,每次进出内核都会留下记录。

1.3 一步区分用户态忙和内核等待

用strace或者perf trace都能看到系统调用耗时。如果追踪结果里绝大多数系统调用的耗时都在微秒级,CPU 高,那瓶颈大概率在用户态计算;如果某些系统调用单次耗时几十毫秒甚至更多,那就是内核等待。这个方向一旦确认,后续排查范围就小多了。

我经常跟同事说:系统调用追踪不是万能的,但它是从“糊涂”走向“明白”的第一个台阶。

2. strace 的正确打开方式:先统计再抠细节

2.1 别一上来就全量输出

网上很多教程让你直接执行strace -p PID,然后看滚动输出。这对小脚本没问题,但在生产环境,这么做会直接让服务瞬间变慢。因为 strace 基于 ptrace 实现,每进入一次系统调用,进程都会被停下,由 tracer 记录完再放行。高并发下全量输出基本等于自杀。

我的习惯是先上统计模式:

strace -f -c -p 12345

-f跟随子进程和线程,-c只做汇总,不输出每条调用。跑 10 到 30 秒,按 Ctrl-C 结束,strace 会打印类似这样的统计:

% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- -------------- 48.22 36.173490 128390 2815 futex 22.13 16.601211 20340 816 epoll_wait 15.02 11.265790 13980 806 read ...

这张表能立刻告诉你:进程把时间花在哪个系统调用上了,调用次数多不多,错误多不多。比盯着一万行滚动日志高效太多。

2.2 熟悉这几个筛选和输出参数

确认了目标系统调用之后,才需要看具体调用细节。常用组合:

strace -f -T -tt -e trace=futex -p 12345
  • -T显示每个系统调用耗时;
  • -tt显示微秒级时间戳;
  • -e trace=指定追踪范围,可以填read,write、network、file、desc这类分组,也可以填具体系统调用名。

看文件相关操作时建议加-y,这样输出的文件描述符会带上具体路径。很多隐蔽问题就是“明明打开了某个文件,结果操作的是另一个路径”。

2.3 追踪正在运行的进程要注意几个硬约束

第一,ptrace 同一时间只能有一个 tracer 附着。如果你挂着 strace,再执行gdb -p会失败;反之亦然。曾经有同事在排障现场先挂了 gdb,我 strace 半天附不上去,浪费了不少时间。

第二,容器内追踪进程经常报Operation not permitted。这不是没权限,而是容器没有CAP_SYS_PTRACE能力,或者宿主机开了kernel.yama.ptrace_scope限制。要么用--cap-add=SYS_PTRACE重新启动容器,要么在宿主机上用nsenter进入进程的 pid namespace 再追踪。

第三,长时间挂 strace 本身会改变程序行为,尤其是同步 IO 密集型的程序。所以 strace 适合“短平快”地取证,不适合长时间开着当监控。

3. perf trace:同样看系统调用,开销更可控

3.1 为什么还需要 perf trace

strace 的问题在于 ptrace 开销太大。如果你在流量高峰期必须做追踪,又不想背锅,可以先试perf trace。它内部走的是内核 tracepoint 和 perf_event 子系统,不是 ptrace,开销比 strace 小一个数量级,而且基本都能拿到同样的系统调用参数。

用法非常像 strace:

perf trace -p 12345

它会滚动输出进程的系统调用,并且自带耗时列。也可以指定只追踪某几个系统调用:

perf trace -e read,write -p 12345

想快速汇总,加-s:

perf trace -s -p 12345

-s模式输出的是聚合统计,和strace -c类似,但采集过程对服务的影响更小,特别适合“线上先看看趋势”的场景。

3.2 什么时候选 perf trace,什么时候选 strace

我自己心里的选择标准大概是这样的:

场景推荐工具理由
进程启动后想跟踪子进程全貌strace参数解析更强大,-ff按进程拆分日志
高流量服务需要临时观察perf trace开销低,汇总快
只需要系统调用耗时分布perf trace数据来自内核 tracepoint,统计更稳
要详细看某个 syscall 的返回值和 errnostrace输出更完整,和代码对接更直接
老内核或容器受限场景两个都试试哪个能跑用哪个

strace 还有一个难以替代的点:它的-e trace=file、-e trace=network这类高级筛选非常直观,适合快速在人脑里建立排查框架。perf trace 更偏原始,功能上反而朴素。

3.3 常见失败:perf_event_paranoid

如果你在普通用户下执行 perf trace,遇到:

You may not have permission to collect stats.

大概率是kernel.perf_event_paranoid限制。可以临时调整:

sudo sysctl -w kernel.perf_event_paranoid=-1

生产环境建议不要永久放宽,用完改回去。另外容器里同样有 capability 限制,需要检查是否具备CAP_PERFMON或CAP_SYS_ADMIN。

4. bpftrace 与 tracepoint:把调用链和耗时分布看透

4.1 tracepoint 是比 ptrace 更稳的观察点

strace 让人犹豫的地方是它会“停下来记录”。而内核从很早的版本开始就提供了 tracepoint,比如raw_syscalls:sys_enter和raw_syscalls:sys_exit,分别在系统调用进入和退出时触发。

bpftrace 就是基于 BPF 技术在这些 tracepoint 上挂载程序,只在内核里做统计,把结果通过 ring buffer 传出来。它不会逐个暂停进程,因此可以做到生产级、低开销的观测。

4.2 一条命令统计各进程的系统调用次数

最常用的 bpftrace 入门命令是这个:

bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

含义:每次有进程发起系统调用,就在以comm(进程名)为 key 的计数器上加一。程序跑一段时间后 Ctrl-C,会按次数排序打印:

@[nginx]: 384920 @[java]: 251234 @[sshd]: 1822

这条命令比strace -f -c干净得多,不需要 attach,不占 ptrace,对目标进程几乎没有侵入。特别适合判断“到底哪个进程在疯狂发系统调用”。

4.3 统计系统调用耗时分布

先看次数还不够,还得看“有多少次是快的,有多少次是慢的”。用 sys_exit 可以算每次系统调用的耗时:

bpftrace -e ' tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; } tracepoint:raw_syscalls:sys_exit /@start[tid]/ { @usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'

输出是一张直方图,能很直白地看到耗时是否分层。比如大部分在 10us 以下,但有一批在 100ms 以上,那就值得继续查这批慢调用属于哪个进程、哪类操作。

用 bpftrace 还有一个高级玩法:用comm == "java"这类条件先过滤,只统计目标进程,避免数据被无关进程污染。

4.4 安装 bpftrace 的几个提醒

Debian/Ubuntu 上通常一句apt install bpftrace就能装,CentOS/Rocky 用dnf install bpftrace。但有两个点需要提前确认:

  • 内核需要开启 BPF 相关选项,绝大多数发行版默认都开了;
  • 老内核没有 BTF(BPF Type Format)信息时,bpftrace 可能报Failed to load BTF。这时候要么换新内核,要么试试回退到旧版本 bpftrace。

如果项目长期需要系统调用级监控,我建议不要只在出问题时才装 bpftrace,而是把它固化到日常巡检脚本里,定时输出一份系统调用排行。很多瓶颈是“量变到质变”的,提前看到趋势,比事后抢救有价值得多。

5. 一次真实案例:syscall 统计帮我锁定锁竞争与等待时间

5.1 场景回顾:CPU 只有 25%,但 RT 飙升

那次压测环境比较干净,Java 网关应用,QPS 大概 1 万。现象是:

  • 服务 RT 从 40ms 爬到 800ms;
  • 进程 CPU 只有 25%;
  • 内存、GC、磁盘都正常;
  • 下游 Redis、数据库都没有明显变慢。

按照常规思路,CPU 不高就应该怀疑线程在等待。等什么?网络?连接池?锁?日志里没有超时,慢查询也没有。这时候最合适的手段就是系统调用统计。

5.2 用 strace 汇总,切出嫌疑范围

执行:

strace -f -c -p 12345

跑 30 秒后,摘要长这样(简化过):

% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- -------------- 74.12 121.4730 241000 504 futex 15.22 24.9430 30000 831 epoll_wait 6.30 10.3200 120 860 read

futex占了将近四分之三的耗时。futex是用户态锁进入内核等待时的系统调用,看到它大量耗时,基本可以断定线程在用户态锁上发生竞争。继续看 Java 线程栈,果然大量线程 BLOCKED 在同一把锁上。

这里有个容易误判的点:看到futex次数多不一定就是问题。futex调用次数少但单次等待几十毫秒,才是锁竞争;如果次数多但每次几十微秒,可能只是正常的线程协调。一定要看耗时占比,而不是调用次数。

5.3 顺着系统调用反查代码

系统调用只能告诉你“问题出在 futex”,但不知道具体是哪把业务锁。这时要结合 Java 的线程 dump:

jstack 12345 > /tmp/jstack.txt

在线程 dump 里搜BLOCKED、waiting to lock,能直接看到各个线程在等哪个对象。那次排查结果是:某个内存缓存的读写锁粒度太大,热点 key 一多,所有线程全在锁上排队。

改法是锁粒度细化 + 热点 key 拆分成多个分片。上线后 RT 立刻回落到 45ms 左右。这次改动的起点就是 30 秒strace -c统计,整个定位时间不到二十分钟。

5.4 这个案例教给我的排查顺序

先别管业务复杂度,先回答三个问题:

  1. 进程时间主要花在哪个系统调用上?
  2. 是单次调用慢,还是调用次数太多?
  3. 这个系统调用背后对应什么资源竞争?

回答完这三问,大部分性能毛刺都能定位到方向。剩下的是业务层代码细节,工具只能帮你缩小范围,不能替你理解业务。

6. 流量大时追系统调用的三个隐蔽坑

6.1 第一个坑:strace 的输出可能被磁盘拖死

不要用默认方式把 strace 输出重定向到磁盘文件,尤其是-o /tmp/strace.log加上-ff多文件输出。高流量下每秒钟可能有几十万条记录,日志文件瞬间膨胀,磁盘 IO 反过来成为新的瓶颈,甚至把服务拖挂。

如果要保存记录,先输出到内存盘,或者用-tt但只追踪目标系统调用,并且限制采集时长。我通常的做法是:

timeout 15 strace -f -c -p 12345

15 秒后自动退出,只拿统计结果。需要明细时再加-e trace=目标调用,并且控制并发量。

6.2 第二个坑:errno 不一定是错误

系统调用返回-1时,strace 会显示 errno。看到ENOENT第一反应是“文件不存在”,但很多程序启动时会做大量路径探查,比如依次尝试打开多个配置文件、加载动态库时扫描多个目录,这些ENOENT是正常流程,不代表有问题。

比较典型的例子:Java 进程启动时会调用openat尝试很多路径,找不到就ENOENT。如果你只看错误数,很容易误判成“程序疯狂报错”。正确的做法是结合调用名和耗时看,错误数量和类型是参考,不是结论。

6.3 第三个坑:工具本身让性能问题“转移”

不管是 strace 还是 perf trace,都存在观测效应。strace 因为 ptrace 机制,开销尤其明显。在已经不稳的进程上全量追踪,可能导致线程调度延迟放大,RT 变得更夸张,你看到的数据是“被污染后”的假象。

bpftrace 虽然轻,但如果你在raw_syscalls:sys_enter上挂了复杂程序,同样会占用 CPU。生产环境追查时,我的原则是:先低开销统计,再小范围采样,最后才考虑全量输出。每一步都只增加“刚好能得出结论”的观测成本,不贪多。

还有一个容易被忽略的小问题:不要在生产高峰期长时间 attach。ptrace 在 attach 和 detach 的瞬间会对所有线程产生一次全局停顿,如果业务线程数很多,那一瞬间可能导致大量请求超时。所以执行strace -p之前,一定确认你接受这个风险。

7. 最后的排查习惯建议

工具会了,还要有稳定习惯。我现在遇到性能问题,默认顺序是:top看 CPU 分布 →pidstat看线程态 → 系统调用统计看等待方向 → 业务日志/线程栈精确定位。系统调用追踪永远是中间那一步,作用是快速建立假设,而不是直接给结论。

建议你在压测环境先把 strace、perf trace、bpftrace 三条命令练熟,尤其是 bpftrace 的直方图输出。等真正出问题时,你只需要几秒钟就能决定用哪个工具、跑多长时间、看哪些字段。这个熟练度,比记住再多命令参数都管用。

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

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

立即咨询