DPDK是一個技術棧,主要用于Intel架構的服務器領域,其主要目的就是提升x86標準服務器的轉發(fā)性能。因此,本文只重點介紹DPDK平臺部分技術在電信云中的最佳實踐。
1、為什么需要DPDK?
在IA上,網絡數(shù)據(jù)包處理遠早于DPDK而存在。從商業(yè)版的Windows到開源的Linux操作系統(tǒng),所有跨主機通信幾乎都會涉及網絡協(xié)議棧以及底層網卡驅動對于數(shù)據(jù)包的處理。然而,低速網絡數(shù)據(jù)轉發(fā)與高速網絡數(shù)據(jù)轉發(fā)的處理對系統(tǒng)的要求完全不一樣。以Linux為例,傳統(tǒng)網絡設備驅動包處理的動作可以概括如下:
數(shù)據(jù)包到達網卡設備。
網卡設備依據(jù)配置進行DMA操作。
網卡發(fā)送中斷,喚醒處理器。
驅動軟件填充讀寫緩沖區(qū)數(shù)據(jù)結構。
數(shù)據(jù)報文達到內核協(xié)議棧,進行高層處理。
如果最終應用在用戶態(tài),數(shù)據(jù)從內核搬移到用戶態(tài)。
如果最終應用在內核態(tài),在內核繼續(xù)進行。
隨著網絡接口帶寬從千兆向萬兆邁進,原先每個報文就會觸發(fā)一個中斷,中斷帶來的開銷變得突出,大量數(shù)據(jù)到來會觸發(fā)頻繁的中斷開銷,導致系統(tǒng)無法承受。
在網絡包高性能轉發(fā)技術領域,有兩個著名的技術框架NAPI和Netmap。NAPI策略用于高吞吐的場景,其策略是系統(tǒng)被中斷喚醒后,盡量使用輪詢的方式一次處理多個數(shù)據(jù)包,直到網絡再次空閑重新轉入中斷等待,其目的就是解決數(shù)據(jù)包在轉發(fā)過程過程中頻繁中斷引入的大量系統(tǒng)開銷。Netmap就是采用共享數(shù)據(jù)包池的方式,減少內核到用戶空間的包復制,從而解決大多數(shù)場景下需要把包從內核的緩沖區(qū)復制到用戶緩沖區(qū)引入大量系統(tǒng)開銷問題。
NAPI與Netmap兩方面的努力其實已經明顯改善了傳統(tǒng)Linux系統(tǒng)上的包處理能力,但是,Linux作為分時操作系統(tǒng),要將CPU的執(zhí)行時間合理地調度給需要運行的任務。相對于公平分時,不可避免的就是適時調度。早些年CPU核數(shù)比較少,為了每個任務都得到響應處理,進行充分分時,用效率換響應,是一個理想的策略?,F(xiàn)今CPU核數(shù)越來越多,性能越來越強,為了追求極端的高性能高效率,分時就不一定總是上佳的策略。以Netmap為例,即便其減少了內核到用戶空間的內存復制,但內核驅動的收發(fā)包處理和用戶態(tài)線程依舊由操作系統(tǒng)調度執(zhí)行,除去任務切換本身的開銷,由切換導致的后續(xù)cache替換(不同任務內存熱點不同),對性能也會產生負面的影響。為此,Intel針對IA架構的這些問題,就提出了DPDK技術棧的架構,其根本目的就是盡量采用用戶態(tài)驅動能力來替代內核態(tài)驅動,從而減少內核態(tài)的開銷,提升轉發(fā)性能。
2、鳥瞰DPDK
什么是DPDK?在《DPDK深入淺出》一書中,有以下一段描述:
針對不同的對象,其定義并不相同。對于普通用戶來說,它可能是一個性能出色的包數(shù)據(jù)處理加速軟件庫;對于開發(fā)者來說,它可能是一個實踐包處理新想法的創(chuàng)新工場;對于性能調優(yōu)者來說,它可能又是一個絕佳的成果分享平臺。當下火熱的網絡功能虛擬化,則將DPDK放在一個重要的基石位置。
DPDK最初的動機很簡單,就是為了證明IA多核處理器能夠支撐高性能數(shù)據(jù)包處理。隨著早期目標的達成和更多通用處理器體系的加入,DPDK逐漸成為通用多核處理器高性能數(shù)據(jù)包處理的業(yè)界標桿。
目前,DPDK技術主要應用于計算領域的硬件加速器、通信領域的網絡處理器和IT領域的多核處理器。隨著軟件(例如,DPDK)在I/O性能提升上的不斷創(chuàng)新,將多核處理器的競爭力提升到一個前所未有的高度。在SDN/NFV領域,DPDK技術得到了空前應用,產生了不少最佳實踐案例。
DPDK提出的目的就是為IA上的高速包處理。下圖所示的DPDK主要模塊分解展示了以基礎軟件庫的形式,為上層應用的開發(fā)提供一個高性能的基礎I/O開發(fā)包。主要利用了有助于包處理的軟硬件特性,如大頁、緩存行對齊、線程綁定、預取、NUMA、IA最新指令的利用、Intel DDIO、內存交叉訪問等。
核心庫Core Libs,提供系統(tǒng)抽象、大頁內存、緩存池、定時器及無鎖環(huán)等基礎組件。
PMD庫,提供全用戶態(tài)的驅動,以便通過輪詢和線程綁定得到極高的網絡吞吐,支持各種本地和虛擬的網卡。
Classify庫,支持精確匹配(Exact Match)、最長匹配(LPM)和通配符匹配(ACL),提供常用包處理的查表操作。
QoS庫,提供網絡服務質量相關組件,如限速(Meter)和調度(Sched)。
除了這些組件,DPDK 還提供了幾個平臺特性,比如節(jié)能考慮的運行時頻率調整(POWER),與Linux kernel stack建立快速通道的 KNI(Kernel Network Interface)。而Packet Framework和DISTRIB為搭建更復雜的多核流水線處理模型提供了基礎的組件。
DPDK軟件包內有一個最基本的三層轉發(fā)實例(l3fwd),可用于測試雙路服務器整系統(tǒng)的吞吐能力,通過現(xiàn)場實驗,可以達到220Gbit/s的數(shù)據(jù)報文吞吐能力。除了通過硬件或者軟件提升性能之外,如今DPDK整系統(tǒng)報文吞吐能力上限已經不再受限于CPU的核數(shù),當前瓶頸在于PCIe(IO總線)的LANE數(shù)。換句話說,系統(tǒng)性能的整體I/O天花板不再是CPU,而是系統(tǒng)所提供的所有PCIe LANE的帶寬,也就是能插入多少個高速以太網接口卡。
在這樣的性能基礎上,網絡節(jié)點的軟化(NFV)就成為可能。對于網絡節(jié)點上運轉的不同形態(tài)的網絡功能,通過軟化并適配到一個通用的硬件平臺,就是軟硬件解耦。解耦正是NFV的一個核心思想,而硬件解耦的多個網絡功能在單一通用節(jié)點上的隔離共生問題,就是另一個核心思想---虛擬化。
3、電信云中數(shù)據(jù)包轉發(fā)性能提升中的DPDK
cache的作用
在當今服務器領域,一個處理器通常包含多個核心(Core),集成Cache子系統(tǒng),內存子系統(tǒng)通過內部或外部總線與其通信。在經典計算機系統(tǒng)中一般都有兩個標準化的部分:北橋(North Bridge)和南橋(SouthBridge)。它們是處理器和內存以及其他外設溝通的渠道。在這類系統(tǒng)中,北橋就是真?zhèn)€架構的瓶頸,一旦北橋處理不過來或故障,整個系統(tǒng)的處理效率就會變低或癱瘓。因此,后來計算機系統(tǒng)中只存在南橋芯片,而北橋部分就被全部移植到CPU的SoC中,其中最重要的部分就是內存控制器,并在此基礎上進一步衍生出NUMA和MPP架構,這個放在后面會講。
我們在本科學習計算機基礎課程時,都知道計算機的內存分為SRAM、DRAM、SDRAM和DDR(1/2/3/4)等不同類型。在早期的PC系統(tǒng)中,主要使用DRAM和SDRAM來作為內存,相比SRAM在成本、功耗方面有不小的優(yōu)勢,而且速度也還可以。后來在現(xiàn)今的PC系統(tǒng)中,利用SDRAM在一個時鐘周期的上下邊沿進行數(shù)據(jù)讀寫,整體數(shù)據(jù)吞吐率帶寬翻倍,也就是DDR RAM,DDR根據(jù)不同的主頻,又分為DDR1/DDR2/DDR3/DDR4。而SRAM,由于其功耗高、成本高,速度很快,一般都作為CPU的cache使用,目前都被封裝的CPU的SoC中。
一般來說,Cache由三級組成,之所以對Cache進行分級,也是從成本和生產工藝的角度考慮的。一級(L1)最快,但是容量最??;三級(LLC,Last Level Cache)最慢,但是容量最大。
一級Cache:一般分為數(shù)據(jù)Cache和指令Cache,數(shù)據(jù)Cache用來存儲數(shù)據(jù),而指令Cache用于存放指令。這種Cache速度最快,一般處理器只需要3~5個指令周期就能訪問到數(shù)據(jù),因此成本高,容量小,一般都只有幾十KB。
二級Cache:和一級Cache分為數(shù)據(jù)Cache和指令Cache不同,數(shù)據(jù)和指令都無差別地存放在一起。速度相比一級Cache慢一些,處理器大約需要十幾個處理器周期才能訪問到數(shù)據(jù),容量也相對來說大一些,一般有幾百KB到幾MB不等。
三級Cache:速度更慢,處理器需要幾十個處理器周期才能訪問到數(shù)據(jù),容量更大,一般都有幾MB到幾十個MB。在多核處理器內部,三級Cache由所有的核心所共有。這樣的共享方式,其實也帶來一個問題,有的處理器可能會極大地占用三級Cache,導致其他處理器只能占用極小的容量,從而導致Cache不命中,性能下降。因此,Intel公司推出了Intel® CAT技術,確保有一個公平,或者說軟件可配置的算法來控制每個核心可以用到的Cache大小。
為了將cache與內存進行關聯(lián),需要對cache和內存進行分塊,并采用一定的映射算法進行關聯(lián)。分塊就是將Cache和內存以塊為單位進行數(shù)據(jù)交換,塊的大小通常以在內存的一個存儲周期中能夠訪問到的數(shù)據(jù)長度為限。當今主流塊的大小都是64字節(jié),因此一個Cache line就是指64個字節(jié)大小的數(shù)據(jù)塊。而映射算法是指把內存地址空間映射到Cache地址空間。具體來說,就是把存放在內存中的內容按照一定規(guī)則裝入到Cache中,并建立內存地址與Cache地址之間的對應關系。當CPU需要訪問這個數(shù)據(jù)塊內容時,只需要把內存地址轉換成Cache地址,從而在Cache中找到該數(shù)據(jù)塊,最終返回給CPU。
根據(jù)Cache和內存之間的映射關系的不同,Cache可以分為三類:第一類是全關聯(lián)型Cache(full associative cache),第二類是直接關聯(lián)型Cache(direct mapped cache),第三類是組關聯(lián)型Cache(N-ways associative cache)。
全關聯(lián)型cache:需要在cache中建立一個目錄表,目錄表的每一項由內存地址、cache塊號和一個有效位組成。當CPU需要訪問某個內存地址時,首先查詢該目錄表判斷該內容是否緩存在Cache中,如果在,就直接從cache中讀取內容;如果不在,就去通過內存地址轉換去內存沖讀取。具體原理如下:
首先,用內存的塊地址A在Cache的目錄表中進行查詢,如果找到等值的內存塊地址,檢查有效位是否有效,只有有效的情況下,才能通過Cache塊號在Cache中找到緩存的內存,并且加上塊內地址B,找到相應數(shù)據(jù),這時則稱為Cache命中,處理器拿到數(shù)據(jù)返回;否則稱為不命中,CPU則需要在內存中讀取相應的數(shù)據(jù)。使用全關聯(lián)型Cache,塊的沖突最?。]有沖突),Cache的利用率也高,但是需要一個訪問速度很快的相聯(lián)存儲器。隨著Cache容量的增加,其電路設計變得十分復雜,因此一般只有TLB cache才會設計成全關聯(lián)型。
直接關聯(lián)型Cache:是指將某一塊內存映射到Cache的一個特定的塊,即Cache line中。假設一個Cache中總共存在N個Cache line,那么內存就被分成N等分,其中每一等分對應一個Cache line。比如:Cache的大小是2K,而一個Cache line的大小是64B,那么就一共有2K/64B=32個Cache line,那么對應我們的內存,第1塊(地址0~63),第33塊(地址64*32~64*33-1),以及第(N*32+1)塊都被映射到Cache第一塊中;同理,第2塊,第34塊,以及第(N*32+2)塊都被映射到Cache第二塊中;可以依次類推其他內存塊。直接關聯(lián)型Cache的目錄表只有兩部分組成:區(qū)號和有效位。具體原理如下:
首先,內存地址被分成三部分:區(qū)號A、塊號B和塊內地址C。根據(jù)區(qū)號A在目錄表中找到完全相等的區(qū)號,并且在有效位有效的情況下,說明該數(shù)據(jù)在Cache中,然后通過內存地址的塊號B獲得在Cache中的塊地址,加上塊內地址C,最終找到數(shù)據(jù)。如果在目錄表中找不到相等的區(qū)號,或者有效位無效的情況下,則說明該內容不在Cache中,需要到內存中讀取??梢钥闯?,直接關聯(lián)是一種很“死”的映射方法,當映射到同一個Cache塊的多個內存塊同時需要緩存在Cache中時,只有一個內存塊能夠緩存,其他塊需要被“淘汰”掉。因此,直接關聯(lián)型命中率是最低的,但是其實現(xiàn)方式最為簡單,匹配速度也最快。
組關聯(lián)型Cache:是目前Cache中用的比較廣泛的一種方式,是前兩種Cache的折中形式。在這種方式下,內存被分為很多組,一個組的大小為多個Cache line的大小,一個組映射到對應的多個連續(xù)的Cache line,也就是一個Cache組,并且該組內的任意一塊可以映射到對應Cache組的任意一個??梢钥闯觯诮M外,其采用直接關聯(lián)型Cache的映射方式,而在組內,則采用全關聯(lián)型Cache的映射方式。比如:有一個4路組關聯(lián)型Cache,其大小為1M,一個Cache line的大小為64B,那么總共有16K個Cache line,但是在4路組關聯(lián)的情況下,就擁有了4K個組,每個組有4個Cache line。一個內存單元可以緩存到它所對應的組中的任意一個Cache line中去。具體原理如下:
目錄表由三部分組成:“區(qū)號+塊號”、Cache塊號和有效位。一個內存地址被分成四部分:區(qū)號A、組號B、塊號C和塊內地址D。首先,根據(jù)組號B按地址查找到一組目錄表項;然后,根據(jù)區(qū)號A和塊號C在該組中進行關聯(lián)查找(即并行查找,為了提高效率),如果匹配且有效位有效,則表明該數(shù)據(jù)塊緩存在Cache中,得到Cache塊號,加上塊內地址D,可以得到該內存地址在Cache中映射的地址,得到數(shù)據(jù);如果沒有找到匹配項或者有效位無效,則表示該內存塊不在Cache中,需要處理器到內存中讀取。
Cache之所以能夠提高系統(tǒng)性能,主要是因為程序執(zhí)行存在局部性現(xiàn)象,即時間局部性(程序中指令和數(shù)據(jù)在時間上的關聯(lián)性,比如:循環(huán)體中的變量和指令)和空間局部性(程序中指令和數(shù)據(jù)在空間上的關聯(lián)性,比如:列表數(shù)據(jù)結構中的元素)。cache就可以根據(jù)程序的局部性特點,以及當前執(zhí)行狀態(tài)、歷史執(zhí)行過程、軟件提示等信息,然后以一定的合理方法,在數(shù)據(jù)/指令被使用前取入Cache,也就是cache預取。
內存的數(shù)據(jù)被加載進cache后,最終還是需要寫回到內存中,這個寫回的過程存在兩種策略:
直寫(write-through):在CPU對Cache寫入的同時,將數(shù)據(jù)寫入到內存中。這種策略保證了在任何時刻,內存的數(shù)據(jù)和Cache中的數(shù)據(jù)都是同步的,這種方式簡單、可靠。但由于CPU每次對Cache更新時都要對內存進行寫操作,總線工作繁忙,內存的帶寬被大大占用,因此運行速度會受到影響。
回寫(write-back):回寫相對于直寫而言是一種高效的方法?;貙懴到y(tǒng)通過將Cache line的標志位字段添加一個Dirty標志位,當處理器在改寫了某個Cache line后,并不是馬上把其寫回內存,而是將該Cache line的Dirty標志設置為1。當處理器再次修改該Cache line并且寫回到Cache中,查表發(fā)現(xiàn)該Dirty位已經為1,則先將Cache line內容寫回到內存中相應的位置,再將新數(shù)據(jù)寫到Cache中。
除了上述這兩種寫策略,還有WC(write-combining)和UC(uncacheable)。這兩種策略都是針對特殊的地址空間來使用的,這里不做詳細討論,有興趣的可以參考Intel官方社區(qū)。
在采用回寫策略的架構中,如果多個CPU同時對一個cache line進行修改后的寫回操作,就存在“臟”數(shù)據(jù)區(qū)域的問題,這就是cache一致性問題。其本質原因是存在多個處理器獨占的Cache,而不是多個處理器。解決Cache一致性問題的機制有兩種:基于目錄的協(xié)議(Directory-based protocol)和總線窺探協(xié)議(Bus snooping protocol)。這里因為篇幅問題,不再展開討論,有興趣的可參見《深入淺出DPDK》一書相關內容。
事實上,Cache對于絕大多數(shù)程序員來說都是透明不可見的,cache完成數(shù)據(jù)緩存的所有操作都是硬件自動完成的。但是,硬件也不是完全智能的。因此,Intel體系架構引入了能夠對Cache進行預取的指令,使一些對程序執(zhí)行效率有很高要求的程序員能夠一定程度上控制Cache,加快程序的執(zhí)行。DPDK對cache進行預取操作如下:
while (nb_rx < nb_pkts)
{
rxdp = &rx_ring[rx_id];
//讀取接收描述符
staterr = rxdp->wb.upper.status_error;
//檢查是否有報文收到
if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
break;
rxd = *rxdp;
//分配數(shù)據(jù)緩沖區(qū)
nmb = rte_rxmbuf_alloc(rxq->mb_pool);
nb_hold++;
//讀取控制結構體
rxe = &sw_ring[rx_id];
……
rx_id++;
if (rx_id == rxq->nb_rx_desc) rx_id = 0;
//預取下一個控制結構體mbuf rte_ixgbe_prefetch(sw_ring[rx_id].mbuf);
//預取接收描述符和控制結構體指針 if ((rx_id & 0x3) == 0) { rte_ixgbe_prefetch(&rx_ring[rx_id]); rte_ixgbe_prefetch(&sw_ring[rx_id]); }
……
//預取報文 rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
//把接收描述符讀取的信息存儲在控制結構體mbuf中
rxm->nb_segs = 1;
rxm->next = NULL;
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
……
rx_pkts[nb_rx++] = rxm;
}
同時,DPDK在定義數(shù)據(jù)結構或者數(shù)據(jù)緩沖區(qū)時就申明cache line對齊,代碼如下:
#define RTE_CACHE_LINE_SIZE 64
#define __rte_cache_aligned __attribute__((__aligned__(RTE_CACHE_LINE_SIZE)))
struct rte_ring_debug_stats
{
uint64_t enq_success_bulk;
uint64_t enq_success_objs;
uint64_t enq_quota_bulk;
uint64_t enq_quota_objs;
uint64_t enq_fail_bulk;
uint64_t enq_fail_objs;
uint64_t deq_success_bulk;
uint64_t deq_success_objs;
uint64_t deq_fail_bulk;
uint64_t deq_fail_objs;
} __rte_cache_aligned;
大頁內存
在前文《x86架構基礎》一文中提到了TLB的概念,其主要用來緩存內存地址轉換中的頁表項,其本質上也是一個cache,稱之為TLB cache。TLB和cache的區(qū)別是:TLB緩存內存地址轉換用的頁表項,而cache緩存程序用到的數(shù)據(jù)和指令。
TLB中保存著程序線性地址前20位[31:12]和頁框號的對應關系,如果匹配到線性地址就可以迅速找到頁框號,通過頁框號與線性地址后12位的偏移組合得到最終的物理地址。TLB使用虛擬地址進行搜索,直接返回對應的物理地址,相對于內存中的多級頁表需要多次訪問才能得到最終的物理地址,TLB查找大大減少了CPU的開銷。如果需要的地址在TLB Cache中,就會迅速返回結果,然后CPU用該物理地址訪問內存,這樣的查找操作也稱為TLB命中;如果需要的地址不在TLB Cache中,也就是不命中,CPU就需要到內存中訪問多級頁表,才能最終得到物理地址。但是,TLB的大小是有限的,因此TLB不命中的概率很大,為了提高內存地址轉換效率,減少CPU的開銷,就提出了大頁內存的概念。
在x86架構中,一般都分成以下四組TLB:
第一組:緩存一般頁表(4KB頁面)的指令頁表緩存(Instruction-TLB)。
第二組:緩存一般頁表(4KB頁面)的數(shù)據(jù)頁表緩存(Data-TLB)。
第三組:緩存大尺寸頁表(2MB/4MB頁面)的指令頁表緩存(Instruction-TLB)。
第四組:緩存大尺寸頁表(2MB/4MB頁面)的數(shù)據(jù)頁表緩存(Data-TLB)
如果采用常規(guī)頁(4KB)并且使TLB總能命中,需要尋址的內容都在該內容頁內,那么至少需要在TLB表中存放兩個表項。如果一個程序使用了512個內容頁也就是2MB大小,那么需要512個頁表表項才能保證不會出現(xiàn)TLB不命中的情況。但是,如果采用2MB作為分頁的基本單位,那么只需要一個表項就可以保證不出現(xiàn)TLB不命中的情況;對于消耗內存以GB為單位的大型程序,可以采用1GB為單位作為分頁的基本單位,減少TLB不命中的情況。需要注意的是:系統(tǒng)能否支持大頁,支持大頁的大小為多少是由其使用的處理器決定的。
在Linux啟動之后,如果想預留大頁,則可以使用以下的方法來預留內存。在非NUMA系統(tǒng)中,可以使用以下方法預留2MB大小的大頁。
# 預留1024個大小為2MB的大頁,也就是預留了2GB內存。
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
系統(tǒng)未開啟大頁內存的狀態(tài)
系統(tǒng)開啟大頁內存后的狀態(tài)
如果是在NUMA系統(tǒng)中,假設有兩個NODE的系統(tǒng)中,則可以用以下的命令:
# 在NODE0和NODE1上各預留1024個大小為2MB的大頁,總共預留了4GB大小。
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 1024 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
而對于大小為1GB的大頁,則必須在Linux的GRUB配置文件中進行修改,并重啟系統(tǒng)生效,不能動態(tài)預留。
DPDK中也是使用HUGETLBFS來使用大頁。首先,它需要把大頁mount到某個路徑,比如/mnt/huge,以下是命令:
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
需要注意的是:在mount之前,要確保之前已經成功預留內存,否則會失敗。該命令只是臨時的mount了文件系統(tǒng),如果想每次開機時省略該步驟,可以修改/etc/fstab文件,加上如下一行:
nodev /mnt/huge hugetlbfs defaults 0 0
對于1GB大小的大頁,則必須用如下的命令:
nodev /mnt/huge_1GB hugetlbfs pagesize=1GB 0 0
然后,在DPDK運行的時候,會使用mmap()系統(tǒng)調用把大頁映射到用戶態(tài)的虛擬地址空間,然后就可以正常使用了。
未完待續(xù)......