深入淺出 eBPF|你要了解的 7 個核心問題

語言: CN / TW / HK

作者:炎尋

過去一年,ARMS 基於 eBPF 技術打造了 Kubernetes 監控,提供多語言無侵入的應用效能,系統性能,網路效能觀測能力,併發布 Kubernetes 問題排查全景圖,驗證了 eBPF 技術的有效性。eBPF 技術和生態發展很好,未來前景廣大,作為該技術的實踐者,本文目標是通過回答 7 個核心問題介紹 eBPF 技術本身,為大家解開 eBPF 的面紗。

關注【阿里云云原生】公眾號,後臺回覆關鍵詞【K8s全景圖】獲取全景圖高清下載地址!

1.png

eBPF 是什麼

eBPF 是一個能夠在核心執行沙箱程式的技術,提供了一種在核心事件和使用者程式事件發生時安全注入程式碼的機制,使得非核心開發人員也可以對核心進行控制。隨著核心的發展,eBPF 逐步從最初的資料包過濾擴充套件到了網路、核心、安全、跟蹤等,而且它的功能特性還在快速發展中,早期的 BPF 被稱為經典 BPF,簡稱 cBPF,正是這種功能擴充套件,使得現在的 BPF 被稱為擴充套件 BPF,簡稱 eBPF。

eBPF 的應用場景是什麼?

網路優化

eBPF 兼具高效能和高可擴充套件特性,使得其成為網路方案中網路包處理的優選方案:

  • 高效能

JIT 編譯器提供近乎核心原生代碼的執行效率。

  • 高可擴充套件

在核心的上下文裡,可以快速地增加協議解析和路由策略。

故障診斷

eBPF 通過 kprobe,tracepoints 跟蹤機制兼具核心和使用者的跟蹤能力,這種端到端的跟蹤能力可以快速進行故障診斷,與此同時 eBPF 支援以更加高效的方式透出 profiling 的統計資料,而不需要像傳統系統需要將大量的取樣資料透出,使得持續地實時 profiling 成為可能。

2.jpeg

安全控制

eBPF 可以看到所有系統呼叫,所有網路資料包和 socket 網路操作,一體化結合程序上下文跟蹤,網路操作級別過濾,系統呼叫過濾,可以更好地提供安全控制。

效能監控

相比於傳統的系統監控元件比如 sar,只能提供靜態的 counters 和 gauges,eBPF 支援可程式設計地動態收集和邊緣計算聚合自定義的指標和事件,極大地提升了效能監控的效率和想象空間。

eBPF 為什麼會出現?

eBPF 的出現本質上是為了解決核心迭代速度慢和系統需求快速變化的矛盾,在 eBPF 領域常用的一個例子是 eBPF 相對於 Linux Kernel 類似於 Javascript 相對於 HTML,突出的是可程式設計性。一般來說可程式設計性的支援通常會帶來一些新的問題,比如核心模組其實也是為了解決這個問題,但是他沒有提供很好的邊界,導致核心模組會影響核心本身的穩定性,在不同的核心版本需要做適配等。eBPF 採用以下策略,使得其成為一種安全高效地核心可程式設計技術:

  • 安全

eBPF 程式必須被驗證器校驗通過後才能執行,且不能包含無法到達的指令;eBPF 程式不能隨意呼叫核心函式,只能呼叫在 API 中定義的輔助函式;eBPF 程式棧空間最多隻有 512 位元組,想要更大的儲存,就必須要藉助對映儲存。

  • 高效

藉助即時編譯器(JIT),且因為 eBPF 指令依然執行在核心中,無需向用戶態複製資料,大大提高了事件處理的效率。

  • 標準

通過 BPF Helpers,BTF,PERF MAP 提供標準的介面和資料模型供開發者使用。

  • 功能強大

eBPF 不僅擴充套件了暫存器的數量,引入了全新的 BPF 對映儲存,還在 4.x 核心中將原本單一的資料包過濾事件逐步擴充套件到了核心態函式、使用者態函式、跟蹤點、效能事件(perf_events)以及安全控制等領域。

eBPF 怎麼用?

3.png

5 個步驟

1、使用 C 語言開發一個 eBPF 程式;

即插樁點觸發事件時要呼叫的 eBPF 沙箱程式,該程式會在核心態執行。

2、藉助 LLVM 把 eBPF 程式編譯成 BPF 位元組碼;

eBPF 程式編譯成 BPF 位元組碼,用於後續在 eBPF 虛擬機器內驗證並執行。

3、通過 bpf 系統呼叫,把 BPF 位元組碼提交給核心;

在使用者態通過 bpf 系統,將 BPF 位元組碼載入到核心。

4、核心驗證並執行 BPF 位元組碼,並把相應的狀態儲存到 BPF 對映中;

核心驗證 BPF 位元組碼安全,並且確保對應事件發生時呼叫正確的 eBPF 程式,如果有狀態需要儲存,則寫入對應 BPF 對映中,比如監控資料就可以寫到 BPF 對映中。

5、使用者程式通過 BPF 對映查詢 BPF 位元組碼的執行狀態。

使用者態通過查詢 BPF 對映的內容,獲取位元組碼執行的狀態,比如獲取抓取到的監控資料。

一個完整的 eBPF 程式,通常包含使用者態和核心態兩部分:使用者態程式需要通過 BPF 系統呼叫跟核心進行互動,進而完成 eBPF 程式載入、事件掛載以及對映建立和更新等任務;而在核心態中,eBPF 程式也不能任意呼叫核心函式,而是需要通過 BPF 輔助函式完成所需的任務。尤其是在訪問記憶體地址的時候,必須要藉助 bpf_probe_read 系列函式讀取記憶體資料,以確保記憶體的安全和高效訪問。在 eBPF 程式需要大塊儲存時,我們還需要根據應用場景,引入特定型別的 BPF 對映,並藉助它向用戶空間的程式提供執行狀態的資料。

eBPF 程式分類和使用場景

bpftool feature probe | grep program_type

以上命令可以檢視系統支援的 eBPF 程式型別,一般有如下型別:

eBPF program_type socket_filter is available eBPF program_type kprobe is available eBPF program_type sched_cls is available eBPF program_type sched_act is available eBPF program_type tracepoint is available eBPF program_type xdp is available eBPF program_type perf_event is available eBPF program_type cgroup_skb is available eBPF program_type cgroup_sock is available eBPF program_type lwt_in is available eBPF program_type lwt_out is available eBPF program_type lwt_xmit is available eBPF program_type sock_ops is available eBPF program_type sk_skb is available eBPF program_type cgroup_device is available eBPF program_type sk_msg is available eBPF program_type raw_tracepoint is available eBPF program_type cgroup_sock_addr is available eBPF program_type lwt_seg6local is available eBPF program_type lirc_mode2 is NOT available eBPF program_type sk_reuseport is available eBPF program_type flow_dissector is available eBPF program_type cgroup_sysctl is available eBPF program_type raw_tracepoint_writable is available eBPF program_type cgroup_sockopt is available eBPF program_type tracing is available eBPF program_type struct_ops is available eBPF program_type ext is available eBPF program_type lsm is available

具體可參考:

https://elixir.bootlin.com/linux/v5.13/source/include/linux/bpf_types.h 

主要是分為 3 大使用場景:

  • 跟蹤

tracepoint, kprobe, perf_event 等,主要用於從系統中提取跟蹤資訊,進而為監控、排錯、效能優化等提供資料支撐。

  • 網路

xdp, sock_ops, cgroup_sock_addr , sk_msg 等,主要用於對網路資料包進行過濾和處理,進而實現網路的觀測、過濾、流量控制以及效能優化等各種豐富的功能,這裡可以丟包,重定向。

4.png

cilium 基本用了所有的 hook 點。

  • 安全和其他

lsm,用於安全,其他還有 flow_dissector, lwt_in 都是一些不怎麼常用的,不再贅述。

eBPF 的最佳實踐是什麼?

尋找核心的插樁點

從前面可以看出來 eBPF 程式本身並不困難,困難的是為其尋找合適的事件源來觸發執行。對於監控和診斷領域來說,跟蹤類 eBPF 程式的事件源包含 3 類:核心函式(kprobe)、核心跟蹤點(tracepoint)或效能事件(perf_event)。此時有 2 個問題需要回答:

1、核心中都有哪些核心函式、核心跟蹤點或效能事件?

  • 使用除錯資訊獲取核心函式、核心跟蹤點

sudo ls /sys/kernel/debug/tracing/events

  • 使用 bpftrace 獲取核心函式、核心跟蹤點

```

查詢所有核心插樁和跟蹤點

sudo bpftrace -l

使用萬用字元查詢所有的系統呼叫跟蹤點

sudo bpftrace -l 'tracepoint:syscalls:*'

使用萬用字元查詢所有名字包含"open"的跟蹤點

sudo bpftrace -l 'open' ```

  • 使用 perf list 獲取效能事件

sudo perf list tracepoint

2、對於核心函式和核心跟蹤點,在需要跟蹤它們的傳入引數和返回值的時候,又該如何查詢這些資料結構的定義格式呢?

  • 使用除錯資訊獲取

sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/format

使用 bpftrace 獲取

sudo bpftrace -lv tracepoint:syscalls:sys_enter_openat

具體如何使用以上資訊,請參考 bcc。

尋找應用的插樁點

1、如何查詢使用者程序的跟蹤點?

  • 靜態編譯語言通過-g 編譯選項保留除錯資訊,應用程式二進位制會包含 DWARF(Debugging With Attributed Record Format),有了除錯資訊,可以通過  readelf、objdump、nm 等工具,查詢可用於跟蹤的函式、變數等符號列表

```

查詢符號表

readelf -Ws /usr/lib/x86_64-linux-gnu/libc.so.6

查詢USDT資訊

readelf -n /usr/lib/x86_64-linux-gnu/libc.so.6 ```

  • 使用 bpftrace

```

查詢uprobe

bpftrace -l 'uprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:*'

查詢USDT

bpftrace -l 'usdt:/usr/lib/x86_64-linux-gnu/libc.so.6:*' ```

uprobe 是基於檔案的。當檔案中的某個函式被跟蹤時,除非對程序 PID 進行了過濾,預設所有使用到這個檔案的程序都會被插樁。

上面說的是靜態編譯語言,他和核心的跟蹤類似,應用程式的符號資訊可以存放在 ELF 二進位制檔案中,也可以以單獨檔案的形式,放到除錯檔案中;而核心的符號資訊除了可以存放到核心二進位制檔案中之外,還會以  /proc/kallsyms  和  /sys/kernel/debug  等形式暴露到使用者空間。

對於非靜態編譯語言來說,主要是兩種:

1、解釋型語言

使用類似編譯型語言應用程式的跟蹤點查詢方法,查詢它們在直譯器層面的 uprobe 和 USDT 跟蹤點,如何將直譯器層面的行為和應用行為關聯需要相關語言的專家來分析。

2、即時編譯型語言

這類語言的應用原始碼會先編譯為位元組碼,再由即時編譯器(JIT)編譯為機器碼執行,還會有大量的優化,跟蹤難度很大,同解釋型程式語言類似,uprobe 和 USDT 跟蹤只能用在即時編譯器上,從即時編譯器的跟蹤點引數裡面獲取最終應用程式的函式資訊。找出即時編譯器的跟蹤點同應用程式執行之間的關係需要相關語言的專家來分析。

可以參考 BCC 的應用程式跟蹤,使用者程序的跟蹤,本質上是通過斷點去執行 uprobe 處理程式。雖然核心社群已經對 BPF 做了很多的效能調優,跟蹤使用者態函式(特別是鎖爭用、記憶體分配之類的高頻函式)還是有可能帶來很大的效能開銷。因此,我們在使用 uprobe 時,應該儘量避免跟蹤高頻函式。

具體如何使用以上資訊,請參考:

https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#events--arguments****

關聯問題與插樁點

一個理想的狀態是所有問題都清楚應當觀察那些插樁點,但是這個要求技術人員對端到端的軟體棧細節都瞭解十分透徹,一個更加合理的方法是二八法則,將軟體棧資料流的最核心的 80%脈絡抓住,保障出現問題一定會在這個脈絡被發現即可。此時再使用核心棧和使用者棧來檢視具體的呼叫棧即可發現核心問題,比如說發現了網路在丟包,但是不知道為什麼丟,此時我們知道網路丟包一定會呼叫 kfree_skb 核心函式,那麼我們可以通過:

sudo bpftrace -e 'kprobe:kfree_skb /comm=="<your comm>"/ {printf("kstack: %s\n", kstack);}'

發現該函式的呼叫棧:

kstack: kfree_skb+1 udpv6_destroy_sock+66 sk_common_release+34 udp_lib_close+9 inet_release+75 inet6_release+49 __sock_release+66 sock_close+21 __fput+159 ____fput+14 task_work_run+103 exit_to_user_mode_loop+411 exit_to_user_mode_prepare+187 syscall_exit_to_user_mode+23 do_syscall_64+110 entry_SYSCALL_64_after_hwframe+68

那麼就可以回溯上面的函式,看看他們具體是哪一行在什麼條件下呼叫的,就能夠定位到問題。這個方法不僅可以定位問題,也可以用於加深對核心呼叫的理解,比如:

bpftrace -e 'tracepoint:net:* { printf("%s(%d): %s %s\n", comm, pid, probe, kstack()); }'

可以檢視所有網路相關的跟蹤點及其呼叫棧。

eBPF 的實現原理是什麼?

5.png

5 個模組

eBPF 在核心主要由 5 個模組協作:

1、BPF Verifier(驗證器)

確保 eBPF 程式的安全。驗證器會將待執行的指令建立為一個有向無環圖(DAG),確保程式中不包含不可達指令;接著再模擬指令的執行過程,確保不會執行無效指令,這裡通過和個別同學瞭解到,這裡的驗證器並無法保證 100%的安全,所以對於所有 BPF 程式,都還需要嚴格的監控和評審。

2、BPF JIT

將 eBPF 位元組碼編譯成本地機器指令,以便更高效地在核心中執行。

3、多個 64 位暫存器、一個程式計數器和一個 512 位元組的棧組成的儲存模組

用於控制 eBPF 程式的執行,儲存棧資料,入參與出參。

4、BPF Helpers(輔助函式)

提供了一系列用於 eBPF 程式與核心其他模組進行互動的函式。這些函式並不是任意一個 eBPF 程式都可以呼叫的,具體可用的函式集由 BPF 程式型別決定。注意,eBPF 裡面所有對入參,出參的修改都必須符合 BPF 規範,除了本地變數的變更,其他變化都應當使用 BPF Helpers 完成,如果 BPF Helpers 不支援,則無法修改。

bpftool feature probe

通過以上命令可以看到不同型別的 eBPF 程式可以執行哪些 BPF Helpers。

5、BPF Map & context

用於提供大塊的儲存,這些儲存可被使用者空間程式用來進行訪問,進而控制 eBPF 程式的執行狀態。

bpftool feature probe | grep map_type

通過以上命令可以看到系統支援哪些型別的 map。

3 個動作

先說下重要的系統呼叫 bpf:

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

這裡 cmd 是關鍵,attr 是 cmd 的引數,size 是引數大小,所以關鍵是看 cmd 有哪些:

// 5.11核心 enum bpf_cmd { BPF_MAP_CREATE, BPF_MAP_LOOKUP_ELEM, BPF_MAP_UPDATE_ELEM, BPF_MAP_DELETE_ELEM, BPF_MAP_GET_NEXT_KEY, BPF_PROG_LOAD, BPF_OBJ_PIN, BPF_OBJ_GET, BPF_PROG_ATTACH, BPF_PROG_DETACH, BPF_PROG_TEST_RUN, BPF_PROG_GET_NEXT_ID, BPF_MAP_GET_NEXT_ID, BPF_PROG_GET_FD_BY_ID, BPF_MAP_GET_FD_BY_ID, BPF_OBJ_GET_INFO_BY_FD, BPF_PROG_QUERY, BPF_RAW_TRACEPOINT_OPEN, BPF_BTF_LOAD, BPF_BTF_GET_FD_BY_ID, BPF_TASK_FD_QUERY, BPF_MAP_LOOKUP_AND_DELETE_ELEM, BPF_MAP_FREEZE, BPF_BTF_GET_NEXT_ID, BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH, BPF_LINK_CREATE, BPF_LINK_UPDATE, BPF_LINK_GET_FD_BY_ID, BPF_LINK_GET_NEXT_ID, BPF_ENABLE_STATS, BPF_ITER_CREATE, BPF_LINK_DETACH, BPF_PROG_BIND_MAP, };

最核心的就是 PROG,MAP 相關的 cmd,就是程式載入和對映處理。

1、程式載入

呼叫 BPF_PROG_LOAD cmd,會將 BPF 程式載入到核心,但 eBPF 程式並不像常規的執行緒那樣,啟動後就一直執行在那裡,它需要事件觸發後才會執行。這些事件包括系統呼叫、核心跟蹤點、核心函式和使用者態函式的呼叫退出、網路事件,等等,所以需要第 2 個動作。

2、繫結事件

b.attach_kprobe(event="xxx", fn_name="yyy")

以上就是將特定的事件繫結到特定的 BPF 函式,實際實現原理如下:

(1)藉助 bpf 系統呼叫,載入 BPF 程式之後,會記住返回的檔案描述符;

(2)通過 attach 操作知道對應函式型別的事件編號;

(3)根據 attach 的返回值呼叫 perf_event_open 建立效能監控事件;

(4)通過 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,將 BPF 程式繫結到效能監控事件。

3、對映操作

通過 MAP 相關的 cmd,控制 MAP 增刪,然後使用者態基於該 MAP 與核心狀態進行互動。

eBPF 的發展現狀?

核心支援

建議>=4.14

生態

eBPF 的生態自下而上的情況如下:

1、基礎設施

支援 eBPF 基礎能力的發展。

  • Linux Kernal
  • LLVM\

2、開發工具集

主要是用於載入,編譯,除錯 eBPF 程式,不同語言有不同的開發工具集:

3、eBPF 應用

提供一套開發工具和指令碼。

基於 bcc,提供一個指令碼語言。

網路優化和安全

網路安全

高效能 4 層負載均衡

可觀測

可觀測

可觀測

排程 bpftrace 指令碼

分散式環境下啟動和管理 eBPF 程式的平臺

動態 linux trace

Linux 執行時安全監測

4、跟蹤生態的網站

寫在最後

用好 eBPF 的前提是對軟體棧的理解

通過上面的介紹,相信大家對 eBPF 已經有了足夠的理解,eBPF 提供的只是一個框架和機制,核心還是需要用 eBPF 的人對軟體棧的理解,找到合適的插樁點,能夠和應用問題進行關聯。

eBPF 的殺手鐗是全覆蓋,無侵入,可程式設計

1、全覆蓋

核心,應用程式插樁點全覆蓋。

2、無侵入

不需要修改任何被 hook 的程式碼。

3、可程式設計

動態下發 eBPF 程式,邊緣動態執行指令,動態聚合分析。

團隊資訊

阿里雲可觀測團隊,覆蓋前端監控、應用監控、容器監控、Prometheus、鏈路追蹤、智慧告警、運維視覺化等多個技術領域及產品,沉澱阿里雲可觀測在不同行業、不同技術場景的可觀測解決方案與最佳實踐。

阿里雲 Kubernetes 監控是一套基於 eBPF 技術,針對 Kubernetes 叢集開發的一站式無侵入式可觀測性產品,基於 Kubernetes 叢集下的指標、應用鏈路、日誌和事件,旨在為 IT 開發運維人員提供整體的可觀測性方案。

介紹:

https://help.aliyun.com/document_detail/260777.html

接入:

https://help.aliyun.com/document_detail/251852.html