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

2022.06.18
深入淺出eBPF|你要了解的七個核心問題 原創

eBPF是一個能夠在內核運行沙箱程序的技術,提供了一種在內核事件和用戶程序事件發生時安全注入代碼的機制,使得非內核開發人員也可以對內核進行控制。

作者| 炎尋

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

eBPF是什麼?

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

eBPF的應用場景是什麼?

網絡優化

eBPF兼具高性能和高可擴展特性,使得其成為網絡方案中網絡包處理的優選方案:

  • 高性能

JIT編譯器提供近乎內核本地代碼的執行效率。

  • 高可擴展

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

故障診斷

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

圖片



安全控制

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怎麼用?

圖片

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
  • 1.

以上命令可以查看系統支持的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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

具體可參考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等,主要用於對網絡數據包進行過濾和處理,進而實現網絡的觀測、過濾、流量控制以及性能優化等各種豐富的功能,這裡可以丟包,重定向。

圖片

cilium基本用了所有的hook點。

  • 安全和其他

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

eBPF的最佳實踐是什麼?

尋找內核的插樁點

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

1.內核中都有哪些內核函數、內核跟踪點或性能事件?

  • 使用調試信息獲取內核函數、內核跟踪點
sudo ls /sys/kernel/debug/tracing/events
  • 1.
  • 使用bpftrace獲取內核函數、內核跟踪點

# 查询所有内核插桩和跟踪点
sudo bpftrace -l

# 使用通配符查询所有的系统调用跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'

# 使用通配符查询所有名字包含"open"的跟踪点
sudo bpftrace -l '*open*'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 使用perf list獲取性能事件
sudo perf list tracepoint
  • 1.

2.對於內核函數和內核跟踪點,在需要跟踪它們的傳入參數和返回值的時候,又該如何查詢這些數據結構的定義格式呢?

  • 使用調試信息獲取
sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/format
  • 1.
  • 使用bpftrace獲取

具體如何使用以上信息,請參考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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 使用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:*'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

uprobe 是基於文件的。當文件中的某個函數被跟踪時,除非對進程PID 進行了過濾,默認所有使用到這個文件的進程都會被插樁。

上面說的是靜態編譯語言,他和內核的跟踪類似,應用程序的符號信息可以存放在ELF 二進製文件中,也可以以單獨文件的形式,放到調試文件中;而內核的符號信息除了可以存放到內核二進製文件中之外,還會以 /proc/kallsyms 和 /sys/kernel/debug 等形式暴露到用戶空間。

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

  • 解釋型語言

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

  • 即時編譯型語言

這類語言的應用源代碼會先編譯為字節碼,再由即時編譯器(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);}'
  • 1.

發現該函數的調用棧:

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
  • 1.

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

可以查看所有網絡相關的跟踪點及其調用棧。

eBPF的實現原理是什麼?

圖片

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不支持,則無法修改。

通過以上命令可以看到不同類型的eBPF程序可以運行哪些BPF Helpers。

5.BPF Map & context

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

bpftool feature probe
  • 1.

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

3個動作

先說下重要的系統調用bpf:

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

這裡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,
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

最核心的就是PROG,MAP相關的cmd,就是程序加載和映射處理。

1.程序加載

調用BPF_PROG_LOAD cmd,會將BPF程序加載到內核,但eBPF 程序並不像常規的線程那樣,啟動後就一直運行在那裡,它需要事件觸發後才會執行。這些事件包括系統調用、內核跟踪點、內核函數和用戶態函數的調用退出、網絡事件,等等,所以需要第2個動作。

2.綁定事件

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

以上就是將特定的事件綁定到特定的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程序,不同語言有不同的開發工具集:

  • Go

https://github.com/cilium/ebpf

https://github.com/aquasecurity/libbpfgo

  • C/C++

https://github.com/libbpf/libbpf

3、eBPF應用

  • bcc(https://github.com/iovisor/bcc)

提供一套開發工具和腳本。

  • bpftrace(https://github.com/iovisor/bpftrace)

基於bcc,提供一個腳本語言。

  • cilium(https://github.com/cilium/cilium)

網絡優化和安全

  • Falco(https://github.com/falcosecurity/falco)

網絡安全

  • Katran(https://github.com/facebookincubator/katran)

高性能4層負載均衡

  • Hubble(https://github.com/cilium/hubble)

可觀測

  • Kindling(https://github.com/CloudDectective-Harmonycloud/kindling)

可觀測

  • Pixie(https://github.com/pixie-io/pixie)

可觀測

  • kubectl trace(https://github.com/iovisor/kubectl-trace)

調度bpftrace腳本

  • L3AF(https://github.com/l3af-project/l3afd)

分佈式環境下啟動和管理eBPF程序的平台

  • ply(https://github.com/iovisor/ply)

動態linux trace

  • Tracee(https://github.com/aquasecurity/tracee)

Linux運行時安全監測

4、跟踪生態的網站

  • https://ebpf.io/projects
  • https://github.com/zoidbergwill/awesome-ebpf

寫在最後

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

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

eBPF的殺手鐧是全覆蓋,無侵入,可編程

1.全覆蓋

內核,應用程序插樁點全覆蓋。

2.無侵入

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

3.可編程

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