當我們談論現代 Linux 核心最令人興奮的技術時,eBPF(extended Berkeley Packet Filter)絕對名列前茅。這項技術讓開發者能夠在不修改核心程式碼、不載入核心模組的情況下,安全地擴充套件核心功能。在我研究系統程式設計多年的經驗中,很少看到像 eBPF 這樣能夠在保障系統安全的前提下,提供如此強大彈性的技術。
BPF 的初始目的:高效封包過濾
BPF 最初的設計非常直接:提供一種高效能的方式來過濾網路封包。當網路流量需要分析時,傳統方法會將所有封包從核心空間複製到使用者空間進行處理,這樣的做法效率極低。BPF 的革命性在於它允許在核心中執行簡單的程式,決定哪些封包值得進一步處理。
讓我們來看一個簡單的 BPF 程式範例:
// 一個簡單的 BPF 封包過濾程式
(ld #0x0c) // 載入乙太網路標頭長度
(ldx mem[x+0]&0xf) // 取得 IP 標頭長度
(add x) // 加上 IP 標頭長度
(ld #0x14) // 載入協定欄位的偏移量
(ld mem[x+0]) // 載入協定欄位
(jneq #0x06, drop) // 若不是 TCP (0x06),則丟棄
(ret #-1) // 接受封包
drop: (ret #0) // 丟棄封包
這段簡單的 BPF 程式展示了過濾網路封包的基本邏輯。程式首先計算乙太網路和 IP 標頭的長度,然後檢查封包的協定欄位。如果不是 TCP 協定(值為 0x06),則封包會被丟棄(回傳 0);否則封包會被接受(回傳 -1)。這種機制讓系統能在核心層面就過濾掉不需要的封包,大幅提升效能。
從這個例子可以看出,BPF 的設計初衷是讓開發者能夠編寫自定義程式在核心中執行,以高效率地決定封包的處理方式。這正是 eBPF 強大功能的雛形。
從 BPF 到 eBPF:技術演進的關鍵里程碑
BPF 技術在 Linux 歷史中有著悠久的發展軌跡:
1997 年:BPF 首次被引入 Linux 核心 2.1.75 版本,主要用於 tcpdump 工具,作為高效率捕捉封包的機制。
2012 年:seccomp-bpf 在核心 3.5 版本中推出,這是 BPF 首次超越封包過濾的應用,開始用於系統安全領域,可以決定是否允許使用者空間應用程式執行特定系統呼叫。
2014 年:在核心 3.18 版本中,BPF 進化為 eBPF(extended BPF),這次升級包含了幾項重大改變:
- 指令集完全重新設計,以更適合 64 位元機器
- 引入 eBPF maps 資料結構,允許 BPF 程式與使用者空間應用程式分享資訊
- 新增 bpf() 系統呼叫,讓使用者空間程式能與核心中的 eBPF 程式互動
- 增加多種 BPF 輔助函式
- 加入 eBPF 驗證器,確保 eBPF 程式的安全性
這些改進奠定了 eBPF 的基礎,但技術發展並未停止。從我的觀察來看,正是這次從 BPF 到 eBPF 的演進,徹底改變了這項技術的應用範疇,使其從單一用途的封包過濾工具,轉變為一個通用的核心程式設計平台。
eBPF 發展成為生產系統的關鍵技術
擴充套件到追蹤與可觀測性
Linux 核心自 2005 年起就有一個名為 kprobes(核心探針)的功能,允許在核心程式碼的幾乎任何指令處設定陷阱。開發者可以編寫核心模組,將函式附加到 kprobes 上,用於除錯或效能測量。
2015 年,Linux 核心增加了將 eBPF 程式附加到 kprobes 的能力,這成為 Linux 系統追蹤方式革命的起點。同時,核心的網路堆積積疊中開始增加鉤子(hooks),允許 eBPF 程式處理更多網路功能。
生產環境中的 eBPF 應用
到了 2016 年,根據 eBPF 的工具已在生產系統中使用。Brendan Gregg 在 Netflix 的追蹤工作在基礎設施和營運圈中廣為人知,他曾表示 eBPF「為 Linux 帶來了超能力」。同年,Cilium 專案宣佈推出,成為第一個使用 eBPF 替代容器環境中整個資料路徑的網路專案。
2017 年,Facebook(現為 Meta)開放原始碼了 Katran 專案。Katran 是一個第 4 層負載平衡器,滿足了 Facebook 對高度可擴充套件和快速解決方案的需求。自 2017 年以來,每個傳送到 Facebook.com 的封包都透過 eBPF/XDP 處理。對我而言,在德克薩斯州奧斯汀的 DockerCon 上看到 Thomas Graf 關於 eBPF 和 Cilium 專案的演講後,我開始對這項技術所帶來的可能性感到無比興奮。
技術成熟與標準化
2018 年,eBPF 成為 Linux 核心內的獨立子系統,由 Isovalent 的 Daniel Borkmann 和 Meta 的 Alexei Starovoitov 擔任維護者(後來 Meta 的 Andrii Nakryiko 也加入)。同年還引入了 BPF Type Format (BTF),這使得 eBPF 程式的可攜性大提高。
2020 年,LSM BPF 被引入,允許 eBPF 程式附加到 Linux 安全模組 (LSM) 核心介面。這標誌著 eBPF 的第三個主要應用場景被確立:除了網路和可觀測性外,eBPF 也成為安全工具的絕佳平台。
多年來,eBPF 的能力已大幅增長,這要歸功於 300 多位核心開發者和許多相關使用者空間工具(如 bpftool)、編譯器和程式語言函式庫的貢獻者。程式曾經限制為 4,096 條指令,但現在這個限制已增長到 100 萬條經過驗證的指令,並且由於支援尾呼叫和函式呼叫,這個限制實際上已經變得無關緊要。
命名的難題:BPF 還是 eBPF?
eBPF 的應用範圍已遠超出封包過濾,使得這個縮寫基本上失去了原本的意義,它已成為一個獨立的術語。由於現今廣泛使用的 Linux 核心都支援「擴充套件」部分,因此 eBPF 和 BPF 這兩個術語在很大程度上可以互換使用。
在核心原始碼和 eBPF 程式設計中,通用術語是 BPF。例如,與 eBPF 互動的系統呼叫是 bpf(),輔助函式以 bpf_ 開頭,不同型別的 (e)BPF 程式用 BPF_PROG_TYPE 開頭的名稱識別。在核心社群之外,「eBPF」這個名稱似乎已經被廣泛接受,例如在社群網站 ebpf.io 和 eBPF 基金會的名稱中。
從我的角度看,這種命名上的混亂反映了技術的快速演進 - 當一項技術的應用範圍遠超其初始設計時,名稱往往會滯後於實際功能。不過,無論稱它為 BPF 還是 eBPF,這項技術的革命性影響都是毋庸置疑的。
理解 Linux 核心:eBPF 的執行環境
要深入理解 eBPF,我們需要掌握 Linux 中核心和使用者空間的區別。
Linux 核心是應用程式與硬體之間的軟體層。應用程式在稱為使用者空間的非特權層中執行,無法直接存取硬體。相反,應用程式使用系統呼叫(syscall)介面請求核心代表它執行操作。硬體存取可能涉及讀取和寫入檔案、傳送或接收網路流量,甚至只是存取記憶體。核心還負責協調並發處理程式,使多個應用程式能同時執行。
作為應用程式開發者,我們通常不直接使用系統呼叫介面,因為程式語言為我們提供了高階抽象和標準函式庫,這些介面更容易程式設計。因此,許多人並不清楚在我們的程式執行時核心做了多少工作。如果你想了解核心被呼叫的頻率,可以使用 strace 工具顯示應用程式所做的所有系統呼叫。
以下是一個範例,使用 cat 將單詞 “hello” 輸出到螢幕上,涉及 100 多個系統呼叫:
$ strace -c echo "hello"
hello
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- --------
24.62 0.001693 56 30 12 openat
17.49 0.001203 60 20 mmap
15.92 0.001095 57 19 newfstatat
15.66 0.001077 53 20 close
10.35 0.000712 712 1 execve
3.04 0.000209 52 4 mprotect
2.52 0.000173 57 3 read
2.33 0.000160 53 3 brk
2.09 0.000144 48 3 munmap
1.11 0.000076 76 1 write
0.96 0.000066 66 1 1 faccessat
0.76 0.000052 52 1 getrandom
0.68 0.000047 47 1 rseq
0.65 0.000045 45 1 set_robust_list
0.63 0.000043 43 1 prlimit64
0.61 0.000042 42 1 set_tid_address
0.58 0.000040 40 1 futex
------ ----------- ----------- --------- --------- --------
100.00 0.006877 61 111 13 total
這個輸出展示了一個簡單的 echo "hello"
命令實際上呼叫了多達 111 個系統呼叫!從開啟檔案(openat)、記憶體對映(mmap)到檔案狀態檢查(newfstatat)和關閉檔案(close),這些都需要核心的參與。這個例子生動地說明瞭即使是最簡單的操作,也高度依賴核心提供的服務。
由於應用程式如此依賴核心,如果我們能觀察其與核心的互動,就能瞭解應用程式的行為。這正是 eBPF 的強大之處 - 它允許我們在核心中增加檢測點,取得這些互動訊息,而無需修改核心程式碼或重新編譯任何東西。
在我的實務經驗中,這種能力為效能分析、安全監控和網路最佳化開啟了前所未有的可能性。eBPF 讓我們能夠以極低的效能開銷,取得系統執行的深度洞察,這在以前只能透過侵入式方法或高開銷的追蹤工具實作。
從單純的封包過濾工具到現在的通用系統程式設計平台,eBPF 的演進代表了 Linux 系統程式設計的一場革命。它讓我們能夠以安全與高效的方式擴充套件核心功能,為網路、安全和可觀測性等領域帶來了革命性的改變。正如 Brendan Gregg 所說,eBPF 確實為 Linux 帶來了「超能力」,而這些能力正在被越來越多的開發者和企業所發掘和應用。
在接下來的內容中,我們將更深入地探討 eBPF 的核心概念、程式設計模型和實際應用案例,幫助你充分利用這項強大技術的潛力。
探索系統呼叫攔截:從理論到實踐
想像一下,如果你能攔截開啟檔案的系統呼叫,你就能精確看到任何應用程式存取了哪些檔案。這種能力對於安全監控、效能分析和故障排除都極為寶貴。但問題來了:我們該如何實作這種攔截?
傳統上,若要在檔案開啟時產生某種輸出,需要直接修改Linux核心,這涉及一系列複雜的挑戰。讓我們探討這些挑戰,以及eBPF如何提供一條全新的解決途徑。
傳統核心功能擴充的艱辛之路
核心修改的技術障礙
Linux核心是一個極其複雜的軟體系統,目前約有3000萬行程式碼。修改任何程式碼函式庫都需要對現有程式碼有相當熟悉度,除非你已經是核心開發者,否則這將是一項艱鉅的挑戰。
當玄貓初次嘗試核心修改時,僅找到正確的修改點就花費了數天時間。核心程式碼的複雜性和相互依賴性使得即使是小改動也可能引起連鎖反應,導致系統不穩定。
社群接受度的挑戰
即使你克服了技術障礙,開發出了能夠攔截系統呼叫的程式碼,你仍面臨一個非技術性的挑戰:讓你的修改被上游接受。
Linux是一個通用作業系統,用於各種環境和情境。這意味著你的修改必須不僅是能運作,還需要被社群(特別是Linux的建立者和主要開發者Linus Torvalds)接受為對所有使用者有益的改變。
資料顯示,僅有三分之一的核心修補程式被接受合併到官方版本中。這個接受率反映了Linux核心維護的嚴謹標準。
漫長的發布週期
假設經過數月的討論和艱苦的開發工作,你的修改終於被接受進入核心。但距離這個功能真正到達大多數使用者的機器還有很長的路要走。
Linux核心每兩到三個月發布一個新版本,但即使你的修改已經進入了其中一個發布版本,它距離在大多數生產環境中可用仍有相當長的時間。這是因為大多數人不直接使用Linux核心,而是使用如Debian、Red Hat、Alpine和Ubuntu等Linux發行版,這些發行版將一個特定版本的Linux核心與各種其他元件封裝在一起。
以企業使用者常用的Red Hat Enterprise Linux (RHEL)為例,當前版本RHEL 8.5(2021年11月發布)使用的是4.18版本的Linux核心,而這個核心版本發布於2018年8月。
從構思階段到生產環境中的Linux核心,新功能需要數年時間才能實作。這種滯後對於需要快速創新的組織來說是一個重大障礙。
核心模組:另一種擴充套件方式
如果你不想等待數年才能讓你的修改進入核心,還有另一個選擇:編寫核心模組。Linux核心設計為可以按需載入和解除安裝核心模組。如果你想改變或擴充套件核心行為,編寫模組確實是一種方法。核心模組可以獨立於官方Linux核心發布分發給其他人使用,因此不必被接受到主要上游程式碼函式庫中。
然而,這仍然是完全的核心程式設計。使用者對使用核心模組一直非常謹慎,原因很簡單:如果核心程式碼當機,它會導致機器和其上執行的所有程式一起當機。使用者如何確信核心模組是安全可執行的?
“安全可執行"不僅意味著不當機,使用者還想知道核心模組在安全性方面是否可靠。它是否包含攻擊者可能利用的漏洞?我們是否信任模組的作者不會在其中放置惡意程式碼?因為核心是特權程式碼,它可以存取機器上的所有內容,包括所有資料,所以核心中的惡意程式碼將是一個嚴重的安全隱患。這同樣適用於核心模組。
核心的安全性是Linux發行版需要很長時間才能整合新版本的一個重要原因。如果其他人在各種情況下執行一個核心版本數月或數年,這應該已經暴露出問題。發行版維護者可以相信他們提供給使用者/客戶的核心是經過強化的,即它是安全可執行的。
eBPF:革命性的核心擴充套件方法
eBPF提供了一種完全不同的安全方法:eBPF驗證器,它確保eBPF程式只有在安全可執行的情況下才會被載入。它不會使機器當機或在硬迴圈中鎖死,也不會允許資料被破壞。我們將在後續內容中更詳細地討論驗證過程。
eBPF程式的動態載入
eBPF程式可以動態地載入到核心中並從核心中移除。一旦它們被附加到一個事件上,無論是什麼導致該事件發生,它們都會被該事件觸發。例如,如果你將一個程式附加到開啟檔案的系統呼叫上,每當任何程式嘗試開啟檔案時,它都會被觸發。無論該程式在程式載入時是否已經在執行都沒有關係。與升級核心然後必須重新啟動機器以使用其新功能相比,這是一個巨大的優勢。
這導致了使用eBPF的可觀測性或安全工具的一個巨大優勢 - 它立即獲得對機器上發生的一切的可見性。在執行容器的環境中,這包括對在這些容器內以及在主機上執行的所有程式的可見性。
此外,人們可以透過eBPF非常快速地建立新的核心功能,而不需要每個其他Linux使用者接受相同的更改。這種靈活性使得eBPF成為快速創新和解決特定問題的理想工具。
eBPF程式的高效能
eBPF程式是新增監測的一種非常高效的方式。一旦載入並經過JIT編譯,程式就會以CPU上的原生機器指令執行。此外,不需要為每個事件付出在核心和使用者空間之間轉換的成本(這是一個昂貴的操作)。
描述eXpress Data Path(XDP)的2018年論文包含了一些eBPF在網路方面實作的效能改進的例子。例如,在XDP中實作路由「相比常規Linux核心實作提高了2.5倍的效能」,而「XDP在負載平衡方面比IPVS提供了4.3倍的效能增益」。
對於效能追蹤和安全可觀測性,eBPF的另一個優勢是相關事件可以在核心內過濾,然後才會產生將它們傳送到使用者空間的成本。過濾特定網路封包畢竟是原始BPF實作的目的。如今,eBPF程式可以收集有關系統中各種事件的訊息,它們可以使用複雜的、定製的程式化過濾器僅將相關訊息子集傳送到使用者空間。
eBPF在雲端原生環境中的應用
現今,許多組織選擇不透過直接在伺服器上執行程式來執行應用程式。相反,許多使用雲端原生方法:容器、Kubernetes或ECS等協調器,或Lambda、雲端函式、Fargate等無伺服器方法。這些方法都使用自動化來選擇每個工作負載將執行的伺服器;在無伺服器中,我們甚至不知道哪個伺服器正在執行每個工作負載。
儘管如此,仍然涉及伺服器,每個伺服器(無論是虛擬機器還是裸機)都執行一個核心。當應用程式在容器中執行時,如果它們在同一(虛擬)機器上執行,它們分享相同的核心。在Kubernetes環境中,這意味著一個節點上所有Pod中的所有容器都使用相同的核心。當我們用eBPF程式監測該核心時,該節點上的所有容器化工作負載都對這些eBPF程式可見。
這種能力在雲端原生環境中尤為重要。當使用Kubernetes這類別容器協調平台時,傳統的監控工具往往難以深入容器內部瞭解應用行為。而eBPF能夠從核心層面「看見」所有容器活動,無需在容器內佈署代理程式,大簡化了監控架構。
傳統核心擴充與eBPF的對比分析
讓我們總結一下傳統核心修改與使用eBPF擴充核心功能的主要區別:
開發週期
- 傳統方法:需要深入理解數百萬行核心程式碼,開發週期長,上游接受率低
- eBPF方法:無需修改核心原始碼,開發週期短,可獨立分發和佈署
佈署與更新
- 傳統方法:需要核心升級和系統重啟,從開發到佈署可能需要數年時間
- eBPF方法:動態載入和解除安裝,無需重啟系統,即時生效
安全性
- 傳統方法:核心模組可能引入不穩定性,缺乏內建安全機制
- eBPF方法:驗證器確保程式安全執行,防止系統當機和資源洩漏
效能影響
- 傳統方法:效能最佳化有限,核心和使用者空間切換成本高
- eBPF方法:JIT編譯提供接近原生的效能,事件過濾減少資料傳輸量
雲端原生相容性
- 傳統方法:難以適應動態變化的容器環境
- eBPF方法:天然適合容器和Kubernetes環境,提供跨容器的可觀測性
eBPF實際應用案例
在玄貓的實踐中,eBPF已被用於多種關鍵場景:
- 網路效能監控:透過eBPF追蹤TCP連線狀態,識別網路瓶頸,最佳化網路效能
- 安全稽核:監控並記錄敏感系統呼叫,提供細粒度的安全可觀測性
- 微服務追蹤:在Kubernetes環境中追蹤微服務間的通訊,無需修改應用程式
- 資源使用分析:精確追蹤CPU、記憶體和I/O使用情況,識別資源浪費
以網路效能監控為例,以下是一個簡化的eBPF程式範例,用於追蹤TCP連線建立時間:
#include <linux/bpf.h>
#include <linux/tcp.h>
BPF_HASH(start_times, u32, u64);
int trace_connect(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 ts = bpf_ktime_get_ns();
start_times.update(&pid, &ts);
return 0;
}
int trace_connect_return(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *start_ts = start_times.lookup(&pid);
if (start_ts) {
u64 duration = bpf_ktime_get_ns() - *start_ts;
// 將連線時間傳送到使用者空間
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&duration, sizeof(duration));
start_times.delete(&pid);
}
return 0;
}
這段eBPF程式追蹤TCP連線的建立時間。它使用兩個探測點:一個在連線開始時(trace_connect
),另一個在連線完成時(trace_connect_return
)。
BPF_HASH(start_times, u32, u64)
建立一個雜湊表,用於儲存程式ID和開始時間戳trace_connect
函式取得當前程式ID和時間戳,並將其存入雜湊表trace_connect_return
函式在連線完成時被呼叫,查詢開始時間並計算連線持續時間bpf_perf_event_output
將計算出的持續時間傳送到使用者空間進行進一步處理
這種方法允許我們精確測量TCP連線建立所需的時間,幫助識別網路延遲問題,而無需修改應用程式或核心。
結論
eBPF代表了Linux核心可擴充套件性的一個正規化轉變。它提供了一種安全、高效、動態的方式來擴充套件核心功能,無需經歷漫長的核心修改週期。這種方法特別適合雲端原生環境,提供了前所未有的可觀測性和控制能力。
隨著雲端計算和容器技術的普及,eBPF的重要性只會增加。未來我們可能會看到更多根據eBPF的創新解決方案,從網路最佳化到安全監控,再到資源管理。對於系統工程師和DevOps專業人員而言,掌握eBPF已成為一項越來越重要的技能。
從技術角度看,eBPF是Linux生態系統中最令人興奮的發展之一,它不僅解決了當前的問題,還為未來的創新奠定了基礎。無論是在傳統資料中心還是現代雲端環境中,eBPF都提供了前所未有的能力,使我們能夠以最小的開銷獲得最大的洞察力。
eBPF:雲原生環境的全視之眼
在現代雲原生架構中,我們經常需要對系統進行深度觀測、效能最佳化和安全防護。傳統方法通常需要修改應用程式碼或採用 Sidecar 模式,但這些方法都存在各種限制。而 eBPF(extended Berkeley Packet Filter)作為一項革命性技術,正在徹底改變我們監測和最佳化系統的方式。
eBPF 技術讓我們能夠在 Linux 核心中動態載入程式,無需重新編譯核心或重新啟動系統,就能夠觀測和修改系統行為。這種能力在雲原生環境中尤為強大,因為它能夠提供對所有容器化應用的全方位可見性,而無需修改這些應用。
eBPF 在 Kubernetes 環境中的超能力
在 Kubernetes 環境中,eBPF 程式能夠對節點上執行的所有應用程式擁有完整可見性。這種能力結合動態載入 eBPF 程式的特性,為雲原生環境提供了真正的超能力:
- 無需修改應用程式或其設定,就能使用 eBPF 工具進行監測
- eBPF 程式一旦載入核心並附加到事件上,立即能夠開始觀測現有的應用程式處理程式
這種方式與 Sidecar 模式形成鮮明對比。Sidecar 模式雖然比直接修改應用程式碼更為方便,但仍存在多項缺陷。
Sidecar 模式的侷限性
Sidecar 方法在 Kubernetes 中被廣泛用於增加日誌記錄、追蹤、安全性和服務網格功能。這種方法透過修改定義應用程式 Pod 的 YAML 檔案,注入一個額外的容器來實作功能擴充套件。
然而,這種方法存在幾個明顯的缺點:
應用重啟問題
為了增加 Sidecar,應用 Pod 必須重新啟動。在生產環境中,這可能意味著服務中斷或降級。
設定修改依賴
Sidecar 注入需要某種機制來修改應用 YAML。通常這是一個自動化過程,例如透過准入控制器(Admission Controller)。但如果佈署沒有正確標記,Sidecar 就不會被增加,應用也就不會被監測到。
容器啟動時序問題
當 Pod 中有多個容器時,它們的就緒時間可能不同,與順序可能不可預測。Sidecar 的注入可能顯著延長 Pod 的啟動時間,甚至導致競態條件或其他不穩定性。例如,Open Service Mesh 檔案描述了應用容器必須能夠承受在 Envoy 代理容器就緒前所有流量被丟棄的情況。
網路效率降低
當服務網格功能透過 Sidecar 實作時,所有進出應用容器的流量都必須經過核心的網路堆積積疊才能到達網路代理容器,這為流量增加了延遲。在實際執行中,這種額外的跳轉會導致明顯的效能損失。
eBPF:超越 Sidecar 的新正規化
幸運的是,隨著 eBPF 的出現,我們有了一個能夠避免上述問題的新模型。
在實作服務觀測時,eBPF 根據工具不需要修改應用或注入 Sidecar,就能夠觀測到系統中發生的一切。這不僅提高了效率,也增強了安全性。
例如,假設有攻擊者在您的主機上佈署了加密貨幣挖礦應用,他們很可能不會為這個惡意程式注入您用於合法工作負載的 Sidecar。如果您依賴根據 Sidecar 的安全工具來防止應用程式建立意外的網路連線,那麼當 Sidecar 未被注入時,這個工具將無法發現挖礦應用連線到其挖礦池的行為。
相比之下,在 eBPF 中實作的網路安全能夠監控主機上的所有流量,因此可以輕鬆阻止這種加密貨幣挖礦操作。這種根據安全原因丟棄網路封包的能力是 eBPF 的一個重要特性。
eBPF 的 “Hello World”
為了更具體地理解 eBPF 程式的運作方式,讓我們來看一個簡單的 “Hello World” 範例。在這個例子中,我將使用 BCC Python 框架,這是一種非常容易上手的方式來編寫基本的 eBPF 程式。
以下是使用 BCC Python 函式庫編寫的 eBPF “Hello World” 應用程式的完整原始碼:
#!/usr/bin/python
from bcc import BPF
program = r"""
int hello(void *ctx) {
bpf_trace_printk("Hello World!");
return 0;
}
"""
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
b.trace_print()
這個程式由兩部分組成:在核心中執行的 eBPF 程式本身,以及載入 eBPF 程式到核心並讀取其生成的追蹤輸出的使用者空間程式碼。
在這個例子中:
hello.py
是應用程式的使用者空間部分hello()
函式是在核心中執行的 eBPF 程式
eBPF 程式本身是用 C 語言編寫的,非常簡單:
int hello(void *ctx) {
bpf_trace_printk("Hello World!");
return 0;
}
這個 eBPF 程式只做一件事:使用一個輔助函式 bpf_trace_printk()
來寫入一條訊息。輔助函式是 eBPF 程式與核心互動的主要方式,它們提供了受控的 API 來執行各種操作。
讓我們逐行分析這個程式的運作方式:
- 首先,程式從 BCC 函式庫匯入 BPF 類別
- 接著定義了 eBPF 程式的 C 程式碼
- 建立了一個 BPF 物件,將 C 程式碼作為引數傳入
- 使用
get_syscall_fnname()
找到execve
系統呼叫的函式名稱 - 使用
attach_kprobe()
將 eBPF 程式附加到execve
系統呼叫上 - 最後,使用
trace_print()
來顯示 eBPF 程式產生的追蹤輸出
當這個程式執行時,每次系統中有程式呼叫 execve
系統呼叫(用於執行新程式)時,我們的 eBPF 程式就會被觸發,並輸出 “Hello World!” 訊息。
eBPF 程式的生命週期
理解 eBPF 程式的生命週期對於掌握這項技術至關重要。一個典型的 eBPF 應用程式生命週期包括以下階段:
- 編寫程式:開發者編寫 eBPF 程式,通常使用 C 或其變體
- 編譯為 BPF 位元碼:程式被編譯為 BPF 位元碼
- 載入到核心:使用者空間程式將 BPF 位元碼載入到核心
- 核心驗證:核心的驗證器檢查程式的安全性
- 即時編譯(JIT):程式被即時編譯為本機器碼以提高效能
- 附加到事件:程式被附加到特定的核心事件(如系統呼叫、網路事件等)
- 執行:當觸發事件發生時,程式被執行
- 與使用者空間通訊:程式可以透過對映(maps)等機制與使用者空間通訊
- 解除安裝:當不再需要時,程式可以從核心解除安裝
這種生命週期模型允許 eBPF 程式在不修改核心原始碼的情況下,安全地擴充套件核心的功能。
eBPF 的安全保障
eBPF 技術之所以能夠安全地在核心中執行動態程式碼,關鍵在於其嚴格的驗證機制。核心中的 eBPF 驗證器會對每個 eBPF 程式進行徹底的靜態分析,確保:
- 程式不會包含無限迴圈
- 所有記憶體存取都是安全的
- 程式不會造成核心當機
- 程式符合特定的複雜度限制
這些安全機制使 eBPF 成為一個安全的平台,允許非特權使用者在核心中執行自定義程式,而不會危及系統的穩定性和安全性。
eBPF 的實際應用
eBPF 的應用範圍非常廣泛,以下是一些常見的使用場景:
系統觀測與效能分析
eBPF 允許深入觀測系統行為,從系統呼叫到網路流量,從檔案系統操作到CPU使用情況,無所不包。工具如 BCC、bpftrace 和 Pixie 都大量使用 eBPF 來提供深入的系統洞察。
在效能分析方面,eBPF 可以提供前所未有的細粒度資訊,幫助識別效能瓶頸。例如,我曾經使用 eBPF 工具來分析一個微服務應用的延遲問題,發現問題出在系統呼叫層面,這是傳統工具難以發現的。
網路安全與監控
eBPF 可以檢查和過濾網路封包,實作高效的網路安全策略。Cilium 等專案利用 eBPF 提供 Kubernetes 原生的網路策略執行,提供比傳統 iptables 更高效、更靈活的網路安全。
服務網格與負載平衡
eBPF 能夠在核心層面直接處理網路流量,避免了傳統服務網格實作中資料包需要穿越多次網路堆積積疊的問題。這大減少了延遲,提高了網路效率。
eBPF 作為一項革命性技術,正在徹底改變我們監測、最佳化和保護雲原生環境的方式。它提供了前所未有的可見性和控制能力,同時避免了傳統方法的諸多限制。
透過允許我們在核心中動態載入程式,eBPF 讓我們能夠根據具體需求定製系統行為,而無需修改應用程式或重新啟動系統。這種能力在雲原生環境中尤為寶貴,因為它能夠提供對所有容器化應用的全方位可見性。
隨著雲原生技術的不斷發展,eBPF 將在系統觀測、網路最佳化和安全防護等領域發揮越來越重要的作用。掌握 eBPF 技術,將使開發者和維運人員能夠更好地理解和最佳化他們的系統,從而構建更高效、更安全的雲原生應用。
eBPF 程式與系統互動的基礎機制
在 eBPF 程式設計中,與系統的互動是透過一系列特殊的函式實作的。這些函式允許 eBPF 程式與核心系統進行資料交換,執行特定操作,以及輸出追蹤資訊。在我們的範例中,bpf_trace_printk()
函式就是這樣一個基本的互動機制,它能將文字輸出到追蹤管道。
從 Python 到 eBPF 的橋樑
BCC (BPF Compiler Collection) 框架為我們提供了一個簡潔的方式來編譯和載入 eBPF 程式。當我們執行以下程式碼時:
b = BPF(text=program)
實際上發生了幾件重要的事:
- BCC 編譯了我們定義在
program
字串中的 C 程式 - 編譯好的 eBPF 程式被載入到核心
- 系統執行安全檢查,確保程式不會危害系統
這個簡單的一行程式碼隱藏了複雜的底層操作。BCC 框架負責處理 eBPF 程式的編譯、驗證和載入過程,讓開發者能專注於程式邏輯而非基礎設施。在我設計大型監控系統時,這種抽象層的價值不言而喻,它讓團隊成員能快速上手 eBPF 開發,而不需深入理解核心機制的複雜細節。
事件繫結:eBPF 程式的觸發機制
eBPF 程式需要繫結到特定事件才能執行。在範例中,我們選擇了 execve
系統呼叫作為觸發點:
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
這段程式碼完成了兩項重要任務:
- 使用
get_syscall_fnname()
取得當前系統架構下execve
系統呼叫的實際函式名稱 - 將我們的
hello
函式繫結到這個系統呼叫的 kprobe 事件
系統呼叫的實際名稱在不同的處理器架構上可能有所不同。使用 get_syscall_fnname()
能確保我們的程式在各種架構上都能正確執行。這是設計跨平台 eBPF 工具時的一個重要考量。在我開發監控工具時,常需要在 x86 和 ARM 架構間切換測試,這個函式幫了大忙。
追蹤輸出與資料收集
一旦 eBPF 程式被載入並繫結到事件,最後一步是收集它產生的資料:
b.trace_print()
這個函式會無限迴圈,從核心的追蹤管道讀取並顯示輸出,直到程式被手動終止。
trace_print()
實際上是從 /sys/kernel/debug/tracing/trace_pipe
這個特殊的虛擬檔案讀取資料。這是核心追蹤系統的標準輸出位置,不僅適用於 eBPF,也用於其他核心追蹤機制。瞭解這一點對於排查問題非常有幫助,因為我們可以直接檢視這個檔案的內容來驗證追蹤是否正常工作。
“Hello World” 程式的運作流程
當執行 “Hello World” eBPF 程式時,整個系統的工作流程如下:
- Python 程式編譯 C 程式碼並載入到核心
- 程式繫結到
execve
系統呼叫的 kprobe - 任何程式執行
execve
系統呼叫時,觸發 eBPF 的hello()
函式 hello()
函式呼叫bpf_trace_printk()
將訊息寫入追蹤管道- Python 程式從追蹤管道讀取並顯示訊息
執行 “Hello World” 的實際效果
當你執行這個程式時,你可能會立即看到一些追蹤輸出,因為系統上可能已經有其他程式在執行程式。如果沒有看到輸出,可以開啟第二個終端機並執行任何命令,就會看到相應的追蹤輸出:
$ hello.py
b' bash-5412 [001] .... 90432.904952: 0: bpf_trace_printk: Hello World'
這個輸出不僅包含了 “Hello World” 字串,還有一些關於觸發事件的上下文資訊:
bash-5412
表示觸發事件的程式是 bash,程式 ID 為 5412- 時間戳和其他追蹤相關的元資料
輸出中的上下文資訊是由核心追蹤基礎設施自動增加的,而非 eBPF 程式本身產生。這些資訊對於理解系統行為非常有價值。在我開發系統監控工具時,常依賴這些元資料來關聯不同事件,建立完整的系統行為圖景。
eBPF 的許可權要求
由於 eBPF 功能強大,能夠深入系統核心,因此需要特殊許可權才能使用。最簡單的方法是以 root 身份執行 eBPF 程式,例如使用 sudo
。
在核心 5.8 版本引入了 CAP_BPF
能力,它提供了執行某些 eBPF 操作的許可權,但對於不同型別的 eBPF 程式,可能還需要額外的能力:
- 追蹤程式需要
CAP_PERFMON
和CAP_BPF
- 網路程式需要
CAP_NET_ADMIN
和CAP_BPF
許可權控制是 eBPF 安全模型的重要部分。在設計生產環境的 eBPF 應用時,我總是遵循最小許可權原則,僅請求程式真正需要的能力。這不僅提高了安全性,也使得許可權問題更容易診斷。例如,若只需要網路功能,就不必請求追蹤相關的能力。
eBPF 的動態特性與透明性
“Hello World” 範例展示了 eBPF 的兩個關鍵特性:
動態行為變更
eBPF 程式可以動態改變系統行為,無需重新啟動機器或現有程式。一旦 eBPF 程式被繫結到事件,它立即開始生效,這使得 eBPF 成為即時監控與除錯的絕佳工具。
透明觀察
使用 eBPF 觀察系統行為時,無需修改被觀察的應用程式。任何執行 execve()
系統呼叫的程式都會觸發我們的 eBPF 程式,無論是直接從終端執行命令,還是透過指令碼執行可執行檔。這種透明性是 eBPF 作為觀察工具的一大優勢。
這兩個特性讓 eBPF 在生產環境中特別有價值。在我處理過的案例中,能夠在不重啟服務的情況下佈署監控工具,大降低了維運風險。同時,透明觀察能力讓我們能夠診斷那些難以重現的問題,因為我們可以在問題發生時立即附加 eBPF 程式,而不會影響正在執行的服務。
追蹤輸出的限制
雖然 bpf_trace_printk()
函式對於簡單的 “Hello World” 範例或基本除錯很有用,但它有很大的侷限性:
- 輸出格式缺乏彈性
- 只支援字串輸出,無法傳遞結構化資訊
- 所有 eBPF 程式都共用同一個追蹤管道,容易造成混淆
這些限制使得 bpf_trace_printk()
不適合用於複雜的 eBPF 應用。幸運的是,eBPF 提供了更強大的資料分享機制:BPF Maps。
BPF Maps:eBPF 的資料結構
Maps 是 eBPF 程式與使用者空間應用分享資料的主要機制,也是區分擴充套件版 BPF 和經典 BPF 的重要特性之一。
Maps 的基本概念
Map 是一種可以從 eBPF 程式和使用者空間存取的資料結構。它們可以用於多種場景:
- 使用者空間寫入設定資訊,供 eBPF 程式讀取
- eBPF 程式儲存狀態,供後續的 eBPF 程式讀取
- eBPF 程式寫入結果或指標,供使用者空間應用讀取和展示
Maps 解決了 eBPF 程式的一個根本限制:狀態儲存。由於 eBPF 程式本身是無狀態的(每次觸發都是獨立執行),Maps 提供了儲存和分享狀態的機制。在我設計複雜監控系統時,Maps 是不可或缺的元件,用於追蹤長時間執行的事務、累積統計資料,以及在不同 eBPF 程式間分享設定。
Maps 的多樣性
Linux 核心定義了多種型別的 BPF Maps,它們在 uapi/linux/bpf.h
檔案中定義。大多數 Maps 都是鍵值儲存,但它們針對不同的使用場景進行了最佳化:
根據索引的 Maps
- 陣列型別的 Maps 使用 4 位元組索引作為鍵
- 雜湊表型別的 Maps 可以使用任意資料型別作為鍵
專用操作的 Maps
- FIFO 佇列:先進先出的資料結構
- LIFO 堆積積疊:先進後出的資料結構
- LRU (最近最少使用) Maps:最佳化記憶體使用的資料結構
- 最長字首比對:用於網路由等場景
- 布隆過濾器:機率性資料結構,用於快速判斷元素是否存在
特殊物件的 Maps
- Sockmaps 和 Devmaps:儲存關於網路通訊端和裝置的資訊
- 程式陣列 Maps:儲存一組索引化的 eBPF 程式,用於實作尾呼叫
- Maps-of-Maps:支援複雜的階層式資料結構
Maps 的多樣性反映了 eBPF 應用場景的廣泛性。在我的實踐中,不同型別的 Maps 用於解決不同的問題:雜湊表適合追蹤程式行為,LRU Maps 適合管理有限資源的快取,程式陣列則用於實作複雜的決策邏輯。選擇正確的 Map 型別對於最佳化效能和資源使用至關重要。
Maps 的實際應用
Maps 的彈性和多樣性使其成為 eBPF 程式設計中最重要的工具之一。透過合理使用 Maps,我們可以:
- 在核心空間和使用者空間之間建立高效的資料通道
- 在不同的 eBPF 程式之間分享狀態和設定
- 實作複雜的資料處理邏輯,如聚合、過濾和轉換
在後續的開發中,我們將看到如何使用不同型別的 Maps 來實作各種功能,從簡單的計數器到複雜的網路流量分析。
eBPF 程式與系統的互動是透過特殊的輔助函式和 Maps 資料結構實作的。從簡單的 bpf_trace_printk()
到複雜的 Maps 型別,eBPF 提供了豐富的工具集來收集、處理和分享資料。
理解 eBPF 的動態特性和透明觀察能力,以及 Maps 在資料分享中的核心角色,是掌握 eBPF 程式設計的關鍵。雖然 bpf_trace_printk()
對於入門和簡單除錯很有用,但在實際應用中,Maps 提供了更強大、更彈性的解決方案。
隨著我們對 eBPF 理解的加深,我們將探索更多進階技術,如何有效地使用不同型別的 Maps 來解決實際問題,以及如何構建複雜的 eBPF 應用程式來監控和最佳化系統效能。