使用 ebpf USDT 追踪用户态程序
Lapin Gris Lv3

USDT 是一种探针技术,让用户态程序也支持使用 perf/ebpf 性能分析工具进行 tracing/profiling。做性能分析通常听过 perf/ebpf 这些工具,这些工具通常用在内核上,用于分析内核的一些性能问题,如果是用户态程序也遇到性能问题了,我们希望用户态程序也能使用 perf/ebpf 工具进行 tracing/profiling。那么就需要 USDT 技术了。

简单来说,USDT 就是探针技术,我们在源码里插入一个固定的探针。当我们 tracing 这个探针的时候,就获取我们埋点的数据了。

USDT 如何插入探针?

插入探针,有时候也叫做埋点。只需要包含 <sys/sdt.h> 头文件,然后利用 DTRACE_PROBE 系列函数埋点即可。

DTRACE_PROBE 系列函数定义在 /usr/include/sys/sdt.h

/* DTrace compatible macro names.  */
DTRACE_PROBE(provider,probe)
DTRACE_PROBE1(provider,probe,parm1)
DTRACE_PROBE2(provider,probe,parm1,parm2)
DTRACE_PROBE3(provider,probe,parm1,parm2,parm3)
[...]

第一次看到DTRACE_PROBE1, 2, 3, 4 … 可能会有些懵,这都是啥呀?!

实际上很简单,而且用法也很简单,我用 DPDK l2fwd 举个例子,
https://git.dpdk.org/dpdk/tree/examples/l2fwd


static void l2fwd_main_loop(void) {

for (i = 0; i < qconf->n_rx_port; i++) {

portid = qconf->rx_port_list[i];
nb_rx = rte_eth_rx_burst(portid, 0, pkts_burst, MAX_PKT_BURST);

printf("portid: %d, queueid: %d, rcvd: %d\n", portid, 0, nb_rx);
}
}

上面的代码是一个利用 DPDK l2 fwd 里的示例代码, rte_eth_rx_burst 函数从网卡收包,我们想知道本次收包收了多少个,于是我们打了一条 log。

这样实现很不优雅,首先是在 datapath 打 log 是错误的决定。其次,我们只希望在 tracing 的时候打印结果,不 tracing 的时候不打印结果。使用 打 log 的方式是无法实现这种效果的,我们需要一个开关,能动态打开和关闭 tracing 的开关,当我们 attach 到这个探针时候,我们可以观测到数据,当我们 detach 的时候,就去掉这个探针,不增加额外的观测消耗。

USDT 可以实现动态开关的功能。我们只需要添加 <sys/sdt.h> 头文件,将 log 语句替换为 DTRACE_PROBE3 语句即可。DTRACE_PROBE5 的数字 3 代表 3 个观测参数,前两个是标识参数,第一个通常用 appname 填充,第二个通常用函数名称填充。当我们使用 perf、ebpf 等观测工具时,通过这两个参数定位我们预先埋下的观测点。

#include <sys/sdt.h>

static void l2fwd_main_loop(void) {

for (i = 0; i < qconf->n_rx_port; i++) {

portid = qconf->rx_port_list[i];
nb_rx = rte_eth_rx_burst(portid, 0, pkts_burst, MAX_PKT_BURST);

DTRACE_PROBE3(l2fwd, l2fwd_main_loop, portid, 0, rcvd);
}
}

查看可用的探针

查看可用探针

bpftrace -l "usdt:/usr/bin/l2fwd:*"

查看正在运行的进程的可用探针,会同时展示其链接的动态库的可用探针

bpftrace -lp $(pidof l2fwd) "usdt:*"

USDT 使用

bpftrace -e 'usdt:/usr/bin/l2fwd:l2fwd_main_loop { printf("%d,%d,%d\n",arg0,arg1,arg2); }'

bpftrace 内置许多参数,比如 comm, pid, cpu, elapsed,利用这些参数可以实现更高级的 tracing。
还可以通过 bcc/libbpf 将一段时间的数据收集起来,进行统计,得出更宏观的指标,这样有一定的代码量。bpftrace 的优势就是一行,很简单。

USDT 原理

包含 USDT 的代码经过编译后生成的二进制文件将有一个名为 .stapsdt.base 的 ELF 段。这个 .stapsdt.base 有助于 tracing 工具在二进制文件加载到内存后计算探针的内存地址。

# readelf -S l2fwd | grep sdt
[17] .stapsdt.base PROGBITS 00000000004053d0 000053d0
[28] .note.stapsdt NOTE 0000000000000000 000061fc

还有一个 ELF note,note 记录了探针的相关信息(名称、地址、信号量、参数)。用户态进程可以读取这个段获得探针的信息。

Displaying notes found in: .note.stapsdt
Owner Data size Description
stapsdt 0x00000042 NT_STAPSDT (SystemTap probe descriptors)
Provider: l2fwd
Name: l2fwd_main_loop
Location: 0x0000000000402596, Base: 0x00000000004053d0, Semaphore: 0x0000000000000000
Arguments: 4@%ebp -4@$0 4@%ecx

USDT 动态开关

反汇编二进制,发现 DTRACE_PROBE 被编译为一条 nop 指令,当不进行 probe 时候,就是执行一条 nop 指令,当进行 probe 的时候替换为 int3 进行跳转到 hook 函数。
image.png