BPF ringbuf vs. BPF perfbuf
参考资料
BPF ring buffer (nakryiko.com)
正文
本文对比了 BPF kernel space 与 user space 间的两种数据传输方法:ringbuf
和 perfbuf
。
perfbuf
:perf 缓冲区为每个 CPU 分配了一个独立的缓冲区,这就引入了如下几个问题
- 内存使用效率低下:大部分时间只有个别 CPU 的缓冲区得到应用,其余缓冲区处于闲置状态,当短时间内大量信息写入某个特定缓冲区时,就会造成数据的溢出,其他 CPU 上的缓冲区无法提供帮助;
- 事件重排序:当 eBPF 程序跟踪的是生命周期这类对象时,事件的顺序就格外重要了;如果相关事件在不同的 CPU 上连续发生(几毫秒内),就可能造成乱序;
- 额外的复制:准备好的数据,首先要复制到一个全局变量,或者 Per-CPU array,然后再复制到
perfbuf
,这样就引入了两次复制,如果perfbuf
空间不足导致数据无法插入缓冲区,那么第一次复制就会被浪费;
ringbuf
:环形缓冲区,维护了一个所有 CPU 共享的缓冲区,解决了上面 perfbuf
面临的问题
- Memory overhead:由于所有 CPU 共享缓冲区,因此在分配同样的大小的缓冲区时,使用
ringbuf
的缓冲区峰值更大,这时某个 CPU 发生连续事件时缓冲区也不会很快被填满;此外,当 CPU 数量增多时,例如从 16 扩展到 32 时,ringbuf
不需要像perfbuf
那样将缓冲区扩大两倍; - Event ordering:由于
ringbuf
只有一个共享的缓冲区,因此当事件 A 比事件 B 提前提交给缓冲区时,那么事件 A 也就会比事件 B 提前写入缓冲区; - Wasted work and extra data copying:
ringbuf
支持reservation/submit
,允许提前预留一块内存供程序直接用于准备数据,这样就节省了一次复制且不会出现复制浪费的问题; - BPF
ringbuf
内部使用非常轻量级的自旋锁,在高并发应用中可能导致数据的丢失;
代码样例
perfbuf
struct bpf_map_def SEC("maps") pb = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
};
struct data {
u32 cwnd;
u32 timestamp;
}
// kern.c
SEC("kprobe/tcp_set_state")
int fun(struct pt_regs *ctx)
{
struct sock *sk = (struct sock*)PT_REGS_PARM1(ctx);
struct tcp_sock *tp = (struct tcp_sock*)sk;
struct data d = {
.timestamp = bpf_ktime_get_ns();
}
bpf_probe_read(&d.cwnd, sizeof*(u32), &tp->snd_cwnd);
bpf_perf_event_output(ctx, &pb, BPF_F_CURRENT_CPU, &d, sizeof(struct data));
return 0;
}
// user.c
static void handle_event (void *ctx, int cpu, void *data, __u32 size) {
struct data d = data;
}
int main()
{
int buff_fd;
struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
buff_fd = bpf_object__find_map_fd_by_name(obj, "pb");
// 新版本为 6 参数,第三个参数写入回调函数,后面三个参数传入 NULL 即可
pb = perf_buffer__new(buff_fd, 8 /* 32KB per CPU */, handle_event, NULL, NULL, NULL);
/* 老版本为 3 参数,
* pb_opts.sample_cb = handle_event;
* pb = perf_buffer__new(buff_fd, 8, &pb_opts);
*/
while ((ret = perf_buffer__poll(pb, 100 /* timeout, ms */)) >= 0) {}
return 0;
}
补充说明
关于 perf_buffer_new
在 5.15 内核中,该函数签名如下:
LIBBPF_API struct perf_buffer *
perf_buffer__new(int map_fd, size_t page_cnt,
const struct perf_buffer_opts *opts);
在 5.17 内核中,该函数改为宏定义,如下所示:
#define perf_buffer__new(...) ___libbpf_overload(___perf_buffer_new, __VA_ARGS__)
#define ___perf_buffer_new6(map_fd, page_cnt, sample_cb, lost_cb, ctx, opts) \
perf_buffer__new(map_fd, page_cnt, sample_cb, lost_cb, ctx, opts)
#define ___perf_buffer_new3(map_fd, page_cnt, opts) \
perf_buffer__new_deprecated(map_fd, page_cnt, opts)
在老版本内核中,perf_buffer__new
函数支持 3 个参数,分别传入 map 的编号,每个 CPU 分配的空间(页面数量),和 perf_buffer_opts
,其中指定了数据到达时的回调函数。
在新版本内核中,perf_buffer__new
为了兼容性改为宏定义,同时支持 3 个参数和 6 个参数,但使用 3 个参数时 libbpf 会提示 warning,6 个参数分别表示 map 编号、每个 CPU 分配的空间、数据到达回调函数、数据丢失回调函数、传递的上下文,以及选项。
ringbuf
struct bpf_map_def SEC("maps") rb = {
.type = BPF_MAP_TYPE_RINGBUF,
.max_entries = 256 * 1024 /* 256 KB */
};
struct data {
u32 cwnd;
u32 timestamp;
}
// kern.c
SEC("kprobe/tcp_set_state")
int fun(struct pt_regs *ctx)
{
struct sock *sk = (struct sock*)PT_REGS_PARM1(ctx);
struct tcp_sock *tp = (struct tcp_sock*)sk;
struct data d = {
.timestamp = bpf_ktime_get_ns();
}
bpf_probe_read(&d.cwnd, sizeof*(u32), &tp->snd_cwnd);
bpf_ringbuf_output(&rb, &d, sizeof(struct data), 0);
return 0;
}
// user.c
int handle_event(void *ctx, void *data, size_t data_sz) {
struct data d = data;
return 0;
}
int main()
{
int buff_fd;
struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
buff_fd = bpf_object__find_map_fd_by_name(obj, "pb");
rb = ring_buffer__new(buff_fd, handle_event, NULL, NULL);
while ((ret = ring_buffer__poll(rb, 100 /* timeout, ms */)) >= 0) {}
return 0;
}
另一个关键点是,内核调用 bpf_ringbuf_output
时不需要提供堆栈上下文信息,如果在 BPF to BPF call
中调用 bpf_ringbuf_output
函数时,就不用一直传递 struct pt_regs *ctx
,也能向用户态发送数据了;
其他更细节的内容请查看 BPF ring buffer (nakryiko.com)