聊聊風口上的 eBPF


eBPF 是一個用於訪問 Linux 內核服務和硬件的新技術,由於其靈活性和高性能等特點,被迅速用於網絡、出錯、跟蹤以及防火牆等多場景。目前國內已有少數企業開始嘗試將 eBPF 引入生產實踐,又拍雲也是其中一個。專為技術開發者提供知識分享的 Open Talk 公開課邀請了又拍雲開發工程師周晨約直播分享 eBPF 的學習經驗與開發心得,並對其分享內容進行整理,下拉至文末點擊閱讀原文可回看原視頻。

大家好,今天分享的主題是《eBPF 探索之旅》,圍繞三部分展開:

  • eBPF 是什么

  • eBPF 能做什么

  • 如何編寫 eBPF 程序

認識 eBPF

eBPF 是什么,從字面上來看是擴展伯克利包處理器,那伯克利包處理器是什么呢?

在此之前先來了解一個性能優秀的常用抓包工具:tcpdump

tcpdump

圖中展示了兩個常用指令

指令一:指定 IP 和端口,可以抓到 IP 為 220.173.103.227,端口為 80 的包

指令二:加上 grep,可以過濾出帶有 route 字段的數據

那么 tcpdump 又是如何做到通過用戶提供的規則處理網絡上收到的包,再 copy 給用戶的呢?如果放在用戶層,就需要在系統里所有 socket 讀寫的時候做一層處理,把規則放上去,這樣做難度太大。而 tcpdump 是基於 libpcap 庫實現的,libpcap 能做到在驅動將包交給內核網絡時,把包取過來,通過用戶傳給 libpcap 的規則將需要的網絡包 copy 一份給用戶,再把包傳給內核網絡棧,而之所以 libpcap 能做到這點全靠 BPF。

BPF

BPF 是基於寄存器虛擬機實現的,支持 jit,比基於棧實現的性能高很多。它能載入用戶態代碼並且在內核環境下運行,內核提供 BPF 相關的接口,用戶可以將代碼編譯成字節碼,通過 BPF 接口加載到 BPF 虛擬機中,當然用戶代碼跑在內核環境中是有風險的,如有處理不當,可能會導致內核崩潰。因此在用戶代碼跑在內核環境之前,內核會先做一層嚴格的檢驗,確保沒問題才會被成功加載到內核環境中。

eBPF:BPF 的擴展

回到 eBPF,它作為一個 BPF 的擴展,都擴展了些什么呢?

  • 首先在功能上,不僅僅局限於網絡,它能夠借助 kprobe 獲取內核函數運行信息,這樣調試內核就不需要 gdb 或者加入內核探點重新編譯內核。

  • 可以借助 uprobe 獲取用戶函數的運行信息,kprobe 和 uprobe 不僅能獲取函數運營信息,還可以獲取代碼執行到了哪一行時的寄存器以及棧信息,其原理可以理解為在某個指令打斷點,當 cpu 執行到這個斷點的時候,cpu 會保存當前的寄存器信息,然后單步執行斷點持載的 handler,也是想要在內核中執行的邏輯,執行完成后 cpu 會回到這個斷點的位置,恢復寄存器的狀態,然后繼續運行下去。

  • 支持 tracepoint,即在寫代碼中加入 trace 點,獲取執行到這點時的信息。

  • 可以嵌入到 perf_event 中。我們熟知的 XDP 以及 tc 都是基於 eBPF 實現的,並且在性能上有着不俗的表現。

eBPF 的功能

  • 系統性能監控/分析工具:能夠實現性能監控工具、分析工具等常用的系統分析工具,比如 sysstate 工具集,里面提供了 vmstate,pidstat 等多種工具,一些常用的 top、netstat(netstat 可被 SS 替換掉),uptime、iostat 等這些工具多數都是從 /proc、/sys、/dev 中獲取的會對系統產生一定的開銷,不適合頻繁的調用。比如在使用 top 的時候通過 cpu 排序可以看到 top cpu 占用也是挺高的,使用 eBPF 可以在開銷相對小的情況下獲取系統信息,定時將 eBPF 采集的數據 copy 到用戶態,然后將其發送到分析監控平台。

  • 用戶程序活體分析:做用戶程序活體分析,比如 openresty 中 lua 火焰圖繪制,程序內存使用監控,cdn 服務異常請求分析,程序運行狀態的查看,這些操作都可以在程序無感的情況下做到,可以有效提供服務質量。

  • 防御攻擊:比如 DDoS 攻擊,DDoS 攻擊主要是在第七層、第三層以及第四層。第七層的攻擊如 http 攻擊,需要應用服務這邊處理。第四層攻擊,如 tcp syn 可以通過 iptable 拒絕異常的 ip,當然前提是能發現以及難點是如何區分正常流量和攻擊流量,簡單的防攻擊會導致一些誤傷,另外 tcp syn 也可以通過內核參數保護應用服務。第 3 層攻擊,如 icmp。對於攻擊一般會通過一些特殊的途徑去發現攻擊,而攻擊的防御則可以通過 XDP 直接在網絡包未到網絡棧之前就處理掉,性能非常的優秀。

  • 流控:可以控制網絡傳輸速率,比如 tc。

  • 替換 iptable:在 k8s 中 iptable 的規則往往會相當龐大,而 iptable 規則越多,性能也越差,使用 eBP 就可以解決,關於這方面有很多開源的實踐可以參考。

  • 服務調優:如下圖所示,在 cdn 服務中難免會出現一些指標突刺的情況,這種突刺拉高整體的指標,對於這種突刺時常會因為找不到切入點而無從下手,eBPF 存在這種潛力能幫助分析解決該問題,當 eBPF 發現網絡抖動,會主動采集當時應用的運行狀態。

eBPF 程序實踐

編寫 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 初始化。

  • text 是要執行的代碼,里面是一個函數

  • kprobe__schedule 內容是調用 bpf_trace_printk(“hello world\n”);return 0

  • kprobe__schedule 的含義是用 kprobe的 特性在內核調用 schedule 函數的時候調用 bpf_trace_printk,打出 hello world

  • bpf_trace_printk 會把這些輸出到 /sys/kernel/debug/tracing/trace_pipe 里,后面的 trace_print 就可以把數據打印出來

下面是通過 kprobe 監控機器 tcp(ipv4)的連接狀態變化。首先需要知道 tcp 狀態變化時內核會調用哪些函數。除了 time-wait 狀態之外,其他狀態基本上是通過 tcp_set_state 設置的。在 time-wait 階段的時候,內核會創建一個新的結構體去存 time-wait 的 socket,內核考慮到內存的開銷問題,之前的 socket 會釋放掉。先不考慮 time-wait。

接下來看看具體的代碼,上圖中是載入到 eBPF 的 C 代碼。

  • 最上面的 BPF_HASH 表示創建一個 BPF 提供的 HASH 表;last 是 HASH 表的名稱;struct sock* 是指 key 的大小,這里表示指針大小;uint64_t 是 value 的大小,為 64 位;最后的 10240 表示 map 最多能夠放多少個元素。

  • 往下是一個結構體 bcc_tcp_state,可以看到后面有一個 BPF_PERF_OUTPUT,它是利用到了 perf ring buffer 的一個特性。

  • 再下面是函數 get_tcp_state_change,該函數會在內核調用 tcp_set_state 的時候調用。

通過內核的幾個參數,內核的結構體 socket,以及這個函數傳進來的一些 state,可以獲取當時 tcp 連接的狀態轉化情況,上圖函數的第一個參數 ctx 實際上是寄存器,后面是要介入函數的兩個參數。這里會把一些 tcp 的狀態存起來,使用 perf_submit 將這些狀態更新到 perf ring buffer 中,就可以在用戶態把 perf ring buffer 東西給讀出來,這就是 tcp 的一些狀態變化。

上圖是 python 代碼。

  • 首先把 C 代碼讀進來,通過調用 bpf 初始化,將代碼編譯成 eBPF 字節碼,載入到 eBPF 虛擬機中運行。

  • 下面是 attach_kprobe,就是在內核調用 tcp,event 是指內核在調用 tcp_set_state 的時候,fn_name 是指內核在調用 tcp_set_state 時會執行 get_tcp_state_change 函數,就是前面 C 代碼中的函數。

  • 打開 perf ring buffer,即后面調用的 bpf[“state_events”].open_perf_buffer,里面的參數是一個 Callback 函數,在ring buffer 有數據的時候就會調用一次 print_state,也就是說在 C 代碼中調用 perf_sumbit 時候就可以調用一次 print_tcpstats 函數,並會輸出存入的數據。

  • 最下面調用了 perf_buffer_poll的功能,只會在 ring buffer 有消息時被喚醒,再調用 Callback 函數,這樣就不會無謂地浪費 CPU。

利用 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。

eBPF 在又拍雲的發展

  • 完善 cdn 系統監控體系

  • 強化 cdn 業務鏈路 traceing,提高服務水平,提供更多的性能分析的途徑

  • 解決 cdn 服務中遇到的某些難以解決的問題 注:目前通過 systemtap 可以解決

  • 將 XDP 引入又拍雲邊緣機器,給予防范 DDoS 攻擊提供幫助

  • 替換 tcpdump 工具,加快抓包效率,減少抓包時對系統性能的影響

推薦閱讀

聊聊 HTTP 常見的請求方式

有贊統一接入層架構演進


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM