本文主要目的是讓你了解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的使用圃阳,但不久之后它或多或少就銷聲匿跡了厌衔。原因在于,對于大多數(shù)人來說捍岳,獲得對網絡流量的內核級可編程控制實際上并不是那么重要富寿,因為工作負載都運行在裸機或vm上睬隶,并且可以通過防火墻和管理程序很好地管理流量。
這一切都在2013年開始改變页徐,當Docker出現(xiàn)時苏潜,突然間容器就大放異彩了。(順便說一句变勇,Docker的發(fā)展類似于eBPF恤左,容器實際上已經存在了幾十年,所以Docker所做的并不是真正的新事物搀绣;相反飞袋,Docker真正的成就是它第一次成功地讓容器流行起來,這主要歸功于引入了更好的工具)链患。與一年后出現(xiàn)的Kubernetes相結合巧鸭,Docker引領了容器世界,在這個世界里麻捻,以非常細粒度的纲仍、逐個容器的方式過濾和控制流量的能力變得非常有價值。
走進eBPF
因此在2014年引入了eBPF芯肤,它通過提供允許程序直接在Linux內核空間中運行的工具,擴展了BPF的原始架構压鉴。我們將在稍后討論為什么這在容器和Kubernetes的環(huán)境中很關鍵崖咨。但首先,讓我們退一步油吭,先解釋在內核空間中運行程序意味著什么击蹲。
基本上,eBPF代碼是由內核執(zhí)行的婉宰,而不像標準應用程序那樣運行在“用戶空間”歌豺。這很重要,主要有三個原因:
1心包、它允許代碼高效地運行类咧。
2、它允許代碼訪問底層內核資源蟹腾,否則從用戶空間訪問這些資源很復雜和昂貴的(就資源開銷而言)痕惋。
3、它允許你觀察用戶空間中運行的任何程序——這對于在用戶空間中運行的可觀察性工具是很難做到的娃殖。
這就是為什么Brendan Gregg等人稱eBPF為“無價的技術”值戳,并將其與JavaScript進行比較:
JavaScript不是靜態(tài)的HTML網站,而是允許你定義在鼠標點擊等事件上運行的小程序炉爆,這些事件在瀏覽器中的安全虛擬機中運行堕虹。使用eBPF卧晓,而不是固定的內核,你現(xiàn)在可以編寫運行在磁盤I/O等事件上的小程序赴捞,這些事件在內核的安全虛擬機中運行逼裆。
如果你是一個鐵桿的Linux極客,你可能會想:“在使用內核模塊之前螟炫,eBPF做了哪些我不能做的事情?”
這是一個合理的問題波附。的確,在內核空間中使用內核模塊執(zhí)行代碼早就可以做到了昼钻。但問題是這些模塊必須被插入到內核中掸屡,這使得它們的部署更加復雜。它們還往往具有復雜的依賴關系然评,這增加了部署方面的麻煩仅财。而且它們不是特別安全,這要求你信任用戶只插入既穩(wěn)定又安全的模塊碗淌。
當然盏求,從理論上講,你還可以修改Linux內核本身亿眠,以便在內核空間中運行你想要的任何代碼碎罚。但是一個普通的開發(fā)人員不能簡單地修改Linux內核——除非他是一個內核程序員(相對而言,這樣的程序員并不多)纳像,或者他想要維護某種自定義的內核分支荆烈,這將是一個管理的噩夢。
從你提出需求到Linux社區(qū)接受你的需求大概需要一年時間竟趾,再從Linux內核到你使用的發(fā)型版本需要5年憔购,這就是一個噩夢。
eBPF允許自定義程序在獨立的內核級虛擬機中運行岔帽,從而解決了所有這些問題玫鸟。我們將在下面看到,你可以在各種工具的幫助下犀勒,輕松地部署你所選擇的代碼屎飘,而不必處理內核模塊依賴關系,接觸內核源代碼贾费,甚至不必擁有root權限枚碗。再見,modprobe铸本;你好肮雨,eBPF字節(jié)碼!
eBPF架構
現(xiàn)在你已經知道了eBPF是怎么發(fā)展起來的,以及它為什么如此強大箱玷,讓我們來談談它的實際工作原理怨规。
一般來說陌宿,要部署eBPF程序,你需要做這些事情:
編寫和編譯代碼
eBPF代碼通常是用“限制性C”編寫的波丰,然后編譯成eBPF字節(jié)碼壳坪。Clang是事實上的編譯標準。
在編寫代碼時掰烟,可以引用bpf_helper函數(shù)來執(zhí)行各種常見操作爽蝴,如內存復制、檢索PID和時間戳屬性以及與其他應用程序通信(需定義eBPF數(shù)據結構纫骑,eBPF maps)蝎亚。因此,你通常不必從頭開始編寫大量自定義代碼先馆。定制代碼僅限于你想要實現(xiàn)的特定功能耻陕。
檢驗與加載
要部署已編譯的eBPF程序吁津,首先調用bpf()系統(tǒng)調用簿姨,它將字節(jié)碼傳遞給內核檢驗器馍刮。內核檢驗器的工作是確保程序不會對內核造成問題。如果驗證成功仿野,內核JIT編譯器將把它轉換為可執(zhí)行的機器代碼铣减。
運行時(runtime)
加載并驗證后,程序就可以執(zhí)行了脚作。它將監(jiān)視你附加到的任何代碼流—無論是在內核空間葫哗、用戶空間還是兩者都有。一旦它運行鳖枕,你就可以使用eBPF映射或預定義的文件描述符訪問程序輸入或輸出魄梯。
下面摘自Cilium的Golang eBPF框架的部分代碼片段應該有助于說明這個過程(上面是eBPF代碼桨螺,下面是加載eBPF程序并與之通信的用戶空間應用程序)宾符。在編譯和運行應用程序時,它將計算系統(tǒng)上正在執(zhí)行的新程序的數(shù)量灭翔。很整潔!
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用例魏烫。容器和Kubernetes可觀測性只是其中之一。
但當涉及到觀察容器和Kubernetes集群時肝箱,eBPF尤其令人興奮哄褒。為什么?因為eBPF允許你獲得“干凈的”數(shù)據來跟蹤任何類型的事件——例如網絡操作或用戶空間中的事件煌张。數(shù)據直接來自Linux內核呐赡,具有最小的性能開銷。這意味著你可以部署一個eBPF程序骏融,在每個數(shù)據包進入或發(fā)送出主機時監(jiān)視它链嘀,然后將其映射到該主機上運行的進程或容器萌狂。其結果是對網絡流量發(fā)生的情況的細粒度可見性。
再加上eBPF程序非常高效和安全怀泊,很容易理解為什么人們現(xiàn)在對eBPF如此興奮茫藏,以及它在云原生世界中為可觀測性解鎖的可能性。
eBPF SDKs的使用
曾經有一段時間——也就是說霹琼,在2010年代中期——編寫和加載eBPF程序是一項大量的工作务傲,因為圍繞eBPF的工具還不成熟。
在過去幾年中枣申,由于引入了更多簡化eBPF使用的工具售葡,以及bpf_helper函數(shù)和eBPF映射的不斷改進,這種情況已經發(fā)生了變化糯而。
同時天通,外部工具鏈有助于簡化eBPF的引導和開發(fā)。關鍵的例子包括BCC和libpf(它現(xiàn)在作為Linux內核的一部分進行維護熄驼,因此開始成為事實上的選項)像寒。如上所述,還有一些eBPF友好的編譯器瓜贾,如Clang诺祸。
而且,對于那些希望在現(xiàn)代語言開發(fā)方面提高eBPF使用水平的人來說祭芦,有一些解決方案要感謝那些能夠編寫用戶空間代碼來與eBPF程序交互的項目筷笨,比如Python,Golang和Rust龟劲。
總的來說胃夏,eBPF工具鏈生態(tài)系統(tǒng)正在快速發(fā)展,現(xiàn)在確切地說哪些工具最終會得到廣泛采用還為時過早昌跌。但是我們可以肯定地說仰禀,圍繞eBPF的工具越來越成熟,它給了開發(fā)人員越來越少的理由回避使用eBPF蚕愤。多虧了這些偉大的工具答恶,即使是我們中間最缺乏動力的開發(fā)者也可以編寫和加載eBPF程序!
eBPF的缺點
總的來說,值得注意的是萍诱,eBPF并不能解決實現(xiàn)中的所有問題悬嗓,而且它受到某些可能永遠不會消失的限制。
一是編寫符合內核檢驗器的eBPF程序可能很棘手裕坊,尤其是對新手來說包竹。如果你的程序被拒絕,檢驗器并不會詳細地解釋原因。獨立于檢驗器的工具有助于解決這一挑戰(zhàn)周瞎,但它們并不能消除程序被拒絕的風險悟狱,并且在試圖找出原因時,你將變得非常沮喪堰氓。而且挤渐,由于驗證只在運行時才發(fā)生,你面臨的風險是双絮,一個內核接受了你的程序浴麻,而另一個內核版本有可能拒絕。
eBPF的另一個限制是eBPF程序在堆椂谂剩空間上受到限制软免,這使得開發(fā)更加困難和不太直觀。(它們以前在指令大小上也有限制焚挠,但在內核5.3中有效地消除了這一限制)膏萧。你需要學習如何高效地編寫eBPF代碼,以使其能夠大規(guī)模地工作蝌衔。
第三個問題是榛泛,盡管eBPF是在Linux內核中實現(xiàn)的,但不同Linux發(fā)行版之間的內核版本和自定義之間的差異意味著eBPF程序并不總是像你期望的那樣可移植噩斟。如果你有一些節(jié)點使用Alpine Linux曹锨,其他節(jié)點運行Ubuntu,那么你可能會發(fā)現(xiàn)你的eBPF程序不能跨所有節(jié)點工作剃允。改進eBPF可移植性的工作正在進行中沛简,但它仍然不像你想的那樣天衣無縫。
eBPF的美好未來
我們不知道十年或二十年后人們是否還會吃牛油果吐司斥废。但是我們非常有信心椒楣,Kubernetes的開發(fā)人員和管理員將利用eBPF來幫助理解節(jié)點和pod內部發(fā)生的事情。
考慮到eBPF生態(tài)系統(tǒng)正變得越來越有組織牡肉,這一點尤其如此捧灰,這要歸功于eBPF基金會等組織。正如Thomas Graf提到的eBPF發(fā)展荚板,我們開始看到“像谷歌和Facebook這樣的大公司在維護和推動eBPF的發(fā)展凤壁》砸伲”
所以跪另,如果你一直在抵抗eBPF革命,現(xiàn)在是投降的時候了煤搜。Kubernetes可觀測性的未來——除其他外——取決于eBPF免绿,你也可以開始學習使用它。