絕對乾貨!初學者也能看懂的DPDK解析

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html

本文由Willko發表於雲+社區專欄linux

1、網絡IO的處境和趨勢git

從咱們用戶的使用就能夠感覺到網速一直在提高,而網絡技術的發展也從1GE/10GE/25GE/40GE/100GE的演變,從中能夠得出單機的網絡IO能力必須跟上時代的發展。程序員

1. 傳統的電信領域github

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

2. 雲的發展api

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

3. 單機性能的飆升服務器

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

2、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。

3、DPDK的基本原理

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

Linux社區也提供了旁路機制Netmap,官方數據10G網卡1400萬PPS,可是Netmap沒普遍使用。其緣由有幾個:

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

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

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

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

DPDK旁路原理:

img
圖片引自Jingjing Wu的文檔《Flow Bifurcation on Intel® Ethernet Controller X710/XL710》

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

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

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

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

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

4、DPDK的基石UIO

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

UIO原理:

img

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

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

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

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

5、DPDK核心優化:PMD

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

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

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

img

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

Interrupt DPDK:

img
圖片引自David Su/Yunhong Jiang/Wei Wang的文檔《Towards Low Latency Interrupt Mode DPDK》

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

6、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。

2. SNA(Shared-nothing Architecture)

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

3. SIMD(Single Instruction Multiple Data)

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

SIMD在遊戲後臺比較常見,可是其餘業務若是有相似批量處理的場景,要提升性能,也可看看可否知足。

4. 不使用慢速API

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

在x86-64下使用RDTSC指令,直接從寄存器讀取,須要輸入2個參數,比較常見的實現:

static inline uint64_t
rte_rdtsc(void)
{
      uint32_t lo, hi;

      __asm__ __volatile__ (
                 "rdtsc" : "=a"(lo), "=d"(hi)
                 );

      return ((unsigned long long)lo) | (((unsigned long long)hi) << 32);
}
複製代碼

這麼寫邏輯沒錯,可是還不夠極致,還涉及到2次位運算才能獲得結果,咱們看看DPDK是怎麼實現:

static inline uint64_t
rte_rdtsc(void)
{
	union {
		uint64_t tsc_64;
		struct {
			uint32_t lo_32;
			uint32_t hi_32;
		};
	} tsc;

	asm volatile("rdtsc" :
		     "=a" (tsc.lo_32),
		     "=d" (tsc.hi_32));
	return tsc.tsc_64;
}
複製代碼

巧妙的利用C的union共享內存,直接賦值,減小了沒必要要的運算。可是使用tsc有些問題須要面對和解決

  1. CPU親和性,解決多核跳動不精確的問題

  2. 內存屏障,解決亂序執行不精確的問題

  3. 禁止降頻和禁止Intel Turbo Boost,固定CPU頻率,解決頻率變化帶來的失準問題

5. 編譯執行優化

  1. 分支預測

現代CPU經過pipelinesuperscalar提升並行處理能力,爲了進一步發揮並行能力會作分支預測,提高CPU的並行能力。遇到分支時判斷可能進入哪一個分支,提早處理該分支的代碼,預先作指令讀取編碼讀取寄存器等,預測失敗則預處理所有丟棄。咱們開發業務有時候會很是清楚這個分支是true仍是false,那就能夠經過人工干預生成更緊湊的代碼提示CPU分支預測成功率。

#pragma once

#if !__GLIBC_PREREQ(2, 3)
# if !define __builtin_expect
# define __builtin_expect(x, expected_value) (x)
# endif
#endif

#if !defined(likely)
#define likely(x) (__builtin_expect(!!(x), 1))
#endif

#if !defined(unlikely)
#define unlikely(x) (__builtin_expect(!!(x), 0))
#endif
複製代碼
  1. CPU Cache預取

Cache Miss的代價很是高,回內存讀須要65納秒,能夠將即將訪問的數據主動推送的CPU Cache進行優化。比較典型的場景是鏈表的遍歷,鏈表的下一節點都是隨機內存地址,因此CPU確定是沒法自動預加載的。可是咱們在處理本節點時,能夠經過CPU指令將下一個節點推送到Cache裏。

API文檔:doc.dpdk.org/api/rte__pr…

static inline void rte_prefetch0(const volatile void *p)
{
	asm volatile ("prefetcht0 %[p]" : : [p] "m" (*(const volatile char *)p));
}
複製代碼
#if !defined(prefetch)
#define prefetch(x) __builtin_prefetch(x)
#endif
複製代碼

…等等

  1. 內存對齊

內存對齊有2個好處:

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

#define __rte_packed __attribute__((__packed__))
複製代碼

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

#ifndef CACHE_LINE_SIZE
#define CACHE_LINE_SIZE 64
#endif

#ifndef aligined
#define aligined(a) __attribute__((__aligned__(a)))
#endif
複製代碼
  1. 常量優化

常量相關的運算的編譯階段完成。好比C++11引入了constexp,好比可使用GCC的__builtin_constant_p來判斷值是否常量,而後對常量進行編譯時得出結果。舉例網絡序主機序轉換

#define rte_bswap32(x) ((uint32_t)(__builtin_constant_p(x) ? \
				   rte_constant_bswap32(x) :		\
				   rte_arch_bswap32(x)))
複製代碼

其中rte_constant_bswap32的實現

#define RTE_STATIC_BSWAP32(v) \
	((((uint32_t)(v) & UINT32_C(0x000000ff)) << 24) | \
	 (((uint32_t)(v) & UINT32_C(0x0000ff00)) <<  8) | \
	 (((uint32_t)(v) & UINT32_C(0x00ff0000)) >>  8) | \
	 (((uint32_t)(v) & UINT32_C(0xff000000)) >> 24))
複製代碼

5)使用CPU指令

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

static inline uint64_t rte_arch_bswap64(uint64_t _x)
{
	register uint64_t x = _x;
	asm volatile ("bswap %[x]"
		      : [x] "+r" (x)
		      );
	return x;
}
複製代碼

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

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

7、DPDK生態

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

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

騰訊雲開源的F-Stack也值得關注一下,開發更簡單,直接提供了POSIX接口。

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

咱們GBN Gateway項目須要支持L3/IP層接入作Wan網關,單機20GE,基於DPDK開發。

問答

如何檢查網絡鏈接?

相關閱讀

把報文再扔回內核,DPDK這樣作

用DPDK rte_ring實現多進程間通訊

低於0.01%的極致Crash率是怎麼作到的?

【每日課程推薦】新加坡南洋理工大學博士,帶你深度學習NLP技術

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區

相關文章
相關標籤/搜索