流量控制–3.Linux流量控制的組件
Linux流量控制的組件
流量控制元素與Linux組件之間的相關性:
traditional element | Linux component |
---|---|
入隊列 | 修訂:從用戶或網路接收報文 |
整流 | class 提供了整流的能力 |
調度 | 一個 qdisc 就是一個調度器。調度器可以是一個簡單的FIFO,也可以變得很複雜,包括classes和其他qdiscs,如HTB。 |
分類 | filter 對象通過一個classifier 對象執行分類。嚴格上講,除filter之外的組件不會用到分類器。 |
策略 | policer 僅作為filter 的一部分而存在。 |
丟棄 | drop 流量要求使用一個帶 policer 的 filter ,動作為”drop” |
標記 | dsmark qdisc 用於標記報文。 |
入隊列; | 驅動隊列位於qdisc和網路介面控制器(NIC)之間。驅動隊列給上層(IP棧和流量控制子系統)提供了數據非同步入隊列的位置(後續由硬體對數據進行操作)。隊列的大小由Byte Queue Limits (BQL)動態設置。 |
4.1 qdisc
簡單講,一個qidsc就是一個調度器。每個出介面都需要某種類型的調度器,默認的調度器為FIFO。Linux下的其他qdisc會根據調度器的規則來重新安排進入調度器隊列的報文。
qdisc是構建所有Linux流量控制的主要部件,也被稱為排隊規則。
classful qdiscs 可以包含類,並提供了可以附加到過濾器的句柄。一個classful qidsc可以不使用子類,但這樣通常會消耗CPU周期和其他系統資源,且毫無意義。
classless qdiscs 不包含類,也不會附加過濾器。由於一個classless qdisc不包含任何類的子類,因此不能使用分類,意味著不能附加任何過濾器。
在使用中可能會對術語root
qdisc 和ingress
qdisc產生混淆。實際中並不存在真正的排隊規則,而是連接流量控制結構的出站(出流量)和入口(入流量)的位置。
每個介面都會包含root
qdisc 和ingress
qdisc。最主要和最常用的是egress qdisc,即root
qdisc,它可以包含任何排隊規則(qdisc
s)以及潛在的類和類結構。大部分文檔適用於root
qdisc及其子qdisc。一個介面傳輸的流量會經過egress或root
qdisc。
一個介面上接收到的流量會經過ingress
qdisc。由於其功能的限制,不允許創建子類,且僅允許存在一個被過濾器 附加的對象。事實上,ingress qdisc僅僅是一個對象,可以在其上附加策略器來限制網路介面上接收的流量。
總之,由於egress qdisc包含一個真正的qdisc,且具有流量控制系統的全部功能,因此可以使用egress qdisc做很多事情。而一個ingress
qdisc僅支援一個策略器。除非另有說明,本文後續將主要關注附加到root qdisc的流量控制結構。
4.2 類
類僅會存在於classful qdisc
(如 HTB 和 CBQ)。類非常靈活,可以包含多個子類或單個子qdisc。一個子類本身也可以包含一個classful qdisc,通過這種方式可以實現複雜的流量控制場景。
任何類都可以附加任意多的過濾器,從而允許選擇一個子類或使用過濾器來重新分類或直接丟棄進入特定類的流量。葉子類是qdisc中的終止類,它包含一個qdisc(默認是FIFO),且不會包含子類。任何包含子類的類都屬於內部類(或root類),而非葉子類。
4.3 過濾器
過濾器是Linux流量控制系統中最複雜的組件,提供了將流量控制的主要元素粘合到一起的機制。過濾器最簡單和最明顯的角色就是對報文進行分類(Section 3.3, 「Classifying」)。Linux過濾器允許用戶使用多個或單個過濾器來將報文分類到一個輸出隊列。
過濾器可能附加到classful qdiscs或類,但入隊列的報文總是首先進入root qdisc。在報文經過的root qdisc上附加的過濾器後,報文可能被重定向到任何子類(子類可以包含自己的過濾器),後續可能對報文進一步分類。
4.4 分類器
過濾器的對象,可以使用tc進行操作,且可以使用不同的分類機制,其中最常用的是u32
分類器。u32分類器允許用戶根據報文的屬性選擇報文。
分類器可以作為過濾器的一部分來標識報文的特徵或元數據。Linux分類器對象可以看作是流量控制分類的基本操作和基本機制。
4.5 策略器
該機制僅作為Linux流量控制中的過濾器的一部分。一個策略器可以在速率超過指定速率時執行一個動作,在速率低於指定速率時執行另一個動作,善用策略可以模擬出一個三色表。參見 Section 10, 「Diagram」。
雖然策略 和整流 都是流量控制中用來限制頻寬的基本元素,但使用策略器並不會導致流量延遲。它只會根據特定的準則來執行某個動作。參見Example 5, 「tc filter
」。
4.6 丟棄
該流量控制機制僅作為策略器的一部分。任何附加到過濾器的策略器都包含一個drop動作。
註:策略器是流量控制系統中唯一可以顯式地丟棄報文的地方。策略器可以限制入隊列的報文的速率,或丟棄匹配特定模式的所有流量。
流量控制系統中,報文的丟失可能是由某個動作引起的副作用。例如,如果使用的調度器使用和GRED一樣的方法控制流時,報文將被丟棄。
或者,當出現突發或超負荷時,如果整流器或調度器的緩衝用盡,也可能會丟棄報文。
4.7 句柄
每個類和classful qdisc(Section 7, 「Classful Queuing Disciplines (qdiscs)」)都要求在流量控制結構中存在一個唯一的標識符,該唯一標識符被稱為句柄,每個句柄包含兩個組成成員,一個主號和一個次號。用戶可以根據以下規則隨意分配這些號。
類和qdiscs的句柄號:
主號
- 該參數對內核完全沒有意義。用戶可能會任意使用一個編號方案,但流量控制結構中具有相同父qdisc的所有對象必須共享一個次句柄號。對於直接附加到root qdisc的對象,傳統的編號方案會從1開始。
次號
- 如果次號為0,則表明該對象為qdisc,否則表明該對象為一個類。所有共享同一個qdisc的類必須包含一個唯一的次號。
特殊的句柄 ffff:0
保留給ingress
qdisc使用。
句柄作為tc過濾器的classid和flowid的目標參數,同時也是用戶側應用使用的標識對象的外部標識符。內核為每個對象維護內部標識符。
4.8 txqueuelen
可以使用ip或ifconfig目錄獲取當前傳輸隊列的長度。令人困惑的是,這些命令對傳輸隊列長度的命名各部不同:
$ifconfig eth0
eth0 Link encap:Ethernet HWaddr 00:18:F3:51:44:10
inet addr:69.41.199.58 Bcast:69.41.199.63 Mask:255.255.255.248
inet6 addr: fe80::218:f3ff:fe51:4410/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:435033 errors:0 dropped:0 overruns:0 frame:0
TX packets:429919 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:65651219 (62.6 MiB) TX bytes:132143593 (126.0 MiB)
Interrupt:23
$ip link
1: lo: mtu 16436 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 00:18:f3:51:44:10 brd ff:ff:ff:ff:ff:ff
Linux默認的傳輸隊列的長度為1000個報文,這是一個相當大的緩衝(特別是當頻寬比較低時)。(為了理解其原因,請參見針對延遲和吞吐量的討論,特別是緩衝膨脹。)
更有趣的是,txqueuelen
僅用作這些排隊規則的默認隊列長度。
pfifo_fast
(Linux的默認隊列規則)sch_fifo
sch_gred
sch_htb
(僅用於默認隊列)sch_plug
sch_sfb
sch_teql
txqueuelen參數控制上述QDiscs的隊列大小。對於大多數的隊列規則,tc命令行中的limit
參數會覆蓋默認的txqueuelen 值。總之,如果沒有使用上述的任意一種隊列規則或覆蓋了默認的隊列長度,那麼txqueuelen 就沒有任何意義。
可以使用ip或ifconfig命令來配置介面的傳輸隊列長度。
ip link set txqueuelen 500 dev eth0
注意:ip命令使用qlen
來表示txqueuelen
。
4.9 驅動隊列(即ring buffer)
在IP棧和網路介面控制器之間存在驅動隊列。該隊列通常使用先進先出的ring buffer來實現(可以認為是一個固定長度的緩衝)。驅動隊列不包含任何報文數據,僅包含指向其他數據結構(socket kernel buffers,簡稱SKBs)的描述符,SKB包含報文數據,並在整個內核中使用。
驅動隊列的輸入源為保存了完整IP報文的IP棧,這些報文可能是本地的,或當設備作為路由器時接收到的需要從一個NIC路由到另一個NIC的報文。IP棧會將報文添加到驅動隊列,並由硬體驅動出隊列,在傳輸時會通過數據匯流排發送到NIC硬體。
驅動隊列存在的原因是為了保證在任何時候,當系統需要傳輸數據時,NIC會立即傳輸該數據。即,驅動隊列為IP棧和硬體操作提供了一個非同步處理數據的位置。一個備選方案是,一旦物理媒介就緒時就向IP棧查詢可用的報文。但由於對這類請求的響應不可能是即時的,因此這種設計浪費了寶貴的傳輸機會,導致吞吐量降低。相反的方案是,IP棧在創建一個報文後會等待硬體就緒,這種方案同樣不理想,因為IP棧將無法繼續其他工作。
更多關於驅動隊列的細節參見5.5章節。
4.10 Byte Queue Limits(BQL)
Byte Queue Limits (BQL) 是Linux內核(> 3.3.0 )引入的一個新特性,用於嘗試自動解決驅動程式隊列大小的問題。該特性添加了一層處理,它會根據當前系統的情況計算出避免出現飢餓的最小緩衝大小,以此作為報文進入驅動隊列的依據。回顧一下,隊列中的數據總量越少,隊列中的報文的最大延遲越小。
需要注意的是,BQL不會修改驅動隊列的實際長度,相反,它會計算當前時間可以入隊列的數據的(位元組數)上限。當隊列中的數據超過該限制之後,驅動隊列的上層需要決定是否保留會丟棄這部分數據。
當發生兩種情況時會觸發BQL機制:當報文進入驅動隊列,或當線路上的傳輸已經結束。下面給出了一個簡單的BQL演算法。LIMIT指BQL計算出的值。
****
** 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
BQL基於測試設備是否發生了飢餓現象,如果是,則增加LIMIT來允許更多的報文入隊列,以此降低飢餓的概率。如果設備繁忙,且後續還有報文持續傳輸到隊列中,當隊列中的報文大於當前系統所需要的數量時,會降低LIMIT來限制飢餓。
下面給出一個真實的例子,可以幫助了解BQL能夠在多大程度上影響排隊的數據量。在一台伺服器上,驅動隊列的大小默認為256個描述符。由於乙太網的MTU為1500位元組,意味著驅動隊列中的報文最大為256 * 1,500 = 384,000位元組(禁用TSO,GSO等)。但此時BQL計算出的限制值為3012位元組。如你所見,BQL大大限制了進入隊列的數據量。
從名稱的第一個單詞可以推斷出BQL的一個有趣的特點–位元組。與驅動隊列和其他大多數報文隊列不同,BQL操作的是位元組。這是因為相比報文數或描述符,位元組數與物理媒介的傳輸時間有著更為直接的關係。
BQL將進入隊列的數據量限制到避免餓死所需的最小數量,從而減少了網路延遲。它還有一個非常重要的副作用,那就是將大多數報文排隊的點從驅動隊列(一個簡單的FIFO)移動到排隊規則(QDisc)層,從而實現更複雜的排隊策略。下一節將介紹Linux的QDisc層。
4.10.1 設置BQL
BQL演算法是自適應的,並不需要過多的人為接入。但如果需要關注低比特率下的最佳延遲,則有可能需要覆蓋計算出的LIMIT值。可以在/sys目錄根據NIC的名稱和位置下找到BQL的狀態和配置。例如我的一台伺服器上的eth0 的目錄為:
可以使用
ethtool -i <介面名稱>
來查看設備的PCI號。
/sys/devices/pci0000:00/0000:00:14.0/net/eth0/queues/tx-0/byte_queue_limits
該目錄中的文件為:
- hold_time: 修改LIMIT的間隔時間,單位毫秒
- inflight: 隊列中還沒有傳輸的位元組數
- limit: BQL計算出的LIMIT值。如果NIC驅動不支援BQL,則為0
- limit_max: 可配置的LIMIT的最大值,減小該值可以優化延遲
- limit_min: 可配置的LIMIT的最小值,增大該值可以優化吞吐量
要對可排隊的位元組數設置上限,請將新值寫入limit_max文件:
echo "3000" > limit_max