作者簡介
趙亞楠,攜程資深架構師,負責攜程云平臺網絡虛擬化、云原生安全、內核等基礎設施研發工作。
譯者序本文翻譯自 BPF 核心開發者 Andrii Nakryiko 2020 的一篇文章:BPF ring buffer。
文章介紹了 BPF ring buffer 解決的問題及背后的設計,并給出了一些代碼示例和內核 patch 鏈接,深度和廣度兼備,是學習 ring buffer 的極佳參考。
由于譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。
目錄
譯者序1 ringbuf 相比 perfbuf 的改進 1.1 降低內存開銷(memory overhead) 1.2 保證事件順序(event ordering) 1.3 減少數據復制(wasted data copy)2 ringbuf 使用場景和性能 2.1 常規場景 2.2 高吞吐場景 2.3 不可掩碼中斷(non-maskable interrupt)場景 2.4 小結3 示例程序(show me the code) 3.1 perfbuf 示例 內核 BPF 程序 用戶空間程序 3.2 ringbuf 示例 內核 BPF 程序 用戶空間程序 3.3 ringbuf reserve/commit API 示例 原理 限制 內核 BPF 程序 用戶空間程序4 ringbuf 事件通知控制 4.1 事件通知開銷 4.2 perbuf 解決方式 4.3 ringbuf 解決方式5 總結其他相關資料(譯注)
以下是譯文。
很多場景下,BPF 程序都需要將數據發送到用戶空間(userspace), BPF perf buffer(perfbuf)是目前這一過程的事實標準,但它存在一些問題,例如 浪費內存(因為其 per-CPU 設計)、事件順序無法保證等。
作為改進,內核 5.8 引入另一個新的 BPF 數據結構:BPF ring buffer(環形緩沖區,ringbuf),
-
相比 perf buffer,它內存效率更高、保證事件順序,性能也不輸前者;
-
在使用上,既提供了與 perf buffer 類似的 API ,以方便用戶遷移;又提供了一套新的reserve/commit API(先預留再提交),以實現更高性能。
此外,實驗與真實環境的壓測結果都表明,從 BPF 程序發送數據給用戶空間時, 應該首選 BPF ring buffer。
1.ringbuf 相比perfbuf的改進
perfbuf 是 per-CPU 環形緩沖區(circular buffers),能實現高效的 “內核-用戶空間”數據交互,在實際中也非常有用,但 per-CPU 的設計 導致兩個嚴重缺陷:
-
內存使用效率低下(inefficient use of memory)
-
事件順序無法保證(event re-ordering)
因此內核 5.8 引入了 ringbuf 來解決這個問題。ringbuf 是一個“多生產者、單消費者”(multi-producer, single-consumer,MPSC) 隊列,可安全地在多個 CPU 之間共享和操作。perfbuf 支持的一些功能它都支持,包括,
-
可變長數據(variable-length data records);
-
通過 memory-mapped region 來高效地從 userspace 讀數據,避免內存復制或系統調用;
-
支持 epoll notifications 和 busy-loop 兩種獲取數據方式。
此外,它還解決了 perfbuf 的下列問題:
-
可變長數據(variable-length data records);
-
通過 memory-mapped region 來高效地從 userspace 讀數據,避免內存復制或系統調用;
-
支持 epoll notifications 和 busy-loop 兩種獲取數據方式。
下面具體來看。
1.1 降低內存開銷(memory overhead)
perfbuf 為每個 CPU 分配一個獨立的緩沖區,這意味著開發者通常需要 在內存效率和數據丟失之間做出折中:
-
越大的 per-CPU buffer 越能避免丟數據,但也意味著大部分時間里,大部分內存都是浪費的;
-
盡量小的 per-CPU buffer 能提高內存使用效率,但在數據量陡增(毛刺)時將導致丟數據。
對于那些大部分時間都比較空閑、周期性來一大波數據的場景, 這個問題尤其突出,很難在兩者之間取得一個很好的平衡。
ringbuf 的解決方式是分配一個所有 CPU 共享的大緩沖區,
-
“大緩沖區”意味著能更好地容忍數據量毛刺
-
“共享”則意味著內存使用效率更高
另外,ringbuf 內存效率的擴展性也更好,比如 CPU 數量從 16 增加到 32 時,
-
perfbuf 的總 buffer 會跟著翻倍,因為它是 per-CPU buffer;
-
ringbuf 的總 buffer 不一定需要翻倍,就足以處理擴容之后的數據量。
1.2 保證事件順序(event ordering)
如果 BPF 應用要跟蹤一系列關聯事件(correlated events),例如進程的啟動和終止、 網絡連接的生命周期事件等,那保持事件的順序就非常關鍵。perfbuf 在這種場景下有一些問題:如果這些事件發生的間隔非常短(幾毫秒)并且分散 在不同 CPU 上,那事件的發送順序可能就會亂掉 ——這同樣是 perbuf 的 per-CPU 特性決定的。
舉個真實例子,幾年前我寫的一個應用需要跟蹤進程 fork/exec/exit 事件,收集進程級別(per-process)的資源使用量。BPF 程序將這些事件 寫入 perfbuf,但它們到達的順序經常亂掉。這是因為內核調度器在不同 CPU 上調度進程時, 對于那些存活時間很短的進程,fork(), exec(), and exit() 會在極短的時間內在不同 CPU 上執行。這里的問題很清楚,但要解決這個問題,就需要在應用邏輯中加入大量的判斷和處理, 只有親自做過才知道有多復雜。
但對于 ringbuf 來說,這根本不是問題,因為它是共享的同一個緩沖區。ringbuf 保證 如果事件 A 發生在事件 B 之前,那 A 一定會先于 B 被提交,也會在 B 之前被消費。這個特性顯著簡化了應用處理邏輯。
1.3 減少數據復制(wasted data copy)
BPF 程序使用 perfbuf 時,必須先初始化一份事件數據,然后將它復制到 perfbuf, 然后才能發送到用戶空間。這意味著數據會被復制兩次:
-
第一次:復制到一個局部變量(a local variable)或 per-CPU array (BPF 的棧空間很小,因此較大的變量無法放到棧上,后面有例子)中;
-
第二次:復制到perfbuf中。
更糟糕的是,如果 perfbuf 已經沒有足夠空間放數據了,那第一步的復制完全是浪費的。
BPF ringbuf 提供了一個可選的 reservation/submit API 來避免這種問題。
-
首先申請為數據預留空間(reserve the space),
-
預留成功后,
-
應用就可以直接將準備發送的數據放到 ringbuf 了,從而節省了 perfbuf 中的第一次復制,
-
將數據提交到用戶空間將是一件極其高效、不會失敗的操作,也不涉及任何額外的內存復制。
-
-
如果因為 buffer 沒有空間而預留失敗了,那 BPF 程序馬上就能知道,從而也不用再 執行 perfbuf 中的第一步復制。
后面會有具體例子。
2 ringbuf 使用場景和性能
2.1 常規場景
對于所有實際場景(尤其是那些基于bcc/libbpf 的默認配置在使用 perfbuf 的場景), ringbuf 的性能都優于 perfbuf 性能。各種不同場景的仿真壓測(synthetic benchmarking) 結果見內核 patch。
2.2 高吞吐場景
Per-CPU buffer 特性的 perfbuf 在理論上能支持更高的數據吞吐, 但這只有在每秒百萬級事件(millions of events per second)的場景下才會顯現。
在編寫了一個真實場景的高吞吐應用之后,我們證實了 ringbuf 在作為與 perfbuf 類似的 per-CPU buffer 使用時,仍然可以作為 perfbuf 的一個高性能替代品,尤其是用到手動管理事件通知(manual data availability notification)機制時。
-
BPF side
-
user-space side
2.3 不可掩碼中斷(non-maskable interrupt)場景
唯一需要注意、最好先試驗一下的場景:BPF 程序必須在 NMI (non-maskable interrupt) context 中執行時,例如處理 cpu-cycles 等 perf events 時。
ringbuf 內部使用了一個非常輕量級的 spin-lock,這意味著如果 NMI context 中有競爭,data reservation 可能會失敗。因此,在 NMI context 中,如果 CPU 競爭非常嚴重,可能會 導致丟數據,雖然此時 ringbuf 仍然有可用空間。
2.4 小結
除了 NMI context 之外,在其他所有場景中優先選擇 ringbuf 而不是 perfbuf 都是非常明智的。
3 示例程序(show me the code)
完整代碼見 bpf-ringbuf-examples project。
BPF 程序的功能是 trace 所有進程的 exec() 操作,也就是創建新進程事件。
每次 exec() 事件:收集進程 ID (pid)、進程名字 (comm)、可執行文件路徑 (filename),然后發送給用戶空間程序;用戶空間簡單通過 printf() 打印輸出。用三種不同方式實現,輸出都類似:
$ sudo ./ringbuf-reserve-commit # or ./ringbuf-output, or ./perfbuf-output
TIME EVENT PID COMM FILENAME
1939 EXEC 3232062 sh /bin/sh
1939 EXEC 3232062 timeout /usr/bin/timeout
1939 EXEC 3232063 ipmitool /usr/bin/ipmitool
1939 EXEC 3232065 env /usr/bin/env
1939 EXEC 3232066 env /usr/bin/env
1939 EXEC 3232065 timeout /bin/timeout
1939 EXEC 3232066 timeout /bin/timeout
1939 EXEC 3232067 sh /bin/sh
1939 EXEC 3232068 sh /bin/sh
^C
事件的結構體定義:
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 512
// BPF 程序發送給 userspace 的事件
struct event {
int pid;
char comm[TASK_COMM_LEN];
char filename[MAX_FILENAME_LEN];
};
這里有意讓這個結構體的大小超過 512 字節,這樣 event 變量就無法 放到 BPF 棧空間(max 512Byte)上,后面會看到 perfbuf 和 ringbuf 程序分別怎么處理。
3.1 perfbuf 示例
內核 BPF 程序
// 聲明一個 perfbuf map。幾點注意:
// 1. 不用特意設置 max_entries,libbpf 會自動將其設置為 CPU 數量;
// 2. 這個 map 的 per-CPU buffer 大小是 userspace 設置的,后面會看到
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); // perf buffer (array)
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
} pb SEC(".maps");
// 一個 struct event 變量的大小超過了 512 字節,無法放到 BPF 棧上,
// 因此聲明一個 size=1 的 per-CPU array 來存放 event 變量
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); // per-cpu array
__uint(max_entries, 1);
__type(key, int);
__type(value, struct event);
} heap SEC(".maps");
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
unsigned fname_off = ctx->__data_loc_filename & 0xFFFF;
struct event *e;
int zero = 0;
e = bpf_map_lookup_elem(&heap, &zero);
if (!e) /* can't happen */
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
// 發送事件,參數列表
bpf_perf_event_output(ctx, &pb, BPF_F_CURRENT_CPU, e, sizeof(*e));
return 0;
}
用戶空間程序
完整代碼 the user-space side, 基于 BPF skeleton(更多信息見 這里)。
看一個關鍵點:使用 libbpf user-space perfbuffer_new() API 來創建一個 perf buffer consumer:
struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
struct perfbuf_output_bpf *skel;
/* Set up ring buffer polling */
pb_opts.sample_cb = handle_event;
pb = perf_buffer__new(bpf_map__fd(skel->maps.pb), 8 /* 32KB per CPU */, &pb_opts);
這里設置 per-CPU buffer 為 32KB, 注意其中的 8 表示的是 number of memory pages,每個 page 是 4KB,因此總大小:8 pages x 4096 byte/page = 32KB。
3.2 ringbuf 示例
完整代碼:
-
BPF-side code
-
user-space code
內核 BPF 程序
bpf_ringbuf_output()
在設計上遵循了bpf_perf_event_output()
的語義, 以使應用從 perfbuf 遷移到 ringbuf 時更容易。為了看出二者有多相似,這里展示下 兩個示例代碼的 diff。
--- src/perfbuf-output.bpf.c 2020-10-25 1822.247019800 -0700
+++ src/ringbuf-output.bpf.c 2020-10-25 1814.510630322 -0700
@@ -6,12 +6,11 @@
char LICENSE[] SEC("license") = "Dual BSD/GPL";
-/* BPF perfbuf map */
+/* BPF ringbuf map */
struct {
- __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
- __uint(key_size, sizeof(int));
- __uint(value_size, sizeof(int));
-} pb SEC(".maps");
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+ __uint(max_entries, 256 * 1024 /* 256 KB */);
+} rb SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
@@ -35,7 +34,7 @@
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
- bpf_perf_event_output(ctx, &pb, BPF_F_CURRENT_CPU, e, sizeof(*e));
+ bpf_ringbuf_output(&rb, e, sizeof(*e), 0);
return 0;
}
只有兩個小改動:
-
ringbuf map 的大小(max_entries)可以在 BPF 側指定了,注意這是所有 CPU 共享的大小。
-
在 userspace 側來設置(或 override) max_entries 也是可以的,API 是 bpf_map__set_max_entries();
-
max_entries的單位是字節,必須是內核頁大小( 幾乎永遠是 4096)的倍數,也必須是 2 的冪次。
-
bpf_perf_event_output()替換成了類似的bpf_ringbuf_output(),后者更簡單,不需要 BPF context 參數。
用戶空間程序
事件 handler 簽名有點變化:
-
會返回錯誤信息(進而終止 consumer 循環)
-
參數里面去掉了產生這個事件的 CPU Index
-void handleevent(void *ctx, int cpu, void *data, unsigned int datasz)
+int handleevent(void *ctx, void *data, sizet data_sz)
{
const struct event *e = data;
struct tm *tm;
如果 CPU index 對你很重要,那你需要自己在 BPF 代碼中記錄它。
另外,ringbuffer API 不提供丟失數據(lost samples)的回調函數,而 perfbuffer 是支持的。如果需要這個功能,必須自己在 BPF 代碼中處理。這樣的設計對于一個(所有 CPU)共享的 ring buffer 能最小化鎖競爭, 同時也避免了為不需要的功能買單:在實際中,這功能除了能用戶在 userspace 打印出有數據丟失之外,其他基本也做不了什么, 而類似的目的在 BPF 中可以更顯式和高效地完成。
第二個不同是 ringbuffer_new() API 更加簡潔:
/* Set up ring buffer polling */
- pb_opts.sample_cb = handle_event;
- pb = perf_buffer__new(bpf_map__fd(skel->maps.pb), 8 /* 32KB per CPU */, &pb_opts);
- if (libbpf_get_error(pb)) {
+ rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
+ if (!rb) {
err = -1;
- fprintf(stderr, "Failed to create perf buffer ");
+ fprintf(stderr, "Failed to create ring buffer ");
goto cleanup;
}
接下來基本上就是文本替換一下的事情了:perf_buffer__poll()
-ring_buffer__poll()
printf("%-8s %-5s %-7s %-16s %s ",
"TIME", "EVENT", "PID", "COMM", "FILENAME");
while (!exiting) {
- err = perf_buffer__poll(pb, 100 /* timeout, ms */);
+ err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR) {
err = 0;
break;
}
if (err < 0) {
- printf("Error polling perf buffer: %d ", err);
+ printf("Error polling ring buffer: %d ", err);
break;
}
}
3.3 ringbuf reserve/commit API 示例
bpf_ringbuf_output()
API 的目的是確保從 perfbuf 到 ringbuf 遷移時無需對 BPF 代 碼做重大改動,但這也意味著它繼承了 perfbuf API 的一些缺點:
-
額外的內存復制(extra memory copy)
這意味著需要額外的空間來構建 event 變量,然后將其復制到 buffer。不僅低效, 而且經常需要引入只有一個元素的 per-CPU array,增加了不必要的處理復雜性。
-
非常晚的 buffer 空間申請(data reservation)
如果這一步失敗了(例如由于用戶空間消費不及時導致 buffer 滿了,或者有大量 突發事件導致 buffer 溢出了),那上一步的工作將變得完全無效,浪費內存空間和計算資源。
原理
如果能提前知道事件將在第二步被丟棄,就無需做第一步了, 節省一些內存和計算資源,消費端反而因此而消費地更快一些。但 xxx_output()風格的API 是無法實現這個目的的。這就是為什么引入了新的bpfringbufreserve()/bpfringbufcommit() API。
-
提前預留空間,或者能立即發現沒有可以空間了(返回
NULL
); -
預留成功后,一旦數據寫好了,將它發送到 userspace 是一個不會失敗的操作。
也就是說只要
bpf_ringbuf_reserve()
返回非空,那隨后的bpf_ringbuf_commit()
就永遠會成功,因此它沒有返回值。
另外,ring buffer 中預留的空間在被提交之前,用戶空間是看不到的, 因此 BPF 程序可以從容地組織自己的 event 數據,不管它有多復雜、需要多少步驟。這種方式也避免了額外的內存復制和臨時存儲空間(extra memory copying and temporary storage spaces)。
限制
唯一的限制是:BPF 校驗器在校驗時(at verification time), 必須知道預留數據的大小 (size of the reservation),因此不支持動態大小的事件數據。
-
對于動態大小的數據,用戶只能退回到用
bpf_ringbuf_output()
方式來提交,忍受額外的數據復制開銷; -
其他所有情況下,reserve/commit API 都應該是首選。
內核 BPF 程序
-
BPF
-
user-space
--- src/ringbuf-output.bpf.c 2020-10-25 1814.510630322 -0700
+++ src/ringbuf-reserve-submit.bpf.c 2020-10-25 1853.409470270 -0700
@@ -12,29 +12,21 @@
__uint(max_entries, 256 * 1024 /* 256 KB */);
} rb SEC(".maps");
-struct {
- __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
- __uint(max_entries, 1);
- __type(key, int);
- __type(value, struct event);
-} heap SEC(".maps");
-
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
unsigned fname_off = ctx->__data_loc_filename & 0xFFFF;
struct event *e;
- int zero = 0;
- e = bpf_map_lookup_elem(&heap, &zero);
- if (!e) /* can't happen */
+ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+ if (!e)
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
- bpf_ringbuf_output(&rb, e, sizeof(*e), 0);
+ bpf_ringbuf_submit(e, 0);
return 0;
}
用戶空間程序
用戶空間代碼與之前的 ringbuf output API 完全一樣,因為這個 API 涉及到的只是提交方(生產方), 消費方還是一樣的方式來消費。
4 ringbuf 事件通知控制
4.1 事件通知開銷
在高吞吐場景中,最大的性能損失經常來自提交數據時,內核的信號通知開銷(in-kernel signalling of data availability) ,也就是內核的 poll/epoll 通知阻塞在讀數據上的 userspace handler 接收數據。
這一點對 perfbuf 和 ringbuf 都是一樣的。
4.2 perbuf 解決方式
perfbuf 處理這種場景的方式是提供了一個采樣通知(sampled notification)機制:每 N 個事件才會發送一次通知。用戶空間創建 perfbuf 時可以指定這個參數。
這種機制能否解決問題,因具體場景而異。
4.3 ringbuf 解決方式
ringbuf 選了一條不同的路:bpfringbufoutput() 和 bpfringbufcommit() 都支持一個額外的 flags 參數,
-
BPF_RB_NO_WAKEUP
:不觸發通知 -
BPF_RB_FORCE_WAKEUP
:會觸發通知
基于這個 flags,用戶能實現更加精確的通知控制。例子見 BPF ringbuf benchmark。
默認情況下,如果沒指定任何 flag,ringbuf 會采用自適應通知 (adaptive notification)機制,根據 userspace 消費者是否有滯后(lagging)來動態 調整通知間隔,盡量確保 userspace 消費者既不用承擔額外開銷,又不丟失任何數據。這種默認配置在大部分場景下都是有效和安全的,但如果想獲得極致性能,那 顯式控制數據通知就是有必要的,需要結合具體應用場景和處理邏輯來設計。
5 總結
本文介紹了 BPF ring buffer 解決的問題及其背后的設計。
文中給出的示例代碼和內核代碼鏈接,展示了 ringbuf API 的基礎和高級用法。希望閱讀本文之后,讀者能對 ringbuf 有一個很好的理解和把握,能根據自己的具體應用 選擇合適的 API 來使用。
其他相關資料(譯注)
內核文檔,BPF ring buffer
有一些更細節的設計與實現,可作為本文補充。
原文標題:[譯] BPF ring buffer:使用場景、核心設計及程序示例
文章出處:【微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
-
內存
+關注
關注
8文章
3020瀏覽量
74008 -
程序
+關注
關注
117文章
3785瀏覽量
81006 -
BPF
+關注
關注
0文章
25瀏覽量
4002
原文標題:[譯] BPF ring buffer:使用場景、核心設計及程序示例
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論