後起之秀 -network policy 之 eBPF 實現

語言: CN / TW / HK

這篇是 Network Policy 最後一篇,主題是關於 eBPF。前面兩篇,我們聊完了 Network Policy 的意義和 iptables 實現,今天我們聊聊如何藉助 eBPF 來擺脫對 iptables 的依賴,並實現 Network Policy。

文章正常是週四更,今年中秋已過去,提前祝大家國慶快樂,來年中秋更快樂!

前世

eBPF 的前世是 BPF。1992 年,Steven McCanne 和 Van Jacobson 寫了一篇論文“The BSD Packet Filter:A New Architecture for User-Level Packet Capture”。在這篇文章裡,作者描述了他們在 Unix Kernel 裡是如何利用 BPF 來過濾網路包的,他們的實現比當時主流的方法快 20 倍。

新方法主要包含了兩個創新:

  • 一個工作在核心態的輕量級虛擬機器,它可以與 CPU 暫存器完美契合工作。

  • 為每個 application 引入了一個專屬的 buffer,應用只需要關心與自己相關的 package 即可。

這個令人驚歎的效率提升使得所有的 Unix 系統都採用了 BPF 來過濾網路包,並棄用了傳統的既耗記憶體效率又低效的方法。BPF 至今仍活躍在各類 Unix 的後繼者身上,包含 Linux Kernel。後文將這部分的 BPF 叫做 cBPF(classic BPF)。

今生

時間來到 2014 年。Alexei Starovoitov 介紹了一種叫 extended BPF(eBPF)的設計。新的設計為匹配最近的硬體做了優化,與 cBPF 相比,它產生的機器碼執行效率更快,可供使用的暫存器從 2 個 32-bit 暫存器大幅提升至 10 個 64-bit 的暫存器,這為基於 eBPF 來實現更快、更復雜的功能提供了基礎條件。eBPF 的速度比 cBPF 快了 4 倍。

Windows 作業系統上著名的 Sysinternals 套件裡包含了一個系統監控的工具 sysmon,它在 Linux 上的實現也是基於 eBPF 的。難怪 Netflix 效能架構師 Gregg 說 BPF 是 OS 核心近 50 年來最基礎性的改動。

圖 1:eBPF 概略圖

從這張概略圖中,我們大致可以看出來 eBPF 專案的一些特點:

  • eBPF program(後文叫 eBPF prog)是執行在 Kernel 裡面的,可以 hook 到 kernel 裡面幾乎任何一個函式上,藉助 Verifier 和 JIT 的加持,可以安全快速地執行,無需擔心會把系統搞崩潰掉。這點可以完勝 kernel module,寫過 kernel module 的人都記得寫核心驅動時那份如履薄冰的痛苦。

  • 可以用它來實現 seccomp、觀測、安全控制、網路流量控制、網路安全、負載均衡、行為監控等各式各樣的功能。

  • 通過 Map,可以與 User space 的程序通訊。這也就意味著可以通過 Map 實時、動態地控制 eBPF program 的行為,並能及時收集 eBPF prog 產生的資料。傳統的檢測網路流量的方法不外乎編寫核心模組或者從檔案系統特定目錄(如/sys/class/net/eth0/statistics/rx_packets)定期讀取資料。每一次讀取意味著一系列檔案開啟、讀取等費時的系統呼叫。

  • Linux 社群提供了各式各樣的 toolchain,包括 bcc,bpftrace,gobpf,libbpf C/C++ Library,協助你以最小代價方便快捷地編寫 eBPF prog。款式各式各樣,總有一個適合你。

下面的圖 2 展示了基於 gobpf 開發 eBPF prog,通過 Verifier 和 JIT 後 hook 到 system call 的流程。除此之外,圖中還展示了一個 eBPF map。

圖 2:通過 SDK gobpf 載入 eBPF prog、hook system call、map 示意圖

下面是一段簡單的 eBPF program 程式碼。

SEC("tracepoint/syscalls/sys_enter_execve")int bpf_prog(void *ctx) {  char msg[] = "Hello, BPF World!";  bpf_trace_printk(msg, sizeof(msg));  return 0;}char _license[] SEC("license") = "GPL";

複製程式碼

通過命令 clang -O2 -target bpf -c bpf_program.c -o bpf_program.o 即可將其編譯成 eBPF prog bpf_program.o

bpf_program.o 是 elf 格式,.text 部分儲存的是位元組碼,載入到核心且通過 Verifier 這一關之後,JIT 負責將其轉換成機器碼。

通過下面的 c 程式碼,可將編譯好的 eBPF prog bpf_program.o 載入到核心。

#include <stdio.h>#include <uapi/linux/bpf.h>#include "bpf_load.h"int main(int argc, char **argv) {  if (load_bpf_file("bpf_program.o") != 0) {    printf("The kernel didn't load the BPF program\n");    return -1;  }  read_trace_pipe();  return 0;}

複製程式碼

如果使用圖 2 所示的 gobpf 的話,就更簡單了。直接呼叫 Go 方法 func (bpf *Module) AttachTracepoint(name string, fd int) error 載入這段原始碼即可。它會自動完成 c 程式碼轉位元組碼的編譯、通過 libbpf 呼叫 sys_bpf()載入 eBPF prog 進核心的工作。

注意:這裡是直接使用 c 原始碼的。傻瓜式的操作方便是方便,但也將一些問題延遲暴露了。比如 c 程式碼如果有編譯問題,只有等呼叫 AttachTracepoint()載入的時候才會發現。編譯 Go 程式碼的時候,是不會進行 c 程式碼的編譯的。

總體來說,eBPF 可以用來做兩大類的事情:tracing 和 networking。

  • Tracing:顧名思義,這類 eBPF prog 可以用來幫助你更好地理解你的系統裡發生了什麼。如程序資源使用情況,是否有異常的系統呼叫行為等等。

  • networking:這類 eBPF prog 用來檢查和處理系統裡的所有的網路包。比如可以在網路包還沒有進入網路棧的時候就進行導流,繞過 iptables 進行流量控制,修改 IP 和埠來實現負載均衡。

具體來說,eBPF 可以被分為大概 22 種的子類別(隨著 Kernel 的開發,會越來越多)。限於篇幅,這裡就不一一列舉了。詳細內容可參考 https://www.man7.org/linux/man-pages/man2/bpf.2.html

緣起

eBPF 是個讓人興奮的好東西,而 K8s 是個讓人亢奮的巨無霸。它們倆的相遇,在 Network Policy 這個地方擦出了奇妙的火花。

前文我們提到用 iptables 來實現 K8s Network Policy,會使得 iptables rule 的條目迅速膨脹到上萬條,這會導致網路包流經網路棧的時候速度變慢。 如果我們將網路棧比作河道,網路包比作水流的話,rule 條目的急速增加就像是在河道里插入了一個又一個攔汙網,它們在有效過濾網路包的時候,也顯著降低了流水的速度。

通過將 eBPF 替代 iptables,能有效改善這種情況。CNI 外掛 Calico 和 Cilium 尤其醉心於此。下面我們以 Calico 來看看它是如何利用 eBPF 來替代 iptables 的。

從網路的角度來看,我們使用 eBPF 主要是為了兩個目的:packet capturing 和 filtering。這表示應用程式可以在網路包流經路徑上插入各種 eBPF prog 以便來抓取資料包的資訊並對特定的網路包進行各種操作。

Networking data path

在談到 eBPF 如何替代 iptable 之前,先讓我們來看下網路資料路徑的概念。如圖 3 所示,當網路裝置驅動收到一個網路包後,XDP 會得到最早的機會來接觸這個 package,此時它操作的資料結構是 xdp_md。XDP 全名為 eXpress Data Path。我覺得比較好的翻譯應該是“快速資料路徑”,此處的“快速”作何解釋呢?在圖 3 中,我特地畫出了一條 XDP_TX 的路徑,可以看到當滿足特定條件時,它完全避開了 tc 和協議棧,直接將資料快速地處理掉。

當 XDP 決定將資料包送往核心做後續處理後,網路中斷處理程式會申請 skb_buff,接下來 traffic control(tc)便開始了它的處理流程,也就是我們聽說過的 QoS 和 Queue Descipline。

注意:從這裡開始,tc 和核心棧以及其它網路核心模組都會以 skb_buff 為處理物件。

之後,skb_buff 向上流入 Networking stack,如果一路暢通,最終會進入應用層。圖中也同樣畫出了當應用層向外傳送一個數據的時候,所流經的 data path。還記得我們上面的河道比喻嗎?網路資料包確實如河水一樣,在河道里面流淌。

當然這個過程中,iptables 依舊位於 Networking stack 中,我們也沒有必要繞開它,只要不設定過多的 iptables rule,便可以快速地穿過 iptables 這道屏障。

  1. 接收資料 data path:device driver --> xdp -- >tc(ingress) --> networking stack --> socket --> application

  2. 傳送資料 data path:application --> socket --> networking stack --> tc(egress) --> device driver

圖 3:networking data path 關鍵節點示意圖

介紹完網路資料路徑再來看圖 4。別忘了 eBPF 裡面的字母'F'代表的是 Filter。聰明的核心工程師自然是不忘初心,允許我們在網路資料路徑若干個關鍵節點上 hook eBPF 來過濾網路資料。

圖 4:eBPF 在 data path 上可以 hook 的各個關鍵節點示意圖(重點是右側部分,暫時忽略左側)

圖 4 右側部分,從下往上可供 hook 的 eBPF 型別至少有如下幾種:

  • XDP

  • tc

  • socket filter

  • Kprobe

  • Tracepoint

這些 hook 點可以和圖 3 淺綠色的框中所示的關鍵節點聯絡起來一起看。實際上,可供 hook 的點還有很多。嗯,老規矩,以後慢慢聊,好吧,我承認,其實是好多我也不會,等我學完一陣子後再來賣。

calico tc eBPF 示例

鋪墊了這麼多,終於到了介紹該如何利用 eBPF 來實現 Network Policy 的時候了。

下圖是一張利用 eBPF hook 到 tc 來實現 Network Policy 的架構圖。圖中 eBPF prog hook 在與 Pod 相連的 veth 上,它包括 3 大主要的子 program:main prog, policy prog 和 epilogue prog。利用 eBPF 的 tail call 功能,這 3 個 prog 依次被呼叫。

圖中 eBPF prog 會接收到來自物理網絡卡和節點上其它虛擬裝置發過來的 traffic。而我們看到 policy prog 自然地會想到 Network Policy。沒錯,通過將 Network Policy 轉譯成這裡需要的命令,即可方便、快速地控制 traffic 是否可以流向 Pod,而這個過程中我們可以看到 iptables 被完美地避開了。

強調一下,這裡所說的避開不是說流量不通過 iptables(實際上節點上其它虛擬裝置發過來的 traffic 可能不可避免地還是會通過 iptables 過濾一次),而是說因為有了 tc eBPF 的存在,我們便可以不再依賴 iptables,不需要建立巨量的 iptables rule,從而顯著減低 iptables 帶來的效能影響。

圖 5:CNI calico 利用 eBPF 來控制 traffic 示意圖

這張圖裡面的 policy prog 會引用到一個 IP set map。聰明的你一定會想到可以從 user space 把允許訪問這個 Pod 的 IP 和拒絕訪問的 IP 做成 allow list 和 deny list,然後塞到這個 map 裡,而 policy prog 可以根據你的設定來決定是否對 traffic 放行。

完美的實現!

以上就是本文的全部內容。碼字不易,更多內容請關注二哥的微信公眾號。您的舉手之勞是對二哥莫大的鼓勵。感謝有你!