基於 eBPF 的軟件網絡功能實現

1、基於 eBPF 實現網絡功能的優勢

目前,基於 eBPF 實現的網絡功能已經被很多公司應用於生產環境中,成爲雲環境下基礎設施的重要組成部分。例如 Meta 的負載均衡器 Katran, Google Cloud 目前使用基於 eBPF 的網絡數據平面等。在學術研究和開源社區中,eBPF 也被廣泛地用來實現網絡功能。典型的例子有,學術研究: BMC (NSDI 2021), SPRIGHT (SIGCOMM 2023), Morpheus (ASPLOS 2022), Electrode (NSDI 2023), DINT (NSDI 2024)  等;開源項目: CIlium, PolyCube, Katran 等。因此基於 eBPF 實現網絡功能逐漸成爲一種趨勢。

這是因爲 eBPF 擁有以下優勢:

  1. eBPF 作爲一種起源於內核的技術,能夠很好地集成到依賴於內核的雲生態中。例如,根據 OvS 團隊的論文,在主機內部容器通信中,內核 XDP 數據路徑的性能優於 OvS 中的 DPDK 數據路徑。

  2. 相比於 DPDK 等方案,eBPF 實現了更好的性能和 CPU 利用率、安全、隔離性、運維成本之間的平衡。例如,eBPF 支持高性能的數據包處理而不會使 CPU 飽和,使得網絡功能和非網絡功能應用能夠在同一設備上運行。

  3. eBPF 允許動態加載用戶代碼然後安全地在內核中執行,無需修改內核源代碼,從而提高了可維護性和靈活性,並加快了網絡功能的開發和部署。

2、eBPF 實現網絡功能面臨的技術挑戰

2.1 用 eBPF 無法實現特定的網絡功能

因爲 eBPF 對非連續內存的使用施加了嚴格限制,阻礙了部分網絡功能核心組件的實現。例如基於跳錶的 key-value store 和基於紅黑樹的優先級隊列等。使用非連續內存意味着 eBPF 需要支持將可變數量的動態內存持久化。儘管最近的 Linux 內核(版本 6.1 及以上)支持分配動態內存並將其持久化到 BPF MAP 中,但驗證器強制規定了 BPF MAP 只能持久化固定數量的動態內存。因此,由於缺乏對可變動態內存的支持,現有的 eBPF 無法使用非連續內存。

例如,以下代碼展示了 eBPF 目前支持動態內存,但無法支持可變數量的動態內存。

2.2 用 eBPF 實現網絡功能性能次優

首先,eBPF 的 RISC 指令集缺乏對特定指令的支持,包括 SIMD 指令和 bitscan 指令 (FFS 等),導致性能下降。例如,不支持 SIMD 導致了 eBPF 在實現網絡功能時無法採用並行計算、並行查找等在網絡功能中被使用的加速方式。在 sketch 等網絡功能中,這會導致 49.2% 的性能下降。其次,eBPF 幫助函數 bpf_get_prandom_u32 對於網絡功能來說性能開銷太大。如果每一個包都調用一次 bpf_get_prandom_u32 導致 NitroSketch 46.6% 的性能下降。

2.3 現有的解決方案存在的缺陷

爲了解決這兩個技術挑戰,可以考慮兩種解決方案。

第一種解決方案是增強 eBPF 的整體架構,例如擴展 eBPF 的指令集、增強驗證器、引入新的運行時和語言級別的安全機制,以及將驗證過程從內核解耦到用戶空間。

然而,由於對內核的修改過於激進,實用價值較低,不易於部署和推廣。例如,擴展 eBPF 指令集需要對內核代碼庫中架構特定的 JIT 編譯器進行修改,目前涵蓋多達 14 種硬件架構。此外,擴展指令集要求對驗證器的代碼進行修改,因爲驗證器針對 eBPF 指令進行驗證。

但是修改驗證器可能會引入新的 bug 和安全問題。儘管重新設計 eBPF 的安全和編程架構在理論上是可行的,這種方案目前難以被直接部署,並且可能對以後的 eBPF 網絡功能產生負面影響。

第二種解決方案是將所有功能無法實現的和性能下降的網絡功能實現爲內核模塊(通過 kptr 和 kfunc 技術)或者集成到內核中 (實現爲新的幫助函數和 BPF MAP)。

然而,將所有網絡功能集成到內核中將對內核造成巨大的改動,難以被內核社區接受。而根據需求集成單個網絡功能,可能會由於需求變化而導致頻繁的內核模塊更換,進而導致內核不穩定。

鑑於網絡社區的快速發展,這種 "一個內核模塊實現一種網絡功能" 的方法可能會使內核變得相當不穩定。

3、基於標準庫的優化 eBPF 網絡功能技術方案

3.1 基於網絡功能中的通用設計模式

網絡功能中存在一些通用的設計模式,總結如下:

  1. 使用 bit scan 指令,例如 FFS (find the first bit), popcnt 指令等,實現快速檢索。這種設計會被用在高性能的優先級隊列的實現上,例如通過 FFS 快速定位第一個存放元素的 bucket 。

  2. 同時計算多個 hash 函數。很多實現網絡測量的網絡功能,會使用一些基於概率和統計的數據結構,例如 sketch 和 bloom filter。同時計算多個 hash 函數來降低衝突概率。

  3. 使用基礎的數據結構。例如,top-k heap, 桶鏈表等。

  4. 使用隨機數。爲了提升性能,部分網絡功能會根據概率執行特定的操作,例如一些 Heavy Hitter。

  5. 使用非連續內存。例如使用跳錶和紅黑樹等。

  6. 將數據保存在連續內存中。例如,網絡功能中的一些高性能的 hash 表,例如 DPDK 中的 cuckoo hash,將多個 key 保存在一塊連續 bucket 中來降低 hash 衝突。

3.2 eBPF 網絡標準庫的設計和實現

爲了在不修改內核的前提下,解決上述的技術挑戰,我們設計並實現一個可供 eBPF 調用的網絡功能標準庫 eNetSTL。

eNetSTL 將上述的通用的模式抽象並實現爲一系列高性能低開銷的 API。在解決問題的同時,避免代碼過度膨脹。eNetSTL 基於 eBPF 的 kernel function (kfunc) 和 kernel pointer (kptr) 技術實現,並將 API 實現在內核模塊中,從而避免了內核的修改。

目前 eNetSTL 的設計除了使用 kfunc 和 kptr 接口外,其他部分是 self-contain 的。因此能保持較好的內核版本的兼容性。eNetSTL 包含的內容如下圖所示:

具體來說,eNetSTL 包含以下內容:

  1. Memory wrapper:  支持在 eBPF 中使用非連續內存的同時,不破壞 eBPF 提供的安全保證。

  2. 算法:包括位運算、基於 SIMD 的並行 hash 計算和並行比較算法。

  3. 數據結構:list bucket 數據結構,支持 GEO (幾何隨機數) 分佈的隨機數池。

其中 Memory wrapper 的實現充分利用了 kfunc 和 kptr 技術。其主要設計包括:

  1. 通過用一個 proxy kptr 來管理所有新分配的 node kptr,避免 BPF MAP 中只能保存靜態數量的 kptr。

  2. 由 eNetSTL 管理所有的底層指針,通過 kfunc 實現節點到節點的指針路由,通過給 kfunc 增加 KF_ACQUIRE tag 來安全獲取下一個節點的指針,並在 eBPF 中直接訪問該指針,例如 a->b。

下面是 Memory wrapper 的部分 API:

4、eNetSTL 使用技術實踐

4.1 基於 eNetSTL 實現跳錶

通過 Memory wrapper API,直接在 eBPF 裏使用非連續內存。我們用簡化版本的單鏈表來展示使用非連續內存 (跳錶的實現類似):

性能測試結果 (40G 網卡 單核性能) 如下圖所示 (紅色折線代表用內核模塊實現,黃色折線代表用 eNetSTL 實現):

我們驗證了跳錶的查找性能和插入性能,可以看到使用 eNetSTL 在使能了原本無法直接實現的跳錶的同時,其性能損耗在 10% 以下。

4.2 基於 eNetSTL 實現 sketch

sketch 是一種在網絡測量領域常用的網絡功能,其核心設計是使用多個 hash 函數將同一條流的數據包映射到多個 counter 上。我們使用 eNetSTL 的 API 來加速多個函數的計算,典型的 Count-min sketch 用 eNetSTL 實現代碼如下:

性能測試結果 (40G 網卡 單核性能) 如下圖所示 (紅色代表用內核模塊實現,黃色代表用 eNetSTL 實現,藍色表示用純 eBPF 實現):

實驗結果顯示,與 eBPF 相比,基於 eNetSTL 的實現平均性能提升了 47.9%。特別是,隨着哈希函數數量的增加,這種提升變得更加顯著,使用 8 個哈希函數時達到了 70.9% 的峯值。這是由於隨着哈希函數數量的增加,SIMD 指令能帶來更多的優化效果。並且調用 eNetSTL 幾乎不會帶來性能損失。

4.3 基於 eNetSTL 優化 Cuckoo Switch 中的 hash 性能

Cuckoo Switch 中使用了 Blocked Cuckoo hash 這一核心數據結構。相比於原始的 Cuckoo hash, Blocked Cuckoo hash 爲了降低 hash 的衝突率,在一個 bucket 中同時保存 16 個 hash 指紋。我們參考 DPDK 的實現,使用 eNetSTL 提供的 hw_hash_crc(用硬件指令生成 crc 來代替 hash 計算)和 基於 SIMD 的並行比較算bpf__find_mask_u16分別優化 hash 的計算、hash 指紋的比較、和 full-key 的比較。

下面是一個簡化後的例子:

性能測試結果 (40G 網卡 單核性能) 如下圖所示 (紅色的折線代表用內核模塊實現,黃色的折線代表用 eNetSTL 實現,藍色的折線表示用純 eBPF 實現):

使用了 eNetSTL 的方案與純 eBPF 相比,平均性能提升 27.4%,並且隨着負載的增加,性能提升更加明顯,在滿負載時達到 33.08%。這是因爲,隨着負載增加,單個條目中的平均比較次數也增加。基於 SIMD 的並行比較優化效果變得更好。在低負載場景下,優化主要體現在使用 hw_hash_crc 替代基於軟件的哈希計算和 SIMD 優化的 full key 比較。與內核相比,採用 eNetSTL 的方案平均性能損失約爲 4.30%。

如果你有 eBPF 及 linux 相關問題,請聯繫小編微信號 wenamao,邀請加入 酷玩 BPF 學習交流羣。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ha4lcCgA2D2A3iDEPK-ZFQ