Linux 網絡堆棧的排隊機制(一)

在任何網絡堆棧或設備中,數據包的隊列都是非常重要。這些隊列使得不在同一時刻加載的模塊能夠相互通信,並且能提高網絡性能,同時也會間接影響到網絡延時的長短。本文章通過闡述IP數據包在Linux網絡中的排隊機制,來解釋兩個問題:

  • BQL一類新特性是如何減小網絡延時的。

  • 如何控制已減小延時後的緩存。

下面這張圖(和它的變形)將會在文中不斷的出現,用以說明具體的概念。

figure_1_v2
figure1

驅動隊列(環形緩存區)

驅動隊列位於IP數據棧和網卡之間。驅動隊列使用先進先出算法,並通過環形緩存區實現—可以暫時把環形緩存區當做一個固定大小的緩存器。這個隊列中不含任何來自包(分組)的數據,直接參與排隊的是描述符(descriptor)。這些描述符指向 “內核套接字緩存”(socket kernel buffers,簡寫爲SKBs),SKB中含有在整個內核處理過程中都要使用的數據包。

figure_2_v2
figure2

進入驅動隊列的數據,來自IP數據棧,在IP數據棧裏所有的IP數據包都要進行排隊。這些數據包可以從本地獲得,當某個網卡在網絡中充當路由器時,數據包也可以從網卡上接收,找到路由後再發出去。從IP數據棧中進入驅動隊列的數據包,先由硬件使之出列,再通過數據總線發送到網卡上,以進行傳輸。

驅動隊列的用處在於,只要系統有數據需要傳輸時,數據能夠馬上被傳送到網卡進行及時傳輸。大致意思就是,驅動隊列給了IP數據棧一個排隊的地方,通過硬件來對數據進行不同時的排隊。實現這個功能的另一種做法是,只要當物理傳輸媒介準備好傳輸數據時,網卡便馬上向IP數據棧申請數據。但是因爲對IP數據棧的數據申請,不可能馬上得到相應,所以這種辦法會浪費掉大量寶貴的傳輸資源,使吞吐量相應地降低。還有另一種正好相反的辦法—在IP數據棧準備好要傳輸的數據包後,進行等待,直到物理傳輸媒介做好傳輸數據的準備爲止。但是這種做法同樣也不理想,因爲在等待時IP數據棧被閒置,沒有辦法做別的工作。

棧中的超大數據包

多數網卡都有最大傳輸單位(MTU),用來表示能夠被物理媒介傳輸的最大幀數。以太網的默認MTU爲1500字節,也有一些支持Jumbo Frames的以太網MTU能夠達到9000多字節的。在IP數據棧中,MTU同時也是數據包傳輸的極限大小。比如,有個應用需要向TCP接口傳送2000字節的數據,這時,IP數據棧就必須創建兩個數據包來傳送它,因爲單個MTU小於2000字節。所以在進行較大數據的傳輸時,MTU如果相對較小,那麼大量數據包就會被創建出來,並且它們都要在物理媒介上傳輸到驅動隊列中。

爲了避免因爲MTU大小限制而出現的大量數據包,Linux內核對傳輸大小進行了多項優化:TCP段裝卸(TCP segmentation offload,簡稱TSO),UDP碎片裝卸(UDP fragmentation offload,簡稱UFO)和類型化段裝卸(generic segmentation offload ,簡稱GSO)。這些優化辦法,使得IP數據棧能夠創建比MTU更大的數據包。對於IPv4來說,優化後能夠創建出最大含65536的數據包,並且這些數據包和MTU大小的數據包一樣能夠進入驅動隊列排隊。在使用TSO和UFO優化時,由網卡將較大的數據包拆分成能夠傳輸的小數據包。對於沒有該硬件拆分功能的網卡,GSO優化能夠通過軟件來實現相同的功能,在數據包進入驅動隊列前迅速完成數據包拆分。

我在前面提過,驅動隊列中能包含描述符的數量是一定的(但描述符可以指向不同大小的數據包)。所以,TSO,UFO和GSO等優化措施將數據包增大,也不完全是件好事,因爲這些優化也會使驅動隊列中進行排隊的字節數增大了許多。圖像3是一個與圖像2的對比圖。

figure_3_v2
figure3

雖然接下來我要將重點放在傳輸路徑(transmit path)上了,但是這裏還是要再強調一下,Linux在數據接收端同樣有類似TSO、UFO和GSO的優化措施。這些接收端優化措施同樣也能將每個數據包的大小限制增大。具體來說,類型接收裝卸(generic receive offload,簡稱GRO)使網卡能夠將接收到的若干數據包合併成一個大數據包後,再傳給IP數據棧。在傳送數據包時,GRO能將原始數據包重組,使之符合IP數據包首尾連接的屬性。GRO同樣也會帶來副作用:較大的數據包在傳送時,可能會被拆分成了若干較小的數據包,這時,就會有多個數據包在同一數據流中同時進行排隊。較大的數據包如果發生了這樣的“微拆分”(micro burst),會對數據流之間的延時產生不利影響。

餓死和延時

雖然設置驅動隊列—即在IP數據棧和硬件網卡間排隊,非常便利,但這樣做也帶來“餓死和延時”的問題。

當網卡開始從驅動隊列中取數據包時,如果恰好這時驅動隊列爲空隊列,那麼硬件其實就失去了一次傳輸數據的機會,也就將系統的吞吐量降低了。我們把這種情況叫做“餓死”(starvation)。需要注意的是,如果驅動隊列爲空,而此時系統又沒有數據需要傳輸時,則不能稱爲“餓死”—-這是系統的正常情況。如何避免“餓死”是一個很複雜的問題,因爲IP數據棧將數據包傳入驅動隊列的過程,和硬件網卡從驅動隊列中取數據包的過程常常不是同時發生的。更加糟糕的是,這兩個過程間的間隔時間很不確定,常常隨着系統負載和網絡接口物理介質等外部環境而變化。比如說,在一個非常繁忙的系統中,IP數據棧就很少有機會能把數據包加入到驅動隊列緩存中,此時,很可能在驅動隊列對更多數據包排隊前,網卡就已經從驅動隊列中取數據了。因此,如果驅動隊列能變得更大的話,出現“餓死”的機率就會得到減小,並且系統吞吐量會相應提高。

雖然較大的隊列能夠保證高吞吐量,但是隊列變大的同時,大量的延時情況也會出現。

figure_4_v2
figure4

在圖像4中,單個帶寬較大的TCP段幾乎把驅動隊列佔滿,我們把它稱爲“塊(阻礙)交通流”(bulk traffic flow)(藍色部分)。在最後進行排隊的,是來自VoIP或遊戲的“交互數據流”(黃色部分)。像VoIP或遊戲一類的交互式應用,一般會在固定的時間間隔到達時,發送較小的數據包。這對延時是非常敏感的。並且這時,傳輸帶寬較大的數據,會使包傳送率(packet rate)增高而且會產生更大的數據包。較高的包傳送率會很快佔滿隊列緩存,進而阻礙交互性數據包的傳輸。爲了進一步說明這種情況,我們先做出如下假設:

  • 網絡接口的傳輸速率爲 5Mbit/sec (5000000 bits/sec)。

  • 每個“塊交通流”中的數據包(分組)大小爲1500bytes(或12000bits)。

  • 每個“交互交通流”中的數據包(分組)大小爲500bytes。

  • 驅動隊列共能容納128個描述符。

  • 現在有127個“塊(阻礙)”數據包,和1個交互數據包最後進行排隊。

127個數據傳輸完畢時,交互數據包才能進行傳輸。在以上假設下,將所有127個塊數據包傳輸完畢共需要(127 * 12,000) / 5,000,000 = 0.304 秒 (以每ping計算延時則爲304 毫秒 )。這樣的延時完全無法滿足交互式應用的需求,並且這個時間中還沒有包含完成所有傳輸所需的時間—因爲我們只計算了完成127個塊數據包傳輸的時間而已。之前我曾提到過,在驅動隊列中,數據包(分組)的大小在TSO等優化下能夠超過1500bytes。所以這也讓延時問題變得更嚴重了。

由超過規定大小的緩存而引起的較大延時,也被稱爲Bufferbloat。在Controlling Queue Delay 和the Bufferbloat中對這個問題有更詳細的闡述。

綜上所述,爲驅動隊列選擇正確的大小是一個Goldilocks問題—不能定的太大因爲會有延時,也不能定的太小因爲吞吐量會降低。

字節隊列限制(BQL)

字節隊列限制(BQL)是最近在linux內核(> 3.3.0)中出現的新特性,它能爲驅動隊列自動分配合適的大小以解決前面提到過的問題。BQL機制在將數據包進行排隊時,會自動計算當前系統下,能夠避免餓死所需的最小驅動隊列緩存大小,再決定是否對數據包進行排隊。如前文所述,進行排隊的數據越少,對數據包的最大延時也越小。

需要注意的是,驅動隊列的實際大小並沒有被BQL改變。BQL只是限制了當前時刻能夠進行排隊的數據多少(以字節計算)而已。任何超過大小限制的那一部分數據,都會被BQL阻擋在驅動隊列之外。BQL機制會在以下兩個事件發生時啓動:

  1. 數據包進入驅動隊列排隊時。

  2. 通過物理介質的傳輸已經完成時。

下面是簡化後的BQL算法。LIMIT指的是BQL計算出來的限制值。

1

2

3

4

5

6

****

** After adding packets to the queue

****

 

if the number of queued bytes is over the current LIMIT value then

        disable the queueing of more data to the driver queue

注意,進行排隊的數據大小可以超過LIMIT,因爲數據在進行LIMIT檢查以前,就已經排隊了。因爲非常大的字節,能夠通過TSO、UFO和GSO優化,一次性進行排隊,所以就造成了進行排隊數據過大的問題。如果你更加重視延時,也許你會將這些優化特性去除掉。文章後面有介紹去除的辦法。

BQL機制的第二階段,在硬件完成傳輸了以後啓動。

1

2

3

4

5

6

7

8

9

10

11

12

13

****

** When the hardware has completed sending a batch of packets

** (Referred to as the end of an interval)

****

 

if the hardware was starved in the interval

        increase LIMIT

 

else if the hardware was busy during the entire interval (not starved) and there are bytes to transmit

        decrease LIMIT by the number of bytes not transmitted in the interval

 

if the number of queued bytes is less than LIMIT

        enable the queueing of more data to the buffer

如代碼所示,BQL主要是在測試系統此時是否出現了餓死。如果出現了餓死,則LIMIT會增加,以使更多數據能夠進去隊列進行排隊。如果系統在測試時間內一直都十分繁忙,而且仍有字節在等着傳入隊列中,則此時隊列太大了,當前系統不需要這麼大的隊列,所以LIMIT會減小,以控制延時。

下面舉一個實例,來幫助大家理解BQL是如何控制用於排隊的數據大小的。在我的其中一個服務器上,默認的驅動隊列大小是256個描述符。因爲以太網MTU大小爲1500bytes,所以此時驅動隊列能對256 * 1,500 = 384,000 bytes進行排隊(如果使用TSO、GSO則排隊字節會更多)。但是,BQL此時計算出的LIMIT值爲3012bytes。所以,BQL大大限制了能夠進入隊列的數據大小。

有關BQL非常有趣的一點,能夠從B—byte 這個字母看出來。跟驅動隊列的大小和其他數據包隊列的大小單位不同,BQL以byte(字節)爲單位進行操作。這是因爲字節的數目,與其在物理介質上傳輸所需時間,有非常直接的關係。而數據包和描述符的數目與該時間則關係不大,因爲數據包和描述符的大小都是不一樣的。

BQL通過將排隊數據的大小進行限制,使之保持在能避免餓死出現的最小值,來減少網絡延時。BQL還有一個重要的特性,它能使原本在驅動隊列中進行排隊(使用FIFO算法排隊)的數據包,轉移到“排隊準則”(queueing discipline (QDisc))上來進行排隊。QDisc能夠實現比FIFO複雜得多的排隊算法策略。下一小節將重點介紹Linux的QDisc機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章