eBPF系列学习(4)了解libbpf、CO-RE (Compile Once – Run Everywhe) | 使用go开发ebpf程序(云原生利器cilium ebpf )
文章目錄
- 一、了解libbpf
- 1. BPF的可移植性CO-RE (Compile Once – Run Everywhere)
- BPF 可移植性面臨的問題
- BPF的可移植性CO-RE (Compile Once – Run Everywhere)
- 2. libbpf和bcc性能對比
- 3. 了解libbpf
- 4. libbpf 庫
- 構建基于libbpf的BPF應用需要使用BPF CO-RE的步驟
- 5. libbpf-bootstrap
- 二、使用go開發ebpf程序
- 1. Go 語言開發庫以及選擇
- 關于Cilium?
- 2. cilium/ebpf庫實踐
- cilium/ebpf 庫
- 使用go開發ebpf程序思路
- 官方demo: tracepoint_in_go
一、了解libbpf
在使用libbpf前,先使用bcc對eBPF相關知識進行學習運行,學習曲線將更平滑。相對于bcc,libbpf與BPF CO-RE的實際編譯部署的難度增大了。
1. BPF的可移植性CO-RE (Compile Once – Run Everywhere)
BPF 可移植性面臨的問題
BPF 程序是由用戶提供的、經過驗證之后在內核上下文中執行的程序。 BPF運行在內核內存空間(kernel memory space)執行,能訪問大量的 內核內部狀態(internal kernel state)。 這使得 BPF 程序功能極其強大,也是為什么它能成功地應用在大量不同場景的原因之一。
但另一方面,與強大能力相伴而生的是我們如今面臨的可移植性問題:BPF 程序 并不控制它運行時所在內核的內存布局(memory layout)。 因此,BPF 程序只能運行在開發和編譯這些程序時所在的內核。
另外,內核類型(kernel types)和數據結構(data structures)也在不斷變化。 不同的內核版本中,同一結構體的同一字段所在的位置可能會不同 —— 甚至已經 移到一個新的內部結構體(inner struct)中。此外,字段還可能會被重命名、刪除、 改變類型,或者(根據不同內核配置)被條件編譯掉。
一旦需要查看原始的內核內部數據(raw internal kernel data)—— 例如 常見的表示進程或線程的 struct task_struct,這個結構體中有非常詳細的進程信息 —— 那你就只能靠自己了。對于 tracing、monitoring 和 profiling 應用來說這個需求 非常常見,而這類 BPF 程序也是極其有用的。
內核版本不同:字段被重命名或移動位置
在這種情況下,如何保證讀到的一定是我們期望讀的那個字段呢 —— 例如,
- 原來的程序是從 struct task_struct offset 8 地址讀取數據,
- 由于新內核加個了 16 字節新字段,那此時正確的方式應該是從 offset 24 地址讀,
這還沒完:如果這個字段被改名了呢?例如,thread_struct 的 fs 字段(獲取 thread-local storage 用), 在 4.6 到 4.7 內核升級時就被重命名為了 fsbase。
內核版本相同但配置不同:字段在編譯時被移除(compile out)
另一種情況:內核版本相同,但內核編譯時的配置不同,導致 結構體的某些字段在編譯器時被完全移除了。
總結: 依賴開發環境本地的內核頭文件編譯的 BPF 程序, 是無法直接分發到其他機器運行 —— 然后期待它們返回正確結果的。 這是由于不同版本的內核頭文件所假設的內存布局是不同的。
這里有個很強的前提:內核頭文件在目標機器上一定存在。 在大部分情況下這都不是問題,但有時可能會帶來麻煩。
這對內核開發者來說也尤其頭疼,因為他們經常要編譯和部署一次性的內核,用于在 開發過程中驗證某些問題。而機器上沒有指定的、版本正確的內核頭文件包,基于 BCC 的應用就無法正常工作。
這種方式會拖慢開發和迭代速度。
總體來說,雖然 bcc 是一個很偉大的工具 —— 尤其是用于快速原型、實驗和開發小工具 —— 但 當用于廣泛部署生產 BPF 應用時,它存在非常明顯的不足。
為了更徹底地解決 BPF 移植性問題,我們設計了 BPF CO-RE,并相信這是BPF 程序的未來開發方式,尤其適用于開發復雜、真實環境中的 BPF 應用。
BPF的可移植性CO-RE (Compile Once – Run Everywhere)
官方:BPF CO-RE (Compile Once – Run Everywhere)
參考URL: https://github.com/libbpf/libbpf#bpf-co-re-compile-once–run-everywhere
BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
參考URL: https://www.cnblogs.com/charlieroro/p/14206214.html
eBPF 的內核無關是有限的,需要 eBPF 機制和開發者共同努力實現。除了 BCC 這種即時編譯的方案,還有另外一種名為 CO-RE (Compile Once – Run Everywhere) 的編譯方式,其核心依賴于 BTF(更加先進的 DWARF 替代方案)。
文章BPF Portability and CO-R 指出,為了提高BPF程序的便攜性,即在不同內核版本上正常工作,而無需為每個特定內核重新編譯的能力,社區提出了一個稱為BPF CO-RE(Compile Once – Run Everywhere)的解決方案。
Libbpf+BPF CO-RE的理念是,BPF程序與任何"正常"用戶空間程序沒有太大區別:它們應該匯編成小型二進制文件,然后以緊湊的形式進行部署,以瞄準主機。Libbpf 扮演 BPF 程序裝載機的角色,執行平凡的設置工作(重定位、加載和驗證 BPF 程序、創建 BPF map、連接到 BPF 掛鉤等),讓開發人員只擔心 BPF 程序的正確性和性能。這種方法將開銷保持在最低水平,消除沉重的依賴關系,使整體開發人員體驗更加愉快。
BPF CO-RE的目標是幫助BPF開發者使用一個簡單的方式解決簡單的可移植性問題(如讀取結構體字段),并使用它來定位復雜的可移植性問題(如不兼容的數據結構,復雜的用戶空間控制條件等)。使得開發者的BPF程序能夠"一次編譯–隨處運行"
Libbpf supports building BPF CO-RE-enabled applications, which, in contrast to BCC, do not require Clang/LLVM runtime being deployed to target servers and doesn’t rely on kernel-devel headers being available.
Libbpf 支持構建 BPF CO-RE-enabled 的應用程序,與BCC相比,它不需要部署的Clang/LLVM運行時,并且不依賴 kernel-devel 內核頭文件。
It does rely on kernel to be built with BTF type information, though. Some major Linux distributions come with kernel BTF already built in:
不過,它確實依靠內核來使用BTF類型信息構建。一些主要的Linux發行版本已內置的內核BTF:
Fedora 31+
RHEL 8.2+
OpenSUSE Tumbleweed (in the next release, as of 2020-06-04)
Arch Linux (from kernel 5.7.1.arch1-1)
Manjaro (from kernel 5.4 if compiled after 2021-06-18)
Ubuntu 20.10
Debian 11 (amd64/arm64)
如果您的內核不支持內置的BTF附帶,則需要構建自定義內核。你需要:
- pahole 1.16+ tool (part of dwarves package), which performs DWARF to BTF conversion;
- kernel built with CONFIG_DEBUG_INFO_BTF=y option;
- you can check if your kernel has BTF built-in by looking for /sys/kernel/btf/vmlinux file:
要開發和構建BPF程序,您將需要Clang/LLVM 10+。默認情況下,以下發行版具有clang/llvm 10+打包:
- Fedora 32+
- Ubuntu 20.04+
- Arch Linux
- Ubuntu 20.10 (LLVM 11)
- Debian 11 (LLVM 11)
- Alpine 3.13+
2. libbpf和bcc性能對比
性能優化大師 Brendan Gregg 在用 libbpf + BPF CO-RE 轉換一個 BCC 工具后給出了性能對比數據:
As my colleague Jason pointed out, the memory footprint of opensnoopas CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.
這句話原文暫未找到,TODO!
我們可以看到在運行時相比 BCC 版本,libbpf + BPF CO-RE 版本節約了近 9 倍的內存開銷。
3. 了解libbpf
擺脫對內核頭文件的依賴
除了使用內核的BTF信息進行字段的重定位意外,還可以將BTF信息生成一個大(基于5.10.1版本生成的長度有106382行)的頭文件(“vmlinux.h”),其中包含了所有的內核內部類型,可以避免對系統范圍的內核頭文件的依賴。可以使用如下方式生成vmlinux.h:
上述命令可以獲得到一個可兼容的C頭文件(即"vmlinux.h"),包含所有的內核類型("所有"意味著包含那些不會通過kernel-devel包暴露的頭文件)。
當使用了vmlinux.h,此時就不需要依賴像#include <linux/sched.h>, #include <linux/fs.h>這樣的頭文件,僅需要#include "vmlinux.h"即可。該頭文件包含了所有的內核類型:暴露了UAPI,通過kernel-devel提供的內部類型,以及其他一些更加內部的內核類型。
不幸的是,BTF(即DWARF)不會記錄#define宏,因此在vmlinux.h中丟失一些常用的宏。但大多數通常不存在的宏可以通過libbpf的bpf_helpers.h(即libbpf提供的內核側的庫)頭文件提供。
libbpf知道如何將BPF程序代碼匹配到特定的內核。它會查看程序記錄的BTF類型和重定位信息,然后將這些信息與內核提供的BTF信息進行匹配。 libbpf解析并匹配所有的類型和字段,更新必要的偏移以及重定位數據,確保BPF程序能夠正確地運行在特定的內核上。如果一切順利,則BPF應用開發人員會獲得一個BPF程序,這種方式可以針對目標主機上的內核進行“量身定制”,就好像程序是專門針對這個內核編譯的,但無需在應用程序中分發Clang以及在目標主機上的運行時中執行編譯,就可以實現所有這些目標。
libbpf 是一個比BCC更新的 BPF 開發庫,也是最新的 BPF 開發推薦方式!
2015年11月 Kernel 4.3 引入標準庫 libbpf, 該標準庫由Huawei 2012 OS內核實驗室的王楠提交。
2018年 為解決BCC的缺陷,CO-RE(Compile Once, Run Everywhere)的想法被提出并實現,最后達成共識:libbpf + BTF + CO-RE代表了eBPF的未來,BCC底層實現逐步轉向libbpf。
4. libbpf 庫
libbpf/bpftool 項目地址:https://github.com/libbpf/libbpf
libbpf庫是基于C / C ++的通用eBPF庫,提供了一些加載bpf程序的方法,封裝了內核提供的bpf()系統調用。
eBPF 程序加載的本質是 BPF 系統調用,Linux 內核通過 BPF 系統調用提供 eBPF 相關的一切操作,比如:程序加載、map 創建刪除等。常見的 loader 都是對這個系統調用的封裝,部分 loader 提供更加原生接近系統調用的操作,部分 loader 則是進行了更多封裝使得編程更便捷。
內核實現的 libbpf 庫,封裝了 BPF 系統調用,使得加載 BPF 程序更便捷。libbpf 不像 iproute2,它能夠使 BPF 相關操作更為便捷,沒有做過多封裝。如果要將程序加載到內核,則需要自己實現一個用戶態程序,調用 libbpf 的 API 去加載到內核。如果要復用 pinned 在 BPF 文件系統的 MAP,也需要用戶態程序調用 libbpf 的 API,在加載程序時進行相關處理。
典型案例:Facebook 開源的 katran 項目使用 libbpf 加載 eBPF 程序。
構建基于libbpf的BPF應用需要使用BPF CO-RE的步驟
構建基于libbpf的BPF應用需要使用BPF CO-RE包含的幾個步驟:
- 生成帶所有內核類型的頭文件vmlinux.h;
- 使用Clang(版本10或更新版本)將BPF程序的源代碼編譯為.o對象文件;
- 使用 libbpfgo 將其編譯為二進制文件,加載到內核中并監聽輸出。
5. libbpf-bootstrap
原文鏈接:Building BPF applications with libbpf-bootstrap
開始使用 BPF 在很大程度上仍然令人生畏,因為即使為簡單的"Hello World"般的 BPF 應用程序設置構建工作流,也需要一系列步驟,對于新的 BPF 開發人員來說,這些步驟可能會令人沮喪和令人生畏。這并不復雜,但知道必要的步驟是一個(不必要的)困難的部分。
libbpf-bootstrap 就是這樣一個 BPF 游樂場,它已經盡可能地為初學者配置好了環境,幫助他們可以直接步入到 BPF 程序的書寫。它綜合了 BPF 社區多年來的最佳實踐,并且提供了一個現代化的、便捷的工作流。libbpf-bootstrap 依賴于 libbpf 并且使用了一個很簡單的 Makefile。對于需要更高級設置的用戶,它也是一個好的起點。即使這個 Makefile不會被直接使用到,也可以很輕易地遷移到別的構建系統上。
二、使用go開發ebpf程序
ebpf的核心程序是通過c編寫,clang進行編譯的。在編譯好ebpf程序后,我們需要將其加載到內核中。目前有很多個項目對ebpf的編寫調試運行的流程進行了優化,比較有名的是bcc和libbpf。很多時候我們希望能夠更加方便的進行程序編寫和部署,也希望程序能夠在不同的linux發行版和內核上使用(即BPF CO-RE),bcc的運行依賴內核的頭文件,也引入了繁重的整個clang llvm工具鏈,libbpf只能使用C/C++進行外部程序的開發。
BCC、libbpf都主要使用 C 語言開發 eBPF 程序,而實際的應用程序可能會以多種多樣的編程語言進行開發。所以,開源社區也開發和維護了很多不同語言的接口,方便這些高級語言跟 eBPF 系統進行交互。
BCC 就提供了 Python、C++ 等多種語言的接口,而使用 BCC 的 Python 接口去加載 eBPF 程序,要比 libbpf 的方法簡單得多。
隨著ebpf的發展,開源社區中也誕生了各種編程語言的開發庫,特別是 Go 和 Rust 這兩種語言,其開發庫尤為豐富。
1. Go 語言開發庫以及選擇
使用 Go 語言管理和分發 ebpf 程序
參考URL: https://www.ebpf.top/post/ebpf_go/
目前使用 Go 開發 eBPF 程序可以使用的框架有 IO Visor-gobpf、Dropbox-goebpf和 Cilium-ebpf等,考慮到 Cilium 的社區活躍度和未來的發展,使用 Cilium 的 ebpf 是一個比較不錯的選擇。
每個庫都有各自的范圍和限制:
- Calico 在用 bpftool 和 iproute2 實現的 CLI 命令基礎上實現了一個 Go 包裝器。
- Aqua 實現了對 libbpf C 庫的 Go 包裝器。
- Dropbox 支持一小部分程序,但有一個非常干凈和方便的用戶API。
- IO Visor 的 gobpf 是 BCC 框架的 Go 語言綁定,它更注重于跟蹤和性能分析。
- Cilium 和 Cloudflare 維護一個 純 Go 語言編寫的庫 (以下簡稱 “libbpf-go”),它將所有 eBPF 系統調用抽象在一個本地 Go 接口后面。
在使用這些 Go 語言開發庫時需要注意,Go 開發庫只適用于用戶態程序中,可以完成 eBPF 程序編譯、加載、事件掛載,以及 BPF 映射交互等用戶態的功能,而內核態的 eBPF 程序還是需要使用 C 語言來開發的。
當涉及到選擇庫和工具來與 eBPF 進行交互時,會讓人有所困惑。在選擇時,你必須在基于 Python 的 BCC 框架、基于 C 的 libbpf 和一系列基于 Go 的 Dropbox、Cilium、Aqua 和 Calico 等庫中選擇。
庫貢獻者的活躍度
總結: cilium/ebpf > iovisor/gobpf > dropbox/goebpf > aquasecurity/libbpfgo
關于Cilium?
cilium
n. 纖毛;睫毛;
[例句]Cilium is the major power source of pallium to transport materials.
纖毛是外套膜進行物質運輸的主要動力來源。
[其他] 復數:cilia
eBPF是一項革命性的技術,可以在Linux內核中運行沙盒程序,而無需重新編譯內核或加載內核模塊。在過去的幾年中,eBPF已成為解決以前依賴于內核更改或內核模塊的問題的標準方法。
eBPF 在動態跟蹤、網絡、安全以及云原生等領域的廣泛應用。
Cilium是一個開源項目,它的基礎是基于eBPF的Linux內核技術,用于透明地提供和保護使用Linux容器管理平臺部署的應用程序服務之間的網絡和API連接。以解決容器工作負載的新可伸縮性,安全性和可見性要求。Cilium超越了傳統的容器網絡接口(CNI),可提供服務解析,策略執行等功能。
Cilium已迅速成為Kubernetes生態系統中的領先技術,為Google Kubernetes Engine(GKE)提供了網絡數據平面,并在其他領先的云原生最終用戶(包括Adobe,DataDog,GitLab和DigitalOcean)中得到采用。
Cilium 母公司 Isovalent
Liz Rice,是Isovalent的首席開源官,這家公司是Cilium網絡項目的幕后推手。也是CNCF技術監督委員會主席
Cilium 是一個用于容器網絡領域的開源項目,主要是面向容器而使用,用于提供并透明地保護應用程序工作負載(如應用程序容器或進程)之間的網絡連接和負載均衡。
2. cilium/ebpf庫實踐
cilium/ebpf 純 Go 程序編寫,從而實現了程序最小依賴;與此同時其還提供了 bpf2go 工具,可用來將 eBPF 程序編譯成 Go 語言中的一部分,使得交付更加方便。
因此,本文也選擇基于 cilium/ebpf 庫來開發和實踐。
cilium/ebpf 庫
cilium/ebpf庫
github: https://github.com/cilium/ebpf
ebpf的核心程序是通過c編寫,clang進行編譯的。在編譯好ebpf程序后,我們需要將其加載到內核中。目前有很多個項目對ebpf的編寫調試運行的流程進行了優化,比較有名的是bcc和libbpf。很多時候我們希望能夠更加方便的進行程序編寫和部署,也希望程序能夠在不同的linux發行版和內核上使用(即BPF CO-RE),bcc的運行依賴內核的頭文件,也引入了繁重的整個clang llvm工具鏈,libbpf只能使用C/C++進行外部程序的開發。
如果想使用go編寫,有兩個選擇:cilium的ebpf項目和libbpf-go,考慮的社區活躍度和未來的發展,也許使用cilium的ebpf工具比較合適。
eBPF 程序加載的本質是 BPF 系統調用,Linux 內核通過 BPF 系統調用提供 eBPF 相關的一切操作,比如:程序加載、map 創建刪除等。常見的 loader 都是對這個系統調用的封裝,部分 loader 提供更加原生接近系統調用的操作,部分 loader 則是進行了更多封裝使得編程更便捷。
cilium/ebpf 庫是一個 GO 語言版本的 libbpf 庫,它封裝了 BPF 系統調用,與內核提供的 libbpf 類似。使用 cilium/ebpf 庫實現用戶態程序加載 eBPF 到內核,在很多方面都類似 libbpf,區別在于這個庫是 GO 語言的,更加方便使用 GO 語言構建一套 eBPF 程序的控制面方案。
純go庫用于讀取,修改和加載EBPF程序,并將其連接到Linux內核中的各種鉤子上。
使用go開發ebpf程序思路
官方github: https://github.com/cilium/ebpf
使用 Go 語言開發 ebpf 程序
https://houmin.cc/posts/adca5ae5/
開發者只需要實現內核態C文件,用戶態go文件,用戶態event消息結構體三個文件即可,框架會自動加載執行。
- 運行在內核態用C寫eBPF代碼,llvm編譯為eBPF字節碼。
- 用戶態使用golang編寫,cilium/ebpf純go類庫,做eBPF字節碼的內核加載,kprobe/uprobe HOOK對應函數。
- 用戶態使用golang做事件讀取、解碼、處理。
cilium/ebpf是一個純GO庫,可提供用于加載,編譯和調試EBPF程序的實用程序。它具有最小的外部依賴性,旨在用于長期運行的過程中。
官方demo: tracepoint_in_go
官方demo參考路徑:
examples/tracepoint_in_go/main.go
//此程序演示如何將eBPF程序附加到跟蹤點。
//程序附加到syscall/sys_enter_openat跟蹤點,并且
//每次 syscall 時,打印出整數123。
//基于預先存在的內核hook(tracepoint)打開跟蹤事件。
//每次用戶空間程序使用’openat()’ 系統調用時,eBPF
//將執行上面指定的程序,并顯示“123”值 在 perf ring 中
純go庫用于讀取,修改和加載EBPF程序,并將其連接到Linux內核中的各種鉤子上。
這是官方完全go寫的demo,演示了 syscall/sys_enter_openat跟蹤點。
總結
以上是生活随笔為你收集整理的eBPF系列学习(4)了解libbpf、CO-RE (Compile Once – Run Everywhe) | 使用go开发ebpf程序(云原生利器cilium ebpf )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [0893]《兽医外科学》
- 下一篇: 5_整体架构优化