Linux 性能调优系列(十二): 服务器又卡了?SystemTap 带你杀到内核看看为啥
1 | 作者:李晓辉 |
前面我们已经学习了 perf、strace 和 ltrace。它们解决的问题不太一样:
flowchart LR
APP["应用程序"]
APP --> LTRACE["ltrace<br/>库函数"]
APP --> STRACE["strace<br/>系统调用"]
APP --> PERF["perf<br/>性能热点"]
STRACE --> KERNEL["Linux Kernel"]
PERF --> KERNEL
KERNEL --> STAP["SystemTap<br/>内核动态追踪"]比如:
perf发现:这个进程 CPU 很高,某个函数占了大量 CPUstrace发现:这个进程正在疯狂调用某些系统调用ltrace发现:程序频繁调用某个用户态库函数
但是有时候我们还想继续往内核里面看:
这个内核函数到底被调用了多少次?
是谁调用的?
什么时候调用的?
文件 IO、网络、调度器内部到底发生了什么?
这时候,就可以考虑 SystemTap。
SystemTap 是什么?
SystemTap 是 Linux 上的动态追踪工具。它最大的特点是:
不需要重新编译 Linux 内核,也不需要重启服务器,就可以给正在运行的系统增加动态探测点。
简单来说:
1 | 正在运行的 Linux |
所以可以把 SystemTap 理解成:
给正在运行的 Linux 内核“装上一根临时探针”。
探针发现感兴趣的事件以后,就执行我们定义好的处理逻辑。
SystemTap 到底怎么工作?
这里不用一上来就背一堆概念。你只需要先理解一件事情:
SystemTap 脚本最终会被编译成一个内核模块,然后加载到正在运行的内核中。
整体流程:
flowchart LR
A["SystemTap 脚本"] --> B["解析"]
B --> C["生成 C 代码"]
C --> D["编译"]
D --> E["内核模块 .ko"]
E --> F["加载到 Linux 内核"]
F --> G["开始追踪"]
G --> H["输出 / 统计结果"]所以 SystemTap 和普通 Shell 脚本有一个很大的区别。
Shell 脚本:
1 | Shell脚本 |
SystemTap:
1 | SystemTap脚本 |
这也是 SystemTap 强大,同时又需要谨慎使用的原因。
SystemTap 最核心的两个概念
学习 SystemTap,其实先记住两个词就够了:
1 | Probe |
可以把它们理解成:
Probe:什么时候触发?
Handler:触发以后干什么?
例如:
1 | stap -e 'probe timer.s(1) { printf("hello\n"); exit() }' |
这里:
1 | probe timer.s(1) |
就是定义:
1 秒以后触发。
而:
1 | { |
就是:
触发以后执行什么操作。
因此 SystemTap 最基本的模型就是:
flowchart LR
EVENT["事件发生"] --> PROBE["Probe<br/>定义触发点"]
PROBE --> HANDLER["Handler<br/>处理逻辑"]
HANDLER --> RESULT["输出 / 统计"]以后看到 SystemTap 脚本,先找这两个东西,就不会觉得它很复杂。
安装 SystemTap
先看看当前运行的内核:
1 | [root@localhost ~]# uname -r |
安装 SystemTap:
1 | [root@localhost ~]# dnf install systemtap -y |
然后运行:
1 | stap-prep |
stap-prep 会根据当前运行的内核,检查并准备 SystemTap 所需要的内核开发和调试环境。
例如:
1 | Configuring for kernel release 6.12.0-211.16.1.el10_2.0.1.x86_64 |
如果自动准备失败,也可以手动安装:
1 | dnf install \ |
这里有一个非常容易混淆的地方:
kernel-debuginfo ≠ kernel-debug
kernel-debuginfo 主要提供调试符号等调试信息。
kernel-debug 则是另一种带特定调试配置的内核构建。
所以:
不要因为名字里都有
debug,就认为这两个包是一回事。
对于 SystemTap,需要关注的是与目标内核匹配的 kernel-debuginfo、kernel-debuginfo-common 和 kernel-devel 等包。
先别急着写复杂脚本
安装完成以后,我们先运行一个最简单的 SystemTap。
1 | stap -e 'probe timer.s(1) { exit() }' |
你可能会发现:
1 | 执行命令 |
为什么?
因为:
1 | timer.s(1) |
表示:
1 秒定时器事件。
而:
1 | exit() |
表示:
退出 SystemTap。
所以这个脚本虽然没有什么实际用途,但是特别适合干一件事情:
验证 SystemTap 环境到底有没有配置成功。
看看 SystemTap 的完整执行过程
刚才我们执行的时候,如果加上 -v:
1 | [root@localhost ~]# stap -v -e 'probe timer.s(1) { exit() }' |
这其实就是 SystemTap 的完整工作流程。
可以记成:
flowchart LR
P1["Pass 1<br/>解析脚本"]
P2["Pass 2<br/>分析脚本 / 探测点"]
P3["Pass 3<br/>生成 C"]
P4["Pass 4<br/>编译 .ko"]
P5["Pass 5<br/>加载并运行"]
P1 --> P2 --> P3 --> P4 --> P5平时不用死记每个 Pass 的细节。真正需要记住的是:
Pass 4:已经编译出了内核模块。
Pass 5:加载模块并真正开始追踪。
因此:
1 | stap -p 4 your_script.stp |
表示:
只编译到 Pass 4,不进入运行阶段。
这个参数后面的交叉插桩会用到。
SystemTap 真正开始有用了
前面的例子只是验证环境。真正排查性能问题,我们通常不会从零开始写脚本。因为 SystemTap 已经提供了大量官方示例。
先看看:
1 | [root@localhost ~]# ls /usr/share/systemtap/examples |
还有一个非常方便的索引:
1 | /usr/share/systemtap/examples/index.html |
所以实际工作中,我更推荐大家养成一个习惯:
flowchart LR
PROBLEM["遇到性能问题"]
PROBLEM --> SEARCH["先找官方示例"]
SEARCH --> EXIST["是否已经有类似脚本?"]
EXIST -->|是| RUN["直接运行"]
EXIST -->|否| WRITE["再考虑自己写脚本"]不要一上来就自己写。
案例一:程序访问文件到底花了多少时间?
假设现在遇到一个问题:
程序访问文件很慢。
但是 iostat 看到的磁盘整体负载并不高。这时候可以尝试:
1 | [root@localhost ~]# stap /usr/share/systemtap/examples/io/iotime.stp |
这个脚本会帮助我们观察进程访问文件时的 IO 情况。我们重点关注:
1 | 进程 |
这样就可以进一步回答:
到底哪个进程访问了哪个文件?
读写过程中花了多少时间?
这和 iostat 的关注点是不一样的。
flowchart LR
IOSTAT["iostat<br/>磁盘整体负载"]
IOTIME["iotime.stp<br/>进程文件访问行为"]
IOSTAT --> A["设备层面"]
IOTIME --> B["进程 / 文件 / IO时间"]所以:
iostat更适合看设备整体情况。
iotime.stp可以进一步观察具体进程的文件访问行为。
案例二:哪个进程疯狂调用系统调用?
再来看一个非常典型的问题。
假设:
1 | 某个服务器 CPU 使用率突然升高 |
我们怀疑:
是不是某个进程正在疯狂和内核交互?
可以运行:
1 | [root@localhost ~]# stap /usr/share/systemtap/examples/process/syscalls_by_proc.stp |
运行完卡住了吧,哈哈,这时候不要着急。它正在后台:
1 | 收集数据 |
按:
1 | Ctrl+C |
以后,才会显示统计结果,例如:
1 | #SysCalls Process Name |
这样我们就能快速知道:
哪些进程产生了大量系统调用。
整个思路非常简单:
flowchart LR
PROCESS["多个进程"]
PROCESS --> SYSCALL["系统调用"]
SYSCALL --> STAP["SystemTap统计"]
STAP --> RESULT["按进程汇总"]这类工具非常适合回答:
“到底哪个进程正在疯狂进入内核?”
SystemTap 的权限
这里不用讲得特别复杂。SystemTap 最终需要加载内核模块,所以它不是一个普通的用户态工具。常见的两个用户组:
1 | stapdev |
可以简单理解成:
| 用户组 | 能做什么 |
|---|---|
stapdev | 可以编译、加载和运行 SystemTap 模块 |
stapusr | 只能运行已经准备好的 SystemTap 模块 |
因此:
stapdev权限非常高,不应该随便授予普通用户。
如果企业希望实现:
1 | 普通用户 |
那么 stapusr 更符合最小权限的思路。
生产环境不能直接装?还有交叉插桩
这才是 SystemTap 里比较有实际价值的一部分。
假设生产服务器:
1 | 不允许安装 GCC |
但是现在又确实需要 SystemTap。怎么办?
答案就是:
交叉插桩。
核心思想就一句话:
把编译放到生产环境之外,把运行留在生产服务器。
flowchart LR
HOST["编译主机"]
HOST --> SCRIPT["SystemTap脚本"]
SCRIPT --> COMPILE["针对目标内核编译"]
COMPILE --> KO["生成 .ko"]
KO --> COPY["复制"]
COPY --> PROD["生产服务器"]
PROD --> RUNTIME["systemtap-runtime"]
RUNTIME --> STAPRUN["staprun"]
STAPRUN --> KERNEL["目标内核"]也就是说:
1 | 编译主机 |
生产服务器不负责编译。
交叉插桩最重要的一点
这里千万不要理解成:
“我在一台 RHEL 服务器编译一个模块,然后拿到另一台 RHEL 服务器就能跑。”
不是。
内核模块必须针对目标内核环境构建。首先在生产服务器确认:
1 | uname -r |
例如:
1 | 6.12.0-211.16.1.el10_2.0.1.x86_64 |
然后编译环境需要准备目标内核对应的:
1 | kernel-devel |
然后:
1 | stap -r <目标内核版本> \ |
例如:
1 | stap -r 6.12.0-211.16.1.el10_2.0.1.x86_64 \ |
最终得到:
1 | mymodule.ko |
再把它复制到目标服务器的 SystemTap 模块目录。
目标服务器只需要:
1 | dnf install systemtap-runtime -y |
然后运行:
1 | staprun mymodule.ko |
所以记住一句话:
编译主机负责“造模块”,生产服务器负责“跑模块”。
SystemTap 为什么需要谨慎?
现在大家应该能理解 SystemTap 为什么需要谨慎了。因为它不是:
1 | 普通用户程序 |
而是:
1 | SystemTap脚本 |
所以错误的内核级追踪代码可能影响系统稳定性。尤其是使用 guru mode、嵌入 C 等高级能力时,更应该谨慎。
生产环境建议:
flowchart LR
A["官方示例"] --> B["测试环境验证"]
B --> C["生产环境短时间运行"]
C --> D["收集数据"]
D --> E["结束追踪"]而不是:
“线上出问题了,我随便写个 SystemTap 脚本直接跑。”
这并不是一个好的生产实践。
SystemTap 和 eBPF有什么关系?
下一篇我们会讲 eBPF。现在不用提前把 eBPF 学完,只需要知道一个核心区别:
flowchart LR
STAP["SystemTap"]
STAP --> C["生成 C"]
C --> KO["内核模块 .ko"]
KO --> K1["Linux Kernel"]
EBPF["eBPF"]
EBPF --> PROG["eBPF 程序"]
PROG --> VERIFY["Verifier"]
VERIFY --> K2["受约束的内核执行环境"]简单来说:
SystemTap:脚本最终生成并加载内核模块。
eBPF:程序需要经过内核 verifier 等机制检查,在受约束的执行模型中运行。
所以两者都可以做:
1 | 内核追踪 |
但底层实现和安全模型不同。下一篇我们再详细讲 eBPF。
到底什么时候使用 SystemTap?
到这里不要再继续背命令了。真正重要的是建立这个判断:
flowchart LR
PROBLEM["发现性能问题"]
PROBLEM --> TOP["top<br/>哪个进程有问题?"]
TOP --> PERF["perf<br/>CPU热点在哪里?"]
PERF --> STRACE["strace<br/>发生了哪些系统调用?"]
STRACE --> STAP["SystemTap<br/>内核内部发生了什么?"]
STAP --> ROOT["进一步定位根因"]可以简单理解成:
| 工具 | 主要回答的问题 |
|---|---|
top | 谁在消耗资源? |
perf | CPU 时间主要消耗在哪里? |
strace | 进程调用了哪些系统调用? |
ltrace | 程序调用了哪些库函数? |
SystemTap | 内核内部发生了什么? |
这才是这几个工具真正应该建立起来的关系。
最后记住几个 SystemTap 命令
验证 SystemTap:
1 | stap -e 'probe timer.s(1) { exit() }' |
运行脚本:
1 | stap your_script.stp |
查看详细执行过程:
1 | stap -v your_script.stp |
只编译到 Pass 4:
1 | stap -p 4 your_script.stp |
查看官方示例:
1 | ls /usr/share/systemtap/examples |
运行 IO 示例:
1 | stap /usr/share/systemtap/examples/io/iotime.stp |
运行系统调用统计:
1 | stap /usr/share/systemtap/examples/process/syscalls_by_proc.stp |
交叉编译:
1 | stap -r <target-kernel-version> \ |
运行预编译模块:
1 | staprun mymodule.ko |
总结
如果只记住这篇文章的几个核心知识点,其实就够了。
第一,SystemTap 是干什么的?
动态追踪正在运行的 Linux 系统,尤其适合深入观察内核行为。
第二,它怎么工作?
1 | SystemTap脚本 |
第三,脚本怎么看?
1 | Probe |
第四,遇到实际问题怎么办?
先找官方示例:
1 | /usr/share/systemtap/examples |
比如:
1 | iotime.stp |
第五,生产服务器不方便编译怎么办?
使用:
交叉插桩。
1 | 编译主机 |
最后再把整个性能分析链条串起来:
flowchart LR
A["发现问题<br/>top"] --> B["CPU热点<br/>perf"]
B --> C["系统调用<br/>strace"]
C --> D["库函数<br/>ltrace"]
D --> E["深入内核<br/>SystemTap"]
E --> F["定位根因"]到这里,SystemTap 就先学到这里。下一篇老李再聊聊eBPF。我们会换一种完全不同的思路,不再重点关注“怎么生成内核模块”,而是直接使用 BCC、bpftrace 等工具,开始真正做:
1 | IO追踪 |
这样从 perf → strace → SystemTap → eBPF,整个 Linux 性能分析工具链就真正串起来了。
