eBPF 架構的優勢

本文假設讀者已瞭解以下內容:

  1. 瞭解 BPF/eBPF 是什麼,瞭解 BPF 的演變歷史,可參考引用 7

  2. 瞭解程序的編譯與執行流程,虛擬機工作原理

  3. 大致瞭解 Android 系統架構以及開發流程

  4. 本文是基於嵌入式 Linux 的開發角度闡述 eBPF 的應用,對負載情況及需求不一樣的其他應用領域(如,雲計劃,後端服務器)可能不太適用,請讀者注意區分

eBPF 功能的概括描述

概括講,eBPF 是一套調試框架,它允許當用戶關心的事件發生時允許直接運行用戶編寫的代碼。是不是感覺很熟悉?在 Linux 中早已存在的 Ftrace,SystemTap,甚至 Java 中 ASM 技術都有類似的功能。在內核空間中執行到目標函數入口時執行用戶事先定義好的代碼,可以處理目標函數的入參數據。當目標函數執行完成後返回時也可以執行用戶事先定義好的代碼,對函數返回值做處理。

注意:eBPF 可以通過 USDT(User Statically-Defined Tracing)技術對用戶空間程序做同樣的效果,但因爲本質上都是執行在內核空間所以不做過多區分。

對目標函數的處理流程,相比其他 Profile 工具而言沒有特殊之處,抽象此流程爲如下:

抽象流程

但是在此執行流程基礎上,eBPF 的特殊之處在於:

  1. 用戶定義的回調函數及數據處理可以直接運行在內核空間

  2. 用戶的回調函數實現是動態執行的,也就是不參與內核或系統的編譯,像 Shell 腳本一樣即寫即可得

  3. 大一統了 Linux 目前主流的調試框架、如:Ftrace,Kprobe,Perf,USDT(User Statically-Defined Tracing)等

這三個特殊之處可了不得,它使得 eBPF 相比之前調試工具:

  1. 目標函數數據的處理都執行在內核空間,這就省去了大量的用戶與內核空間上的數據拷貝,系統調用,上下文切換等負載。性能強悍到甚至可以直接處理網絡請求包(BPF 是 Berkeley Packet Filters 的縮寫而 e 代表 Extended),從他的名字就能猜到它就是爲此而生

  2. 因爲用戶回調函數是動態執行,大大提高了應用上的靈活性。遇到手頭問題的時候,想查哪個狀態隨手一敲就能得到結果

  3. 之前學習各類 Linux 上的調試工具時候總覺得東西好多而且好亂。eBPF 使用了一套 API 統一了各種底層調試機制,套用互聯網應用開發常說的話,”讓程序員更專注在具體業務上”

要是我的話該怎麼設計?

與 eBPF 類似的調試系統就是 Dtrace,它主要應用在 Solaris,MacOS 上。Linux 社區上也有多個分支版本,但始終沒有合併到主幹上。第一次初步瞭解 eBPF 的時候我也設想過如果要我設計類 Dtrace 的系統,應該怎麼設計。假設我們將過程分爲三步:

  1. 事件發生時通知到調試系統

  2. 調試系統接到通知後在內核層執行用戶註冊的回調函數

  3. 用戶代碼不需要編譯,可直接運行

1. 事件發生時通知到調試系統

首先想到就是複用 ftrace 接口,將原先訪問/sys/kernel/debug/tracing 操作接口轉換成更爲方便的 API,可供用戶層的回調函數使用。後來想到既然都是運行在內核層,不如把範圍擴大一些。只要是帶符號的內核函數,都可以當做目標函數供用戶使用。對目標函數的尋址方法沿用類似 Dtrace 方案,也就是約定好命名規則。

2. 調試系統接到通知後在內核層執行用戶註冊的回調函數

既然是可執行代碼,那就需要編譯。編譯器將用戶的回調函數直接編譯成目標 CPU 架構的 ELF 文件後將 ELF 文件寫入到內核的類似 ioctl 之類的系統調用上。這套理論上可行,但實際上問題很多。有一個難搞定的問題是用戶函數可以通過指針操作,訪問到內核的任何數據接口,這在安全性上是不可接受的。後來又看到 Lua 引擎運行在內核上的項目,是不是也可以參考此類架構,在 Kernel 中安插一個解釋腳本引擎,如 Lua,甚至簡單版本的 Java,Python 之類。這個方案的不足之處在於,這類語言的編譯器比較龐大,對內核來說一是代碼不夠統一,二是過於複雜容易造成安全漏洞。

3. 用戶代碼不需編譯即可直接運行

這條與第 2 條是本質上是同一個,爲了支持動態執行,要麼寫入到內核之前編譯好目標代碼,要麼由內核來直接解釋執行

社區的實現方案

社區的實現相比我的初步設想優點不一樣,而且更好更強大。

社區的實現方案
  1. 首先,它統一了幾乎所有內核現有的調試框架,ftrace,kproble 都可以使用,甚至 perf 事件,用戶空間程序自定義事件也都沒問題。編程接口上使用簡單的規則定義了具體要監聽哪種調試框架下的哪種事件,使用起來非常方便

  2. 然後參照 Java 語言虛擬機方案,如下

    1. 先對 Java 語言通過編譯器編譯成優化好的 Bytecode

    2. 虛擬機對輸入進來的 Bytecode 做例行檢查後以解釋執行或 JIT 執行

  3. 社區的方案是將 Java 語言替換成 BPF 語言,Java 編譯器替換成 LLVM 編譯器,它生成出來的文件稱爲 BPF Bytecode

  4. 使用者把生成出來的 Bytecode 通過一個叫 bpf()系統調用傳遞給內核,由內核虛擬機處理

  5. 內核沒有沿用任何已有的虛擬機,而是實現了一套簡單,但是又能保證安全的虛擬機來執行 BPF bytecode。它既不能大量的循環操作,也不能訪問指定範圍外的內核數據。這麼做是爲了保證內核安全以及回調程序的性能。因爲它是執行在內核上下文,如果隨便來個 for(;;) {}豈不是分分鐘把系統搞掛?這是絕對不允許的。格外需要說明的是 BPF 程序實際上可以執行循環語句,但數量有限

有意思的 eBPF 工具棧

經過上面討論可知,我們需要用戶空間運行的 LLVM 編譯器,也需要跟內核打交道的處理程序(調用 bpf()及相關命令),又要把 eBPF 返回的數據展示給用戶。整個流程涉及的點比較多,可以手動編寫代碼來完成也可以使用社區正在開發的開源項目,BPFtrace 及 BCC

如果想要了解 eBPF 整個流程的話,從簡單的 demo 代碼開始入手確是個好選擇。請參考引用 3 項目,學習下最基礎的 eBPF 流程是怎樣的。BPFtrace,BCC 其實都是這個 demo 程序的高階版本,他們存在的目的無非是再封裝一層,屏蔽了大量繁瑣的細節之後讓”讓程序員更專注在具體業務上”

BPFTrace

來幾段所謂 One liner 的程序觀賞一下(項目參考引用 4)

# bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
Attaching 320 probes...

...
@[tracepoint:syscalls:sys_enter_access]: 3291
@[tracepoint:syscalls:sys_enter_close]: 3897
@[tracepoint:syscalls:sys_enter_newstat]: 4268
@[tracepoint:syscalls:sys_enter_open]: 4609
@[tracepoint:syscalls:sys_enter_mmap]: 4781
# bpftrace -e 'kprobe:do_sys_open { printf("%s: %s\n", comm, str(arg1)) }'
Attaching 1 probe...
git: .git/objects/da
git: .git/objects/pack
git: /etc/localtime
systemd-journal: /var/log/journal/72d0774c88dc4943ae3d34ac356125dd
DNS Res~ver #15: /etc/hosts

是不是特別簡單優美,即使沒學過 BPF 語法一看就能懂。具體細節請參考 BPFtrace 項目文檔,文檔寫的很全很詳細。BPFTrace 設計意圖在於提供簡單的編程方式實現對系統的調試。BPFTrace 可以這麼簡潔的原因是他已經封裝好了常用的函數以及程序行爲,簡單的代價是功能上有所割捨。適用於快速概念驗證,調試探索階段使用,更復雜功能還要看 BCC

BCC(BPF Compiler Collection)

# ./bitehist.py
Tracing... Hit Ctrl-C to end.

kbytes : count distribution
0 -> 1 : 3 | |
2 -> 3 : 0 | |
4 -> 7 : 211 |********** |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 800 |**************************************|

BCC 的功能基本上 BPFTrace 類似,但是他呈現的方式不太一樣(項目參考引用 5),具體表現爲

  1. BCC 中的工具雖然提供的功能跟 BPFTrace 一樣,但是他能做更多的事情。不像 BPFTrace 簡單一行代碼,它是用 Python 代碼實現的命令,在此程序中加載一個叫 BCC 的 python 程序包,在這個庫中實現了對 eBPF 的所有封裝。所有它可以利用 Python 以及龐大的工具庫實現非常豐富的監控流程控制與結果展示,如:畫圖,統計,甚至對輸出數據做機器學習訓練

  2. 在 Python 程序中需要以 BPF 語言實現回調函數,所以對 eBPF 行爲的控制更加細緻及深入,具體參考 Reference Guide(參考引用 6)

  3. 對 eBPF 返回的結果做更多的處理操作

總之如作者所說,簡單需求使用 BPFTrace,複雜需求使用 BCC

eBPF 在 Android 系統優化上的暢想

BCC 項目中提供了大量的調試小程序,在我看來這些其實是對歷史性能調試工具的重新實現,是以更優雅的方式實現。eBPF 更大的魅力在於他的高性能跟靈活性。靈活性體現在只要是編譯好的 bytecode 內核可以直接執行,這在線上固件的調試上帶來了極大的便利性

強悍的性能

實現直接對用戶版本的固件進行性能迴歸測試。之前爲了達到同樣的效果,要麼直接對內核修改,要麼開啓大量調試功能並執行特殊的性能分析程序。這會帶來的一個麻煩問題是需要維護單獨開發分支以免干擾用戶主幹分支,而且隨着時間的迭代,代碼基線會偏離於線上用戶版本,後續維護成本很大。所以在 CI/CD 的落地過程中,系統固件部分的測試是工程實踐難點。如果 eBPF 的高性能可以滿足可接受範圍內的性能損失,那完全可以直接拿線上版本做迴歸測試

虛擬機帶來的靈活性

實現針對線上用戶的單點下發,以瞭解更多的用戶實際使用的系統負載情況。瞭解真實用戶的系統負載對改進系統設計來說幫助很大,他就像一個燈塔指示了優化方向及系統瓶頸點。但痛點是在用戶版本固件中不敢開大量的調試功能用於收集系統運行狀態,因爲性能損耗非常大。在 eBPF 之後可以根據情況隨時修改腳本,並做灰度測試用於收集相關運行指標,會大大加快整個優化流程。Android 中的很多應用都熱衷於熱修復一樣,不打擾用戶的情況下修改線上程序的需求在系統開發時也是很有用的功能

寫在最後

  1. 隨着軟件技術的發展,調試機制也會不斷發展,方向是朝着更具有框架性,更易於使用,更高性能爲目標。不像之前那樣,爲了解決特定問題而設計,在完整性上提升了很多

  2. 業務負載變複雜的同時,手上工具也需要越來越靈活多變,用以面對不停變化的局面。先進生產力,有一部分是體現在工具的使用上,好的工具可避免沒必要的時間浪費,提升工作效率,將精力集中在更有挑戰的事情上

  3. 雖說軟件輪子複用是好工程的指標之一,但從 eBPF 實現來看它借鑑了其他項目的好想法但實現上完全是另起爐竈

  4. 軟件架構設計中,分層思路是萬古不變的原則

  5. BCC 在 android 上的應用實踐請參考本博的博文eBPF on Android(引用 8)

Reference

  1. http://www.brendangregg.com/perf.html

  2. https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#probes

  3. https://github.com/pratyushanand/learn-bpf

  4. https://github.com/iovisor/bpftrace

  5. https://github.com/iovisor/bcc

  6. https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md

  7. https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html ; eBPF 簡史

  8. http://www.caveman.work/2019/01/29/eBPF-on-Android/

相關文章