最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

Linux內(nèi)核UDP收包為什么效率低?性能怎么優(yōu)化?一文幫你解決?。◤倪@幾點入手)

2022-05-18 14:32 作者:補給站Linux內(nèi)核  | 我要投稿
  • 現(xiàn)在很多人都在詬病Linux內(nèi)核協(xié)議棧收包效率低,不管他們是真的懂還是一點都不懂只是聽別人說的,反正就是在一味地懟Linux內(nèi)核協(xié)議棧,他們的武器貌似只有DPDK。

  • 但是,即便Linux內(nèi)核協(xié)議棧收包效率真的很低,這是為什么?有沒有辦法去嘗試著優(yōu)化?而不是動不動就DPDK。

我們從最開始說起。

  • Linux內(nèi)核作為一個通用操作系統(tǒng)內(nèi)核,脫胎于UNIX那一套現(xiàn)代操作系統(tǒng)理論。

  • 但一開始不知道怎么回事將網(wǎng)絡(luò)協(xié)議棧的實現(xiàn)塞進了內(nèi)核態(tài),從此它就一直在內(nèi)核態(tài)了。既然網(wǎng)絡(luò)協(xié)議棧的處理在內(nèi)核態(tài)進行,那么網(wǎng)絡(luò)數(shù)據(jù)包必然是在內(nèi)核態(tài)被處理的。無論如何,數(shù)據(jù)包要先進入內(nèi)核態(tài),這就涉及到了進入內(nèi)核態(tài)的方式:

  1. 外部可以從兩個方向進入內(nèi)核-從用戶態(tài)系統(tǒng)調(diào)用進入或者從硬件中斷進入。

也就是說,系統(tǒng)在任意時刻,必然處在兩個上下文中的一個:

  1. 進程上下文

  2. 中斷上下文 (在非中斷線程化的系統(tǒng),也就是任意進程上下文)

  • 收包邏輯的協(xié)議棧處理顯然是自網(wǎng)卡而上的,它顯然是在中斷上下文中,而數(shù)據(jù)包往用戶進程的數(shù)據(jù)接收處理,顯然是在應(yīng)用程序的進程上下文中, 數(shù)據(jù)包通過socket在兩個上下文中被轉(zhuǎn)接。

  • 在socket層的數(shù)據(jù)包轉(zhuǎn)接處,必然存在著一個隊列緩存,這是一個典型的 生產(chǎn)者-消費者 模型,中斷上下文的終點作為生產(chǎn)者將數(shù)據(jù)包入隊,而進程上下文作為消費者從隊列消費數(shù)據(jù)包:

  • 非常清爽的一個圖,這個圖是 兩個上下文接力處理協(xié)議棧收包邏輯的必然結(jié)果 ,讓我們加入一些實際必須要考慮的問題后,我們會發(fā)現(xiàn)這幅圖并不是那么清爽,然后再回過頭看如何來優(yōu)化。

  • 既然兩個上下文都要在任意可能的時刻操作同一個socket進行數(shù)據(jù)包的轉(zhuǎn)交,那么必須有一個同步機制保護socket元數(shù)據(jù)以及數(shù)據(jù)包skb本身。

  • 由于Linux內(nèi)核中斷,軟中斷可能處在任意進程上下文,唯一的同步方案幾乎就是spinlock了,于是,真正的圖示應(yīng)該是下面的樣子:

  • 現(xiàn)在可以說,類似上面的這種這種保護是非常必要的,特別是對于TCP而言。

  • 我們知道,TCP是基于事務(wù)的有狀態(tài)傳輸協(xié)議,而且攜帶復(fù)雜的流控和擁塞控制機制,這些機制所依托的就是socket當前的一些狀態(tài)數(shù)據(jù),比如inflight,lost,retrans等等,這些狀態(tài)數(shù)據(jù)在發(fā)包和接收ACK/SACK期間會不斷變化,所以說:

  • 在一個上下文完成一次事務(wù)傳輸之前,必須鎖定socket狀態(tài)數(shù)據(jù)。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)? ? ?

?

比方說發(fā)包流程。數(shù)據(jù)包的發(fā)送可以出現(xiàn)在兩個上下文中:

  1. 進程上下文:系統(tǒng)調(diào)用觸發(fā)的發(fā)包。

  2. 中斷上下文:ACK/SACK觸發(fā)的發(fā)包。

  • 任何一個上下文的發(fā)包過程必須被TCP協(xié)議本身比如擁塞控制,流量控制這些所終止,而不能被中途切換到另一個上下文中,所以必須鎖定。

  • 問題是,上圖中的鎖定是不是太狠了些,中斷上下文自旋時間完全取決于進程上下文的行文,這不利于軟中斷的快速返回,極大地降低了系統(tǒng)的響應(yīng)度。

  • 于是,需要把鎖的粒度進行細分。Linux內(nèi)核并沒有在橫向上將鎖的粒度做劃分,而是在縱向上,采用兩個層次的鎖機制:

  • 我們看到的Linux內(nèi)核在處理收包邏輯時的backlog,其實抽象出來就是上面的二級鎖,它是不是很像Windows的IRQL機制呢?伴隨著APC,DPC,你可以把暫時由于高level的IRQL阻滯而無法執(zhí)行的邏輯放入DPC:

  • 由于進程上下文對socket的low鎖占有,中斷上下文將skb排入次level的backlog隊列,當進程上下文釋放low鎖的時候,順序執(zhí)行次level被排入的任務(wù),即處理backlog中的skb。

  • 事實上這是一種非常常見且通用的設(shè)計,除了Windows的IRQL,Linux中斷的上半部/下半部也是這種基于思想設(shè)計的。

  • 前面說了,TCP的一次事務(wù)可能非常 復(fù)雜耗時 ,并且必須一次完成,這意味著期間必須持有socket low鎖,以發(fā)包邏輯 tcp_write_xmit 函數(shù)為例,其內(nèi)部循環(huán)發(fā)包,直到受到窗口限制而終止,每一次tcp_transmit_skb返回耗時3微秒~5微秒,平均4微秒,以每次發(fā)送4個包為例,在這期間,若使用spinlock,那么中斷上下文的收包路徑將自旋16微秒,16微秒對于spinlock而言有點久了,于是采用兩級的lock機制,非常有效!

  • backlog隊列機制有效降低了中斷上下文的spin時延,提高了系統(tǒng)的響應(yīng)度,非常不錯。

但問題是,UDP有必要這樣嗎?

  • 首先,UDP是無狀態(tài)的,收包和發(fā)包都無需事務(wù),協(xié)議棧對UDP的處理,從來都是單個報文粒度的,因此只需要保護唯一的socket接收隊列即可,即 sk_receive_queue 。

  • 需要保護的接收隊列操作區(qū)間都是指令級別的時延,采用一把單一的 sk_receive_queue->lock 足矣。

  • 確實,在Linux 2.6.25版本內(nèi)核之前,就是這么干的。而自從2.2版本內(nèi)核,TCP就已經(jīng)采用二級鎖backlog隊列了。

  • 然而,在2.6.25版本內(nèi)核中,Linux協(xié)議棧的UDP收包路徑,轉(zhuǎn)而采用了兩層鎖的backlog隊列機制,和TCP一樣的邏輯:

  • 顯然這非常沒有必要。如果你有多個線程同時操作一個UDP socket,將會直面這個熱點,但事實上,你很難遭遇這樣的場景,如果非要說一個,那么DNS服務(wù)器可能首當其中。

  • 之所以在2.6.25版本內(nèi)核引入了二級鎖backlog隊列,大致是考慮到UDP需要統(tǒng)計內(nèi)存全局記賬,以防UDP吃盡系統(tǒng)內(nèi)存,可以review一下 sk_rmem_schedule 函數(shù)的邏輯。而在2.6.25版本內(nèi)核之前,UDP的內(nèi)存使用是不記賬的,由于UDP本身沒有任何類似流控,擁塞控制之類的約束機制,很容易被惡意程序?qū)⑾到y(tǒng)內(nèi)存吃盡。

  • 因此,除了sk_receive_queue需要保護,內(nèi)存記賬邏輯也是需要保護的,比如累加當前skb對內(nèi)存的占用到全局數(shù)據(jù)結(jié)構(gòu)。但即便如此,把這些統(tǒng)計數(shù)據(jù)的更新都塞入到spinlock的保護區(qū)域,也還是要比兩級lock要好。

  • 在我看來,之所以引入二級鎖backlog機制來保護內(nèi)存記賬邏輯,這是在 借鑒 TCP的代碼,或者說 抄代碼 更直接些。這個攜帶backlog隊列機制的UDP收包代碼存在了好多年,一直在4.9內(nèi)核才終結(jié)。

事實上,僅僅下面的邏輯就可以了:

  • 簡單直接!Linux內(nèi)核的UDP處理邏輯在4.10版本也確實去掉了兩級的lock。恢復(fù)到了2.6.25內(nèi)核版本之前的邏輯。

  • 上面的優(yōu)化帶來了可觀的性能收益,但是卻并不值得炫耀。

  • 因為上面的優(yōu)化更像是解決了一個bug,這個bug是在2.6.25版本內(nèi)核因為借鑒TCP的backlog實現(xiàn)而引入的,而事實上,UDP并不需要這種花哨的backlog邏輯。所以說,上面的效果并非優(yōu)化而帶來的效果,而是解了一個bug帶來的效果。

但是為什么遲至4.10版本才發(fā)現(xiàn)并解決這個問題的呢?

  • 我想這件事可能跟QUIC有關(guān)。

  • 用得少的邏輯自然就不容易發(fā)現(xiàn)問題,這就好比David Miller在2.6版本內(nèi)核引入IPv6實現(xiàn)的那幾個bug,就是因為IPv6用的人少,所以一直在很晚的4.23+版本內(nèi)核才被發(fā)現(xiàn)被解決。對于UDP,一直到2.6.24版本其實現(xiàn)都挺好,符合邏輯,2.6.25引入的二級鎖bug同樣是因為UDP本身用的少而沒有被發(fā)現(xiàn)。

  • 在QUIC之前,很少有那種有來有回的持續(xù)全雙工UDP長連接,基本都是request/response的oneshot類型的連接。然而QUIC卻是類似TCP的全雙工協(xié)議,在數(shù)據(jù)發(fā)送端持續(xù)發(fā)送大塊數(shù)據(jù)的同時,伴隨著的是接收大量的ACK報文,這顯然和TCP一樣,也是一種反饋控制的方式來驅(qū)動數(shù)據(jù)的發(fā)送。

  • QUIC是有確認機制的,但是處理確認卻不是在內(nèi)核進行的,內(nèi)核只是一個快速將確認包收到用戶態(tài)QUIC處理進程的一個通路,這個通路越快越好!

  • 也就是說,QUIC的ACK報文的接收效率會影響其數(shù)據(jù)的發(fā)送效率。

  • 隨著QUIC的大規(guī)模部署,人們才開始逐漸關(guān)注其背后UDP的收包效率問題。

  • 擺脫了二級鎖的backlog隊列之后,僅僅是為UDP后續(xù)的優(yōu)化掃清了障礙,這才是真正剛剛開始。擺在UDP的內(nèi)核協(xié)議棧收包效率面前的,有一個現(xiàn)成的靶子,那就是DPDK。

  • 挺煩DPDK的,說實話,被人天天說的東西都挺煩。不過你得先把內(nèi)核協(xié)議棧的UDP性能優(yōu)化到接近DPDK,再把這種鄙視當后話來講才更酷。

  • 由于UDP的處理非常簡單,因此實現(xiàn)一個能和DPDK對接的UDP用戶態(tài)協(xié)議棧則并不是一件難事。而TCP則相反,它非常復(fù)雜,所以DPDK很少有完整處理TCP端到端邏輯的,大多數(shù)都只是做類似中間節(jié)點DPI這種事。目前都沒有幾個好用的基于DPDK的TCP實現(xiàn),但是UDP實現(xiàn)卻很多。

  • DPDK的偽粉絲拿UDP說事的,比拿TCP說事,成本要低很多。

  • 好吧,那為什么DPDK處理UDP收包效率那么高?

答案很簡單, DPDK是在進程上下文輪詢接收UDP數(shù)據(jù)包的! 也就是說,它擺脫了兩個問題:

  1. 進程上下文和中斷上下文操作共享數(shù)據(jù)的鎖問題。

  2. 進程上下文和中斷上下文切換導致的cache miss問題。

  • 這兩點其實也就是 “為什么內(nèi)核協(xié)議棧性能干不過用戶態(tài)協(xié)議?!?的要點。當然,Linux內(nèi)核協(xié)議棧無法擺脫這兩點問題,也就回答了本文的題目中的第一個問題, “Linux內(nèi)核UDP收包為什么效率低?” 。

  • 不同的上下文異步操作同一份數(shù)據(jù),鎖是必不可少的。關(guān)于鎖的話題已經(jīng)爛大街了。

  • 現(xiàn)在僅就cache來討論,中斷上下文和進程上下文之間的切換,也有一個明顯的case:

  • 中斷上下文中修改了socket的元統(tǒng)計數(shù)據(jù),該修改會表現(xiàn)在cache中,然而當其wakeup該socket的處理進程后,切換到進程上下文的recv系統(tǒng)調(diào)用,其也或讀或?qū)戇@個統(tǒng)計數(shù)據(jù),伴隨著cache的flush以及cache的一致性同步。

  • 如果這些操作統(tǒng)一在進程上下文中進行,cache的利用率將會高效很多。當然,回到UDP收包不合理的backlog隊列機制,其實backlog本身存在的目的之一,就是為了讓進程上下文去處理,以提高cache的利用率,減少不必要的flush。然而,初衷未必能達到效果,在傳輸層用backlog將skb推給進程上下文去處理,已經(jīng)太晚了,何必不再網(wǎng)卡就給進程上下文呢?就像DPDK那樣。

  • 其實Linux內(nèi)核社區(qū)早就意識到了這兩點,早在3.11版本內(nèi)核中引入的busy poll機制就是為了解決鎖和切換問題的。busy poll的思想非常簡單,那就是:

  • 不再需要軟中斷上下文往接收隊列里“推”數(shù)據(jù)包,而改成自己在進程上下文里主動從網(wǎng)卡上“拉”數(shù)據(jù)包。

  • 落實到代碼上,那就是在進程上下文的recvmsg函數(shù)中直接調(diào)用napi的收包函數(shù),從ring buffer里拿數(shù)據(jù),自己調(diào)用netif_receive_skb。

  • 如果busy poll總能執(zhí)行,它總是能拉取到自己下一個需要的數(shù)據(jù)包,那么這基本就是DPDK的效率了,然而和DPDK一樣,這并不是一個統(tǒng)一的解決方案,輪詢固然對于收包有收益,但中斷是不能丟的,用CPU的自旋輪詢換取收包效率,這買賣代價太大,畢竟Linux內(nèi)核并非專職收包的

  • 當然了,也許內(nèi)核態(tài)實現(xiàn)協(xié)議棧本身就是一種錯誤,但這個話題有點跑偏,畢竟我們就是要優(yōu)化內(nèi)核協(xié)議棧的,而不是放棄它。

  • 現(xiàn)在,我們不能指望busy poll擔當所有的性能問題,仍然要依靠中斷。既然依靠中斷,鎖的問題就是優(yōu)化的重點。

  • 以雙核CPU為例,假設(shè)CPU0專職處理中斷,而收包進程則綁定在CPU1上,我們很快能意識到, CPU0和CPU1對于每一個skb的enqueue和dequeue均在爭搶socket的sk_receive_queue的spinlock 。

優(yōu)化措施顯而易見, 將多個skb聚集起來,一次性入接收隊列 。顯然,這需要兩個隊列:

  1. 維護聚集隊列:由中斷上下文將skb推入該隊列。

  2. 維護接收隊列:進程上下文從該隊列拉取skb。

  • 接收隊列為空時,交換聚集隊列和接收隊列。

  • 這樣,同樣在上述雙核CPU的情況下,只有在上面的第3點的操作中,才需要鎖保護。

  • 考慮到機器的CPU并非雙核,可能是任意核,收包進程也未必綁定任何CPU,因此上述每一個隊列均需要一把鎖保護,無論如何, 和單隊列相比,雙隊列情況下,鎖的競爭減少了一半!

  • 如此一來,雙隊列解除了中斷上下文和進程上下文之間的鎖競爭。

來看一下對比圖示:

  • 引入雙隊列后:

即便已經(jīng)很不錯了,但是:

  1. 中斷上下文中不同CPU可能會收到同一個socket的skb,CPU依然會在聚集隊列的鎖上蹦跶。

  2. 不同的CPU上的進程也可能會處理同一個socket,本意是合作,卻需要接收隊列的鎖來將其操作串行化。

  • 沒辦法,通用的操作系統(tǒng)內(nèi)核只能做到這里了,如果要解決以上的問題,就需要按照任何和角色明確綁CPU核心了,然而這也就不再是通用的內(nèi)核了。最終,你會在內(nèi)核里聞到DPDK的腐臭味,超級惡心。

  • 對了,我暫且將雙隊列區(qū)分為了 聚集隊列 和 接收隊列 ,更好的名字可能是 backlog隊列 和 接收隊列 。中斷上下文總是操作backlog隊列,而進程上下文在接收隊列為空時,交換backlog隊列為接收隊列。然而,backlog隊列這個名字在我看來非常臭名昭著,所以,暫且不用它了。

  • 我想本文應(yīng)該就要結(jié)束了,確實沒有源碼分析,事實上,我覺得我寫的這篇要比下面的這種有意思的多,然而可能在網(wǎng)上能找到的基本都是這種非常詳細的源碼分析:

  • 我為什么沒有談UDP的GRO,LRO機制,因為太不通用了。但是另一方面,如果應(yīng)用程序加以稍微支持,UDP的GRO,LRO將會帶來非常可觀的收益,別忘了,內(nèi)核只是UDP報文的一個通路即可,既然是通路,它便不包含處理邏輯,越快通過,越好。如果你在乎高吞吐,那么就GRO唄,如下:

  • UDP的通用L4 GRO相當于一個非常簡單的5層協(xié)議,應(yīng)用程序按照len字段稍加解析拆分即可,這將極大減少系統(tǒng)調(diào)用的次數(shù),減少上下文切換帶來的cache miss損耗。


Linux內(nèi)核UDP收包為什么效率低?性能怎么優(yōu)化?一文幫你解決!(從這幾點入手)的評論 (共 條)

分享到微博請遵守國家法律
张掖市| 三门县| 漳州市| 武平县| 田东县| 康保县| 宜章县| 通海县| 兴隆县| 绥棱县| 五河县| 兰溪市| 乐陵市| 登封市| 平顶山市| 广元市| 略阳县| 皋兰县| 西吉县| 乡宁县| 印江| 玛曲县| 山丹县| 密云县| 沿河| 墨玉县| 武威市| 伊通| 理塘县| 沂南县| 黄山市| 肃北| 临高县| 汝南县| 榆树市| 鄂温| 保康县| 东港市| 伊宁市| 上杭县| 共和县|