eBPF 是一個用於訪問 Linux 內核服務和硬件的新技術,因爲其靈活性和高性能等特色,被迅速用於網絡、出錯、跟蹤以及防火牆等多場景。目前國內已有少數企業開始嘗試將 eBPF 引入生產實踐,又拍雲也是其中一個。專爲技術開發者提供知識分享的 Open Talk 公開課邀請了又拍雲開發工程師周晨約直播分享 eBPF 的學習經驗與開發心得,並對其分享內容進行整理,下拉至文末點擊閱讀原文可回看原視頻。html
你們好,今天分享的主題是《eBPF 探索之旅》,圍繞三部分展開:前端
eBPF 是什麼,從字面上來看是擴展伯克利包處理器,那伯克利包處理器是什麼呢?python
在此以前先來了解一個性能優秀的經常使用抓包工具:tcpdumpios
tcpdumpnginx
圖中展現了兩個經常使用指令網絡
指令一:指定 IP 和端口,能夠抓到 IP 爲 220.173.103.227,端口爲 80 的包架構
指令二:加上 grep,能夠過濾出帶有 route 字段的數據socket
那麼 tcpdump 又是如何作到經過用戶提供的規則處理網絡上收到的包,再 copy 給用戶的呢?若是放在用戶層,就須要在系統裏全部 socket 讀寫的時候作一層處理,把規則放上去,這樣作難度太大。而 tcpdump 是基於 libpcap 庫實現的,libpcap 能作到在驅動將包交給內核網絡時,把包取過來,經過用戶傳給 libpcap 的規則將須要的網絡包 copy 一份給用戶,再把包傳給內核網絡棧,而之因此 libpcap 能作到這點全靠 BPF。tcp
BPF函數
BPF 是基於寄存器虛擬機實現的,支持 jit,比基於棧實現的性能高不少。它能載入用戶態代碼而且在內核環境下運行,內核提供 BPF 相關的接口,用戶能夠將代碼編譯成字節碼,經過 BPF 接口加載到 BPF 虛擬機中,固然用戶代碼跑在內核環境中是有風險的,若有處理不當,可能會致使內核崩潰。所以在用戶代碼跑在內核環境以前,內核會先作一層嚴格的檢驗,確保沒問題纔會被成功加載到內核環境中。
eBPF:BPF 的擴展
回到 eBPF,它做爲一個 BPF 的擴展,都擴展了些什麼呢?
編寫 eBPF 程序的內核最低也要是 3.15,此版本恰好能夠支持 eBPF ,但這時 eBPF 支持的特性比較少,不建議使用,最好是 4.8 以上的內核,內核越新 eBPF 支持的功能就越成熟。另外像 kprobe、uprobe、traceport 相關的參數要開起來,不然只能用 BPF的某些特性,而沒法使用eBPF 的特性,至關因而空殼。經過路徑 /lib/modules/uname-r
/source/.config 或者在 /boot/ 下查找對應版本的內核 config 來查看系統是否開啓了所需的參數。
編寫 eBPF 程序的對環境也有必定的要求。eBPF 代碼須要編譯成 llvm 的字節碼,纔可以在 eBPF 及虛擬機中運行,所以須要安裝 llvm 以及 clang,安裝好以後能夠經過 llc 來查看是否支持 BPF。
eBPF 代碼示例
內核、環境都準備好後就能夠開始編寫工做了。若是是不借助任何工具直接手寫一個 eBPF 程序會很是的困難,由於內核提供的文檔對如何編寫 eBPF 程序的說明是比較缺少的。固然內核也有提供工具,在內核包中的 bpftool 工具。推薦是使用工具 bcc,它可以下降寫 BPF 程序的難度,提供了python、lua 的前端。以 python 爲例,只須要寫好須要載入 eBPF 的 C代碼,再經過 bcc 提供的 BPF 類就能夠將代碼載入到 eBPF 虛擬機中,執行 python 程序,代碼就能夠運行起來了。
圖中是 bcc 工具的使用例子,代碼很是簡單,導入一下 BPF,進行 BPF 初始化。
下面是經過 kprobe 監控機器 tcp(ipv4)的鏈接狀態變化。首先須要知道 tcp 狀態變化時內核會調用哪些函數。除了 time-wait 狀態以外,其餘狀態基本上是經過 tcp_set_state 設置的。在 time-wait 階段的時候,內核會建立一個新的結構體去存 time-wait 的 socket,內核考慮到內存的開銷問題,以前的 socket 會釋放掉。先不考慮 time-wait。
接下來看看具體的代碼,上圖中是載入到 eBPF 的 C 代碼。
經過內核的幾個參數,內核的結構體 socket,以及這個函數傳進來的一些 state,能夠獲取當時 tcp 鏈接的狀態轉化狀況,上圖函數的第一個參數 ctx 其實是寄存器,後面是要介入函數的兩個參數。這裏會把一些 tcp 的狀態存起來,使用 perf_submit 將這些狀態更新到 perf ring buffer 中,就能夠在用戶態把 perf ring buffer 東西給讀出來,這就是 tcp 的一些狀態變化。
上圖是 python 代碼。
利用 uprobe 查看應用服務信息
上圖是經過 uprobe 查看 nginx 請求分佈的狀況。首先要看 nginx 建立請求的位置,是在 ngx_http_create_request,和以前同樣寫一個要嵌入 eBPF 虛擬機的 C 代碼,仍是建立一個 HASH 表,名稱是 req_distr,key 是 32 位大小,value 是 64 位,核心函數是 check_ngx_http_create_request,在 nginx 調用該函數時,會執行這個鉤子函數,函數內部調用的是 count_req。把 PID 和 PID 上建立的請求次數對應起來,當 PID 調用過 ngx_http_create_request 時,請求計數就會 +1。如此也就能夠看到整個請求在各個 work 上的分佈狀況。
圖中是 python 代碼,一樣把 C 代碼讀進來,並調用 bbf 把代碼編譯成 llvm 字節碼,載入到 eBPF 虛擬機中,再調用 attach_uprobe。name 是指 nginx 的一個二進制文件,sym 是指要在哪一個函數中打個斷點,上圖是 ngx_http_create_request 函數。fn_name 是在 ngx_http_create_request 函數執行的時候須要調用的函數。另外須要注意二進制文件必需要把編譯符號開放出來,好比編譯的時加個 -g,不然會找不到這個函數。最下面是簡單地獲取 HASH 表,去輸出 HASH 表的 key 和 value,這樣就能看到 pid 對應的 request 數量,pid 也就會對應着 worker,如此就可以查看到運行 nginx 的請求分佈狀況。
查看運行中的 eBPF 程序與 map
能夠經過內核包中 bpftool 提供的 bpftool 工具查看,它的目錄是在 /lib/modules/uname-r
/tools/bpf/bpftool 中,須要本身編譯一下,在 /lib/modules/uname-r
/tools 下執行 make-C/bpf/bpftool 就能夠了。
上圖是 bpftool 工具查看 map(前面 BPF_HASH 建立的)狀況的效果,-p 參數,可以展現得好看一些。prog 參數能夠把在虛擬機中跑的程序給展現出來。這樣就能看到到底運行了那些 eBPF 程序以及申請的 map。