後起之秀 -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 放行。

完美的實現!

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