爲什麼 k8s 管理員要懂 eBPF
本文主要目的是讓你瞭解 eBPF 的來龍去脈,以及爲什麼它在觀察容器和 Kubernetes 集羣時特別有用。
eBPF 有點像牛油果吐司:它的原料已經存在很長一段時間了。然而,僅僅在過去的幾年裏,eBPF 突然成爲 IT 界最新、最偉大的流行語之一。
我們無法解釋牛油果吐司是如何在全球時尚圈風靡一時的。但我們可以告訴你,爲什麼 eBPF 在 Kubernetes 可觀測性的革命中變得如此重要。讓我們來看看 eBPF 的歷史,它是如何工作的,它解決了哪些問題,以及爲什麼你應該開始使用它。
什麼是 eBPF?eBPF 簡史...
eBPF 是 “extended Berkeley Packet Filter” 的縮寫,你不可能瞭解 eBPF 的歷史,除非你瞭解老式伯克利包過濾器 BPF。
BPF 於 1993 年引入,作爲一種爲 Linux 內核配備可編程的、高效的虛擬機的方式,可以控制和過濾流量。這在當時是很有意義的,因爲 Linux 那個時候剛能夠支持軟件定義網絡,而 BPF 提供了一種強大的操作方法。
也就是說,儘管在 20 世紀 90 年代,就有關於 BPF 的使用,但不久之後它或多或少就銷聲匿跡了。原因在於,對於大多數人來說,獲得對網絡流量的內核級可編程控制實際上並不是那麼重要,因爲工作負載都運行在裸機或 vm 上,並且可以通過防火牆和管理程序很好地管理流量。
這一切都在 2013 年開始改變,當 Docker 出現時,突然間容器就大放異彩了。(順便說一句,Docker 的發展類似於 eBPF,容器實際上已經存在了幾十年,所以 Docker 所做的並不是真正的新事物;相反,Docker 真正的成就是它第一次成功地讓容器流行起來,這主要歸功於引入了更好的工具)。與一年後出現的 Kubernetes 相結合,Docker 引領了容器世界,在這個世界裏,以非常細粒度的、逐個容器的方式過濾和控制流量的能力變得非常有價值。
走進 eBPF
因此在 2014 年引入了 eBPF,它通過提供允許程序直接在 Linux 內核空間中運行的工具,擴展了 BPF 的原始架構。我們將在稍後討論爲什麼這在容器和 Kubernetes 的環境中很關鍵。但首先,讓我們退一步,先解釋在內核空間中運行程序意味着什麼。
基本上,eBPF 代碼是由內核執行的,而不像標準應用程序那樣運行在 “用戶空間”。這很重要,主要有三個原因:
1、它允許代碼高效地運行。
2、它允許代碼訪問底層內核資源,否則從用戶空間訪問這些資源很複雜和昂貴的 (就資源開銷而言)。
3、它允許你觀察用戶空間中運行的任何程序——這對於在用戶空間中運行的可觀察性工具是很難做到的。
這就是爲什麼 Brendan Gregg 等人稱 eBPF 爲 “無價的技術”,並將其與 JavaScript 進行比較:
JavaScript 不是靜態的 HTML 網站,而是允許你定義在鼠標點擊等事件上運行的小程序,這些事件在瀏覽器中的安全虛擬機中運行。使用 eBPF,而不是固定的內核,你現在可以編寫運行在磁盤 I/O 等事件上的小程序,這些事件在內核的安全虛擬機中運行。
如果你是一個鐵桿的 Linux 極客,你可能會想:“在使用內核模塊之前,eBPF 做了哪些我不能做的事情?”
這是一個合理的問題。的確,在內核空間中使用內核模塊執行代碼早就可以做到了。但問題是這些模塊必須被插入到內核中,這使得它們的部署更加複雜。它們還往往具有複雜的依賴關係,這增加了部署方面的麻煩。而且它們不是特別安全,這要求你信任用戶只插入既穩定又安全的模塊。
當然,從理論上講,你還可以修改 Linux 內核本身,以便在內核空間中運行你想要的任何代碼。但是一個普通的開發人員不能簡單地修改 Linux 內核——除非他是一個內核程序員 (相對而言,這樣的程序員並不多),或者他想要維護某種自定義的內核分支,這將是一個管理的噩夢。
從你提出需求到 Linux 社區接受你的需求大概需要一年時間,再從 Linux 內核到你使用的髮型版本需要 5 年,這就是一個噩夢。
eBPF 允許自定義程序在獨立的內核級虛擬機中運行,從而解決了所有這些問題。我們將在下面看到,你可以在各種工具的幫助下,輕鬆地部署你所選擇的代碼,而不必處理內核模塊依賴關係,接觸內核源代碼,甚至不必擁有 root 權限。再見,modprobe;你好,eBPF 字節碼!
eBPF 架構
現在你已經知道了 eBPF 是怎麼發展起來的,以及它爲什麼如此強大,讓我們來談談它的實際工作原理。
一般來說,要部署 eBPF 程序,你需要做這些事情:
編寫和編譯代碼
eBPF 代碼通常是用 “限制性 C” 編寫的,然後編譯成 eBPF 字節碼。Clang 是事實上的編譯標準。
在編寫代碼時,可以引用 bpf_helper 函數來執行各種常見操作,如內存複製、檢索 PID 和時間戳屬性以及與其他應用程序通信 (需定義 eBPF 數據結構,eBPF maps)。因此,你通常不必從頭開始編寫大量自定義代碼。定製代碼僅限於你想要實現的特定功能。
檢驗與加載
要部署已編譯的 eBPF 程序,首先調用 bpf() 系統調用,它將字節碼傳遞給內核檢驗器。內核檢驗器的工作是確保程序不會對內核造成問題。如果驗證成功,內核 JIT 編譯器將把它轉換爲可執行的機器代碼。
運行時 (runtime)
加載並驗證後,程序就可以執行了。它將監視你附加的任何代碼流—無論是在內核空間、用戶空間還是兩者都有。一旦它運行,你就可以使用 eBPF 映射或預定義的文件描述符訪問程序輸入或輸出。
下面摘自 Cilium 的 Golang eBPF 框架的部分代碼片段應該有助於說明這個過程 (上面是 eBPF 代碼,下面是加載 eBPF 程序並與之通信的用戶空間應用程序)。在編譯和運行應用程序時,它將計算系統上正在執行的新程序的數量。很整潔!
SEC("kprobe/sys_execve")
int kprobe_execve() {
u32 key = 0;
u64 initval = 1, *valp;
valp = bpf_map_lookup_elem(&kprobe_map, &key);
if (!valp) {
bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
return 0;
}
__sync_fetch_and_add(valp, 1);
return 0;
}
// Open a Kprobe at the entry point of the kernel function and attach the
// pre-compiled program. Each time the kernel function enters, the program
// will increment the execution counter by 1. The read loop below polls this
// map value once per second.
kp, err := link.Kprobe(fn, objs.KprobeExecve, nil)
if err != nil {
log.Fatalf("opening kprobe: %s", err)
}
defer kp.Close()
// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
log.Println("Waiting for events..")
for range ticker.C {
var value uint64
if err := objs.KprobeMap.Lookup(mapKey, &value); err != nil {
log.Fatalf("reading map: %v", err)
}
log.Printf("%s called %d times\n", fn, value)
}
k8s 可觀測和 eBPF 使用場景
有各種各樣的 eBPF 用例 (https://www.tigera.io/learn/guides/ebpf/)。容器和 Kubernetes 可觀測性只是其中之一。
但當涉及到觀察容器和 Kubernetes 集羣時,eBPF 尤其令人興奮。爲什麼?因爲 eBPF 允許你獲得 “乾淨的” 數據來跟蹤任何類型的事件——例如網絡操作或用戶空間中的事件。數據直接來自 Linux 內核,具有最小的性能開銷。這意味着你可以部署一個 eBPF 程序,在每個數據包進入或發送出主機時監視它,然後將其映射到該主機上運行的進程或容器。其結果是對網絡流量發生的情況的細粒度可見性。
再加上 eBPF 程序非常高效和安全,很容易理解爲什麼人們現在對 eBPF 如此興奮,以及它在雲原生世界中爲可觀測性解鎖的可能性。
eBPF SDKs 的使用
曾經有一段時間——也就是說,在 2010 年代中期——編寫和加載 eBPF 程序是一項大量的工作,因爲圍繞 eBPF 的工具還不成熟。
在過去幾年中,由於引入了更多簡化 eBPF 使用的工具,以及 bpf_helper 函數和 eBPF 映射的不斷改進,這種情況已經發生了變化。
同時,外部工具鏈有助於簡化 eBPF 的引導和開發。關鍵的例子包括 BCC 和 libpf(它現在作爲 Linux 內核的一部分進行維護,因此開始成爲事實上的選項)。如上所述,還有一些 eBPF 友好的編譯器,如 Clang。
而且,對於那些希望在現代語言開發方面提高 eBPF 使用水平的人來說,有一些解決方案要感謝那些能夠編寫用戶空間代碼來與 eBPF 程序交互的項目,比如 Python,Golang 和 Rust。
總的來說,eBPF 工具鏈生態系統正在快速發展,現在確切地說哪些工具最終會得到廣泛採用還爲時過早。但是我們可以肯定地說,圍繞 eBPF 的工具越來越成熟,它給了開發人員越來越少的理由迴避使用 eBPF。多虧了這些偉大的工具,即使是我們中間最缺乏動力的開發者也可以編寫和加載 eBPF 程序!
eBPF 的缺點
總的來說,值得注意的是,eBPF 並不能解決實現中的所有問題,而且它受到某些可能永遠不會消失的限制。
一是編寫符合內核檢驗器的 eBPF 程序可能很棘手,尤其是對新手來說。如果你的程序被拒絕,檢驗器並不會詳細地解釋原因。獨立於檢驗器的工具有助於解決這一挑戰,但它們並不能消除程序被拒絕的風險,並且在試圖找出原因時,你將變得非常沮喪。而且,由於驗證只在運行時才發生,你面臨的風險是,一個內核接受了你的程序,而另一個內核版本有可能拒絕。
eBPF 的另一個限制是 eBPF 程序在堆棧空間上受到限制,這使得開發更加困難和不太直觀。(它們以前在指令大小上也有限制,但在內核 5.3 中有效地消除了這一限制)。你需要學習如何高效地編寫 eBPF 代碼,以使其能夠大規模地工作。
第三個問題是,儘管 eBPF 是在 Linux 內核中實現的,但不同 Linux 發行版之間的內核版本和自定義之間的差異意味着 eBPF 程序並不總是像你期望的那樣可移植。如果你有一些節點使用 Alpine Linux,其他節點運行 Ubuntu,那麼你可能會發現你的 eBPF 程序不能跨所有節點工作。改進 eBPF 可移植性的工作正在進行中,但它仍然不像你想的那樣天衣無縫。
eBPF 的美好未來
我們不知道十年或二十年後人們是否還會喫牛油果吐司。但是我們非常有信心,Kubernetes 的開發人員和管理員將利用 eBPF 來幫助理解節點和 pod 內部發生的事情。
考慮到 eBPF 生態系統正變得越來越有組織,這一點尤其如此,這要歸功於 eBPF 基金會等組織。正如 Thomas Graf 提到的 eBPF 發展,我們開始看到 “像谷歌和 Facebook 這樣的大公司在維護和推動 eBPF 的發展。”
所以,如果你一直在抵抗 eBPF 革命,現在是投降的時候了。Kubernetes 可觀測性的未來——除其他外——取決於 eBPF,你也可以開始學習使用它。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rjWW6NKchWQJ3B2YqD_9Qg