LWN 上有大量关于动态插桩内核代码的文章。借助一种称为用户静态定义跟踪(USDT)探针的特殊机制,这些强大的跟踪能力同样可以应用于用户空间应用程序。USDT探针为用户空间代码插桩提供了一种低开销的方案,是调试生产环境运行中应用的利器。在关于 BPF 和 BCC 的系列文章中,我们将探讨 USDT 探针的由来,以及如何使用它们来洞察您自身应用程序的行为。
USDT 探针的概念最早源于 Sun 公司的 DTrace 工具。虽然 DTrace 并非静态跟踪点的首创者(其原始论文的“相关工作”部分提及了多种早期实现),但它无疑极大地推广了这一技术。随着 DTrace 的普及,许多应用程序开始在关键函数中嵌入 USDT 探针,以辅助运行时行为的跟踪与诊断。因此,这些探针通常需要通过 --enable-dtrace 这类构建配置开关来启用,也就不足为奇了。
例如,MySQL 提供了众多探针,帮助数据库管理员了解连接来源、正在执行的 SQL 命令,以及客户端与服务器间数据传输的底层细节。其他流行的软件,如 Java、PostgreSQL、Node.js,甚至 GNU C 库,也都提供了启用探针的选项,覆盖了从内存分配到垃圾回收等广泛的活动。
在 Linux 系统上,有多个工具可以处理 USDT 探针。SystemTap 是一个受欢迎的选择,也是 DTrace 的替代方案(因为 DTrace 直到近期才在 Linux 上获得支持)。perf 工具对 USDT 探针(内核中称为“静态定义跟踪”)的支持于 v4.8-rc1 版本合并。甚至 LTTng 自 2012 年起就能发出与 USDT 兼容的探针。而开发者工具链中最新、或许也是最用户友好的工具和脚本,则来自 BPF 编译器集合(BCC)。
用于处理 USDT 探针的 BCC 工具
自 2016 年 3 月起,BCC 就开始支持 USDT 探针。tplist 工具允许您查看内核、应用程序或库中可用的探针,并能帮助发现可供 trace 工具启用的探针名称。在一个支持 SDT 的 C 库版本上运行该工具,结果示例如下:
# tplist.py -l /lib64/libc-2.27.so
/lib64/libc-2.27.so libc:setjmp
/lib64/libc-2.27.so libc:longjmp
/lib64/libc-2.27.so libc:longjmp_target
/lib64/libc-2.27.so libc:memory_mallopt_arena_max
/lib64/libc-2.27.so libc:memory_mallopt_arena_test
...
-l 参数指示 tplist 文件参数是库或可执行文件。省略 -l 则会让 tplist 打印内核跟踪点列表。
可以对列表应用过滤器来缩短输出。例如,使用过滤器 sbrk 只会打印名称包含“sbrk”的探针。使用 -vv 参数则会打印探针位置及可用参数:
./tplist.py -vv -l /lib64/libc-2.27.so sbrk
/lib64/libc-2.27.so libc:memory_sbrk_less [sema 0x0]
location #1 0x816dd
argument #1 8 unsigned bytes @ ax
argument #2 8 signed bytes @ bp
/lib64/libc-2.27.so libc:memory_sbrk_more [sema 0x0]
location #1 0x826af
argument #1 8 unsigned bytes @ ax
argument #2 8 signed bytes @ r12
了解参数细节对于知晓哪些寄存器存放了函数参数至关重要。知道参数位置后,我们就可以使用 BCC 的 trace 工具通过类似以下命令打印其内容:
# trace.py 'u:/lib64/libc-2.27.so:memory_sbrk_more "%u", arg1' -T
TIME PID TID COMM FUNC -
21:46:51 12781 12781 ls memory_sbrk_more 114974720
上述输出表明,运行 ls 命令的进程触发了 memory_sbrk_more 探针并记录了一次命中。
BCC 中的其他工具为 Java、Python、Ruby 和 PHP 等高级语言启用了 USDT 探针。例如,lib/ustat.py 是一个监控工具,它整合了方法调用、垃圾回收、对象分配等多种事件信息,并通过类似 top 的界面展示:
# ustat.py
Tracing... Output every 10 secs. Hit Ctrl-C to end
12:17:17 loadavg: 0.33 0.08 0.02 5/211 26284
PID CMDLINE METHOD/s GC/s OBJNEW/s CLOAD/s EXC/s THR/s
3018 node/node 0 3 0 0 0 0
上述输出显示,进程 ID 为 3018 的 node 进程在十秒内触发了三次垃圾回收事件。与大多数脚本一样,ustat.py 会持续运行直至被用户中断。
BCC 还包含针对特定应用的专用脚本。例如,bashreadline.py 可打印所有运行中 bash shell 的命令:
# bashreadline.py
TIME PID COMMAND
05:28:25 21176 ls -l
05:28:28 21176 date
05:28:35 21176 echo hello world
dbslower.py 则用于打印延迟超过指定阈值的数据库(MySQL 或 PostgreSQL)操作,是进行 运维 性能分析的实用工具:
# dbslower.py mysql -m 1000
Tracing database queries for pids 25776 slower than 1000 ms...
TIME(s) PID MS QUERY
1.421264 25776 2002.183 call getproduct(97)
3.572617 25776 2001.381 call getproduct(97)
为您的应用程序添加 USDT 探针
SystemTap 提供了一个用于向应用程序添加静态探针的 API。要创建这些探针,您需要安装 systemtap-sdt-devel 包,该包提供 sys/sdt.h 头文件。以下是一个简单的 C 程序示例,展示了如何添加并使用 BCC 工具来列出和启用探针:
#include <sys/sdt.h>
#include <sys/time.h>
#include <unistd.h>
int main(int argc, char **argv)
{
struct timeval tv;
while(1) {
gettimeofday(&tv, NULL);
DTRACE_PROBE1(test-app, test-probe, tv.tv_sec);
sleep(1);
}
return 0;
}
这个程序会循环运行,每秒触发一次名为 test-probe 的探针,并将当前秒数作为参数传递。DTRACE_PROBEn() 系列宏用于在代码中创建探针点,宏名末尾的数字 n 代表参数个数。
这些宏的实现方式是在探针位置放置一条无操作(no-op)的汇编指令,并在应用程序的 ELF 文件中写入包含探针地址和名称等信息的注释。由于未激活的探针运行时开销仅是一条无操作指令,且 ELF 注释不加载到内存,因此对性能影响极小。
使用 tplist 工具,我们可以查看编译后程序中的探针及其参数:
# tplist.py -vv -l ./simple-c
simple-c test-app:test-probe [sema 0x0]
location #1 0x40057b
argument #1 8 signed bytes @ ax
假设上述 simple-c 程序正在运行,我们可以构造探针说明符,使用 trace 工具来打印其第一个参数:
# trace.py 'u:./simple-c:test-probe "%u", arg1' -T -p $(pidof simple-c)
TIME PID TID COMM FUNC -
21:55:44 13450 13450 simple-c test-probe 1524430544
21:55:45 13450 13450 simple-c test-probe 1524430545
输出的最后一列显示了探针触发时的秒数,即我们在探针中传递的参数。需要注意探针参数的数据类型,因为它会对应到探针说明符中使用的 printf 风格格式字符串。
结论
USDT 探针将内核跟踪点的灵活性延伸到了用户空间应用程序。得益于 DTrace 的推动,许多流行应用和高级编程语言都已支持 USDT 探针。BCC 提供了简洁的工具来操作这些探针,使开发者能够轻松列出可用探针并跟踪打印诊断数据。通过使用 SystemTap 的 API 和 DTRACE_PROBE() 宏系列,您可以在自己的代码中方便地添加探针。USDT 探针能以极小的运行时开销,帮助您在生产环境中有效地对应用程序进行故障排除和深度剖析。
原文来源: https://lwn.net/Articles/753601/