聊聊風口上的 eBPF

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

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

  • eBPF 是什麼
  • eBPF 能作什麼
  • 如何編寫 eBPF 程序

認識 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 的擴展,都擴展了些什麼呢?

  • 首先在功能上,不只僅侷限於網絡,它可以藉助 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 常見的請求方式

有贊統一接入層架構演進

相關文章
相關標籤/搜索