一文看懂 DPDK 技術

一、網絡 IO 的處境和趨勢

從我們用戶的使用就可以感受到網速一直在提升,而網絡技術的發展也從 1GE/10GE/25GE /40GE/100GE 的演變,從中可以得出單機的網絡 IO 能力必須跟上時代的發展。

  1. 傳統的電信領域

IP 層及以下,例如路由器、交換機、防火牆、基站等設備都是採用硬件解決方案。基於專用網絡處理器(NP),有基於 FPGA,更有基於 ASIC 的。但是基於硬件的劣勢非常明顯,發生 Bug 不易修復,不易調試維護,並且網絡技術一直在發展,例如 2G/3G/4G/5G 等移動技術的革新,這些屬於業務的邏輯基於硬件實現太痛苦,不能快速迭代。傳統領域面臨的挑戰是急需一套軟件架構的高性能網絡 IO 開發框架。

  1. 雲的發展

私有云的出現通過網絡功能虛擬化(NFV)共享硬件成爲趨勢,NFV 的定義是通過標準的服務器、標準交換機實現各種傳統的或新的網絡功能。急需一套基於常用系統和標準服務器的高性能網絡 IO 開發框架。

  1. 單機性能的飆升

網卡從 1G 到 100G 的發展,CPU 從單核到多核到多 CPU 的發展,服務器的單機能力通過橫行擴展達到新的高點。但是軟件開發卻無法跟上節奏,單機處理能力沒能和硬件門當戶對,如何開發出與時並進高吞吐量的服務,單機百萬千萬併發能力。即使有業務對 QPS 要求不高,主要是 CPU 密集型,但是現在大數據分析、人工智能等應用都需要在分佈式服務器之間傳輸大量數據完成作業。這點應該是我們互聯網後臺開發最應關注,也最關聯的。

二、Linux + x86 網絡 IO 瓶頸

在數年前曾經寫過《網卡工作原理及高併發下的調優》一文,描述了 Linux 的收發報文流程。根據經驗,在 C1(8 核)上跑應用每 1W 包處理需要消耗 1% 軟中斷 CPU,這意味着單機的上限是 100 萬 PPS(Packet Per Second)。從 TGW(Netfilter 版)的性能 100 萬 PPS,AliLVS 優化了也只到 150 萬 PPS,並且他們使用的服務器的配置還是比較好的。假設,我們要跑滿 10GE 網卡,每個包 64 字節,這就需要 2000 萬 PPS(注:以太網萬兆網卡速度上限是 1488 萬 PPS,因爲最小幀大小爲 84B《Bandwidth, Packets Per Second, and Other Network Performance Metrics》),100G 是 2 億 PPS,即每個包的處理耗時不能超過 50 納秒。而一次 Cache Miss,不管是 TLB、數據 Cache、指令 Cache 發生 Miss,回內存讀取大約 65 納秒,NUMA 體系下跨 Node 通訊大約 40 納秒。所以,即使不加上業務邏輯,即使純收發包都如此艱難。我們要控制 Cache 的命中率,我們要了解計算機體系結構,不能發生跨 Node 通訊。

從這些數據,我希望可以直接感受一下這裏的挑戰有多大,理想和現實,我們需要從中平衡。問題都有這些

  1. 傳統的收發報文方式都必須採用硬中斷來做通訊,每次硬中斷大約消耗 100 微秒,這還不算因爲終止上下文所帶來的 Cache Miss。

  2. 數據必須從內核態用戶態之間切換拷貝帶來大量 CPU 消耗,全局鎖競爭。

  3. 收發包都有系統調用的開銷。

  4. 內核工作在多核上,爲可全局一致,即使採用 Lock Free,也避免不了鎖總線、內存屏障帶來的性能損耗。

  5. 從網卡到業務進程,經過的路徑太長,有些其實未必要的,例如 netfilter 框架,這些都帶來一定的消耗,而且容易 Cache Miss。

三、DPDK 的基本原理

從前面的分析可以得知 IO 實現的方式、內核的瓶頸,以及數據流過內核存在不可控因素,這些都是在內核中實現,內核是導致瓶頸的原因所在,要解決問題需要繞過內核。所以主流解決方案都是旁路網卡 IO,繞過內核直接在用戶態收發包來解決內核的瓶頸。

Linux 社區也提供了旁路機制 Netmap,官方數據 10G 網卡 1400 萬 PPS,但是 Netmap 沒廣泛使用。其原因有幾個:

  1. Netmap 需要驅動的支持,即需要網卡廠商認可這個方案。

  2. Netmap 仍然依賴中斷通知機制,沒完全解決瓶頸。

  3. Netmap 更像是幾個系統調用,實現用戶態直接收發包,功能太過原始,沒形成依賴的網絡開發框架,社區不完善。

那麼,我們來看看發展了十幾年的 DPDK,從 Intel 主導開發,到華爲、思科、AWS 等大廠商的加入,核心玩家都在該圈子裏,擁有完善的社區,生態形成閉環。早期,主要是傳統電信領域 3 層以下的應用,如華爲、中國電信、中國移動都是其早期使用者,交換機、路由器、網關是主要應用場景。但是,隨着上層業務的需求以及 DPDK 的完善,在更高的應用也在逐步出現。

DPDK 旁路原理: 

左邊是原來的方式數據從 網卡 -> 驅動 -> 協議棧 -> Socket 接口 -> 業務

右邊是 DPDK 的方式,基於 UIO(Userspace I/O)旁路數據。數據從 網卡 -> DPDK 輪詢模式 -> DPDK 基礎庫 -> 業務

用戶態的好處是易用開發和維護,靈活性好。並且 Crash 也不影響內核運行,魯棒性強。

DPDK 支持的 CPU 體系架構:x86、ARM、PowerPC(PPC)

DPDK 支持的網卡列表:https://core.dpdk.org/supported/,我們主流使用 Intel 82599(光口)、Intel x540(電口)

四、DPDK 的基石 UIO

爲了讓驅動運行在用戶態,Linux 提供 UIO 機制。使用 UIO 可以通過 read 感知中斷,通過 mmap 實現和網卡的通訊。

UIO 原理: 

要開發用戶態驅動有幾個步驟:

  1. 開發運行在內核的 UIO 模塊,因爲硬中斷只能在內核處理

  2. 通過 / dev/uioX 讀取中斷

  3. 通過 mmap 和外設共享內存

五、DPDK 核心優化:PMD

DPDK 的 UIO 驅動屏蔽了硬件發出中斷,然後在用戶態採用主動輪詢的方式,這種模式被稱爲 PMD(Poll Mode Driver)。

UIO 旁路了內核,主動輪詢去掉硬中斷,DPDK 從而可以在用戶態做收發包處理。帶來 Zero Copy、無系統調用的好處,同步處理減少上下文切換帶來的 Cache Miss。

運行在 PMD 的 Core 會處於用戶態 CPU100% 的狀態 

網絡空閒時 CPU 長期空轉,會帶來能耗問題。所以,DPDK 推出 Interrupt DPDK 模式。

Interrupt DPDK: 

它的原理和 NAPI 很像,就是沒包可處理時進入睡眠,改爲中斷通知。並且可以和其他進程共享同個 CPU Core,但是 DPDK 進程會有更高調度優先級。

六、DPDK 的高性能代碼實現

  1. 採用 HugePage 減少 TLB Miss

默認下 Linux 採用 4KB 爲一頁,頁越小內存越大,頁表的開銷越大,頁表的內存佔用也越大。CPU 有 TLB(Translation Lookaside Buffer)成本高所以一般就只能存放幾百到上千個頁表項。如果進程要使用 64G 內存,則 64G/4KB=16000000(一千六百萬)頁,每頁在頁表項中佔用 16000000 * 4B=62MB。如果用 HugePage 採用 2MB 作爲一頁,只需 64G/2MB=2000,數量不在同個級別。

而 DPDK 採用 HugePage,在 x86-64 下支持 2MB、1GB 的頁大小,幾何級的降低了頁表項的大小,從而減少 TLB-Miss。並提供了內存池(Mempool)、MBuf、無鎖環(Ring)、Bitmap 等基礎庫。根據我們的實踐,在數據平面(Data Plane)頻繁的內存分配釋放,必須使用內存池,不能直接使用 rte_malloc,DPDK 的內存分配實現非常簡陋,不如 ptmalloc。

  1. SNA(Shared-nothing Architecture)

軟件架構去中心化,儘量避免全局共享,帶來全局競爭,失去橫向擴展的能力。NUMA 體系下不跨 Node 遠程使用內存。

  1. SIMD(Single Instruction Multiple Data)

從最早的 mmx/sse 到最新的 avx2,SIMD 的能力一直在增強。DPDK 採用批量同時處理多個包,再用向量編程,一個週期內對所有包進行處理。比如,memcpy 就使用 SIMD 來提高速度。

SIMD 在遊戲後臺比較常見,但是其他業務如果有類似批量處理的場景,要提高性能,也可看看能否滿足。

  1. 不使用慢速 API

這裏需要重新定義一下慢速 API,比如說 gettimeofday,雖然在 64 位下通過 vDSO 已經不需要陷入內核態,只是一個純內存訪問,每秒也能達到幾千萬的級別。但是,不要忘記了我們在 10GE 下,每秒的處理能力就要達到幾千萬。所以即使是 gettimeofday 也屬於慢速 API。DPDK 提供 Cycles 接口,例如 rte_get_tsc_cycles 接口,基於 HPET 或 TSC 實現。

  1. 編譯執行優化

  1. 分支預測

現代 CPU 通過 pipeline、superscalar 提高並行處理能力,爲了進一步發揮並行能力會做分支預測,提升 CPU 的並行能力。遇到分支時判斷可能進入哪個分支,提前處理該分支的代碼,預先做指令讀取編碼讀取寄存器等,預測失敗則預處理全部丟棄。我們開發業務有時候會非常清楚這個分支是 true 還是 false,那就可以通過人工干預生成更緊湊的代碼提示 CPU 分支預測成功率。

  1. CPU Cache 預取

Cache Miss 的代價非常高,回內存讀需要 65 納秒,可以將即將訪問的數據主動推送的 CPU Cache 進行優化。比較典型的場景是鏈表的遍歷,鏈表的下一節點都是隨機內存地址,所以 CPU 肯定是無法自動預加載的。但是我們在處理本節點時,可以通過 CPU 指令將下一個節點推送到 Cache 裏。

API 文檔:https://doc.dpdk.org/api/rte__prefetch_8h.html

  1. 內存對齊

內存對齊有 2 個好處:

l 避免結構體成員跨 Cache Line,需 2 次讀取才能合併到寄存器中,降低性能。結構體成員需從大到小排序和以及強制對齊。參考《Data alignment: Straighten up and fly right》

l 多線程場景下寫產生 False sharing,造成 Cache Miss,結構體按 Cache Line 對齊。

  1. 常量優化

常量相關的運算的編譯階段完成。比如 C++11 引入了 constexp,比如可以使用 GCC 的__builtin_constant_p 來判斷值是否常量,然後對常量進行編譯時得出結果。

  1. 使用 CPU 指令

現代 CPU 提供很多指令可直接完成常見功能,比如大小端轉換,x86 有 bswap 指令直接支持了。

這個實現,也是 GLIBC 的實現,先常量優化、CPU 指令優化、最後才用裸代碼實現。畢竟都是頂端程序員,對語言、編譯器,對實現的追求不一樣,所以造輪子前一定要先了解好輪子。

Google 開源的 cpu_features 可以獲取當前 CPU 支持什麼特性,從而對特定 CPU 進行執行優化。高性能編程永無止境,對硬件、內核、編譯器、開發語言的理解要深入且與時俱進。

七、DPDK 生態

對我們互聯網後臺開發來說 DPDK 框架本身提供的能力還是比較裸的,比如要使用 DPDK 就必須實現 ARP、IP 層這些基礎功能,有一定上手難度。如果要更高層的業務使用,還需要用戶態的傳輸協議支持。不建議直接使用 DPDK。

目前生態完善,社區強大(一線大廠支持)的應用層開發項目是 FD.io(The Fast Data Project),有思科開源支持的 VPP,比較完善的協議支持,ARP、VLAN、Multipath、IPv4/v6、MPLS 等。用戶態傳輸協議 UDP/TCP 有 TLDK。從項目定位到社區支持力度算比較靠譜的框架。

Seastar 也很強大和靈活,內核態和 DPDK 都隨意切換,也有自己的傳輸協議 Seastar Native TCP/IP Stack 支持,但是目前還未看到有大型項目在使用 Seastar,可能需要填的坑比較多。

更多內容參考 “中國電信 DPDK 技術白皮書 v1.0、DPDK 基礎—認識 DPDK 技術、DPDK 架構高清版、DPDK 編程指南(中文版)”。

原文鏈接:

https://cloud.tencent.com/developer/article/1198333

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