流量控制--3.Linux流量控制的元件

charlieroro發表於2020-11-16

Linux流量控制的元件

流量控制元素與Linux元件之間的相關性:

traditional element Linux component
入佇列 修訂:從使用者或網路接收報文
整流 class 提供了整流的能力
排程 一個 qdisc 就是一個排程器。排程器可以是一個簡單的FIFO,也可以變得很複雜,包括classes和其他qdiscs,如HTB。
分類 filter 物件通過一個classifier 物件執行分類。嚴格上講,除filter之外的元件不會用到分類器。
策略 policer僅作為filter的一部分而存在。
丟棄 drop 流量要求使用一個帶 policerfilter ,動作為"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,它可以包含任何排隊規則(qdiscs)以及潛在的和類結構。大部分文件適用於root qdisc及其子qdisc。一個介面傳輸的流量會經過egress或root qdisc。

一個介面上接收到的流量會經過ingress qdisc。由於其功能的限制,不允許建立子類,且僅允許存在一個被過濾器 附加的物件。事實上,ingress qdisc僅僅是一個物件,可以在其上附加策略器來限制網路介面上接收的流量。

總之,由於egress qdisc包含一個真正的qdisc,且具有流量控制系統的全部功能,因此可以使用egress qdisc做很多事情。而一個ingress qdisc僅支援一個策略器。除非另有說明,本文後續將主要關注附加到root qdisc的流量控制結構。

4.2 類

類僅會存在於classful qdisc (如 HTBCBQ)。類非常靈活,可以包含多個子類或單個子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

可以使用ipifconfig目錄獲取當前傳輸佇列的長度。令人困惑的是,這些命令對傳輸佇列長度的命名各部不同:

$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 就沒有任何意義。

可以使用ipifconfig命令來配置介面的傳輸佇列長度。

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

相關文章