在排查系统 CPU 使用率高的问题时,很可能遇到过这样的困惑:明明通过 top 命令发现系统的 CPU 使用率(特别是用户 CPU 使用率)特别高,但通过 ps、pidstat 等工具都找不出 CPU 使用率高的进程。这是什么原因导致的呢?
一般情况下,这类问题很可能是以下两个原因导致的:
第一,应用程序里面直接调用其他二进制程序,并且这些程序的运行时间很短,通过 top 工具不容易发现;
第二,应用程序自身在不停地崩溃重启中,且重启间隔较短,启动过程中资源的初始化导致了高 CPU 使用率。
使用 top、ps 等性能工具很难发现这类短时进程,这是因为它们都只会按照给定的时间间隔采样,而不会实时采集到所有新创建的进程。那要如何才能采集到所有的短时进程呢?那就是利用 eBPF 的事件触发机制,跟踪内核每次新创建的进程,这样就可以揪出这些短时进程。
要跟踪内核新创建的进程,首先得找到要跟踪的内核函数或跟踪点。如果你了解过 Linux 编程中创建进程的过程,我想你已经知道了,创建一个新进程通常需要调用 fork() 和 execve() 这两个标准函数,它们的调用过程如下图所示:
因为我们要关心的主要是新创建进程的基本信息,而像进程名称和参数等信息都在 execve() 的参数里,所以我们就要找出 execve() 所对应的内核函数或跟踪点。
借助bpftrace 工具,你可以执行下面的命令,查询所有包含 execve 关键字的跟踪点:
sudo bpftrace -l '*execve*'
命令执行后,你会得到如下的输出内容:
kprobe:__ia32_compat_sys_execve
kprobe:__ia32_compat_sys_execveat
kprobe:__ia32_sys_execve
kprobe:__ia32_sys_execveat
kprobe:__x32_compat_sys_execve
kprobe:__x32_compat_sys_execveat
kprobe:__x64_sys_execve
kprobe:__x64_sys_execveat
kprobe:audit_log_execve_info
kprobe:bprm_execve
kprobe:do_execveat_common.isra.0
kprobe:kernel_execve
tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execveat
tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execveat
从输出中,你可以发现这些函数可以分为内核插桩(kprobe)和跟踪点(tracepoint)两类。内核插桩属于不稳定接口,而跟踪点则是稳定接口。因而,在内核插桩和跟踪点两者都可用的情况下,应该选择更稳定的跟踪点,以保证 eBPF 程序的可移植性(即在不同版本的内核中都可以正常执行)。
排除掉 kprobe 类型之后,剩下的 tracepoint:syscalls:sys_enter_execve、tracepoint:syscalls:sys_enter_execveat、tracepoint:syscalls:sys_exit_execve 以及 tracepoint:syscalls:sys_exit_execveat 就是我们想要的 eBPF 跟踪点。其中,sys_enter_ 和 sys_exit_ 分别表示在系统调用的入口和出口执行。
只有跟踪点的列表还不够,因为我们还想知道具体启动的进程名称、命令行选项以及返回值,而这些也都可以通过 bpftrace 来查询。在命令行中执行下面的命令,即可查询:
# 查询sys_enter_execve入口参数
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execve
int __syscall_nr
const char * filename
const char *const * argv
const char *const * envp
# 查询sys_exit_execve返回值
$ sudo bpftrace -lv tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execve
int __syscall_nr
long ret
# 查询sys_enter_execveat入口参数
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_execveat
tracepoint:syscalls:sys_enter_execveat
int __syscall_nr
int fd
const char * filename
const char *const * argv
const char *const * envp
int flags
# 查询sys_exit_execveat返回值
$ sudo bpftrace -lv tracepoint:syscalls:sys_exit_execveat
tracepoint:syscalls:sys_exit_execveat
int __syscall_nr
long ret
从输出中可以看到,sys_enter_execveat() 比 sys_enter_execve() 多了两个参数,而文件名 filename、命令行选项 argv 以及返回值 ret的定义都是一样的。
使用 bpftrace 查询到了 execve 相关的跟踪点,以及这些跟踪点的具体格式。
bpftrace、BCC 和 libbpf 这三种方式各有优缺点,在实际的生产环境中都有大量的应用:
- bpftrace 通常用在快速排查和定位系统上,它支持用单行脚本的方式来快速开发并执行一个 eBPF 程序。不过,bpftrace 的功能有限,不支持特别复杂的 eBPF 程序,也依赖于 BCC 和 LLVM 动态编译执行。
- BCC 通常用在开发复杂的 eBPF 程序中,其内置的各种小工具也是目前应用最为广泛的 eBPF 小程序。不过,BCC 也不是完美的,它依赖于 LLVM 和内核头文件才可以动态编译和加载 eBPF 程序。
- libbpf 是从内核中抽离出来的标准库,用它开发的 eBPF 程序可以直接分发执行,这样就不需要每台机器都安装 LLVM 和内核头文件了。不过,它要求内核开启 BTF 特性,需要非常新的发行版才会默认开启(如 RHEL 8.2+ 和 Ubuntu 20.10+ 等)。