原創翻譯,轉載請注明出處。
分層調度器的時機主要體現在TX側,正好在傳遞報文之前。它的主要目的是在每個網絡節點按照服務級別協議來對不同的流量分類和對不同的用戶的報文區分優先級並排序。
一、概述
分層調度器跟以前使用網絡處理器實現的每條流或一組流的報文隊列和調度的流量管理器很相似。它看起來像在傳輸之前的一個臨時存儲了很大數量報文的緩沖區(入隊操作)。當網卡TX請求更多報文去發送的時候,這些報文遞交給網卡TX的預定義的SLA的報文選擇邏輯模塊之后會刪除。(出隊操作)。
分層調度器對大數量的報文隊列做了優化。當只需要小數量的隊列時,會使用消息傳遞隊列來替代。更多詳情參考"Worst Case Scenarios for Performance"。
二、分層調度
如下圖:
分層的第一級是 Ethernet TX 1/10/40端口,之后的分級是子端口,流水線,流分類和隊列。
有代表性的是,每一個子端口表示一個預定義好的用戶組,而每一個流水線表示一個個人用戶。每一個流分類表示不同的流量類型,流量類型包含了具體的丟包率,時延,抖動等需求,比如語音,視頻或數據傳輸。每一個隊列從一到多個相同用戶相同類型的連接里接待(原文是動詞:host)報文。
下面的表格對每個分層做了功能描述:
# 級別 兄弟 功能描述
1 Port 0 1、以太端口1/10/40 GbE輸出; 2、多個端口具有相同的優先級,RR調度
2 Subport 可配置,默認8個 1、使用令牌桶算法流量整形(每個子端口一個令牌桶) 2、每個流分類在子端口級別上有強制上限。 3、當高優先級不使用時,低優先級的流分類可以重用子端口的帶寬
3 Pipe 可配置,默認4K 1、使用令牌桶算法流量整形,每個流水線一個令牌桶
4 Traffic Class 4 1、同一個流水線的流分類按嚴格優先級順序處理 2、在流水線級別每個流分類有強制的上限 3、當高優先級不使用時,低優先級的流分類可以重用流水線的帶寬 4、當子端口流分類超額訂購之后(配置造成的),流水線的流分類上限是一個動態調整的值。並共享給所有子端口流水線
5 queue 4 1、同一個流分類里的隊列按照預定義的權重按WRR調度
三、API
1.端口調度配置API
rte_sched.h里包含了端口,子端口和流水線的配置函數。
2.端口調度器入隊API
int rte_sched_port_enqueue(struct rte_sched_port *port, struct rte_mbuf **pkts, uint32_t n_pkts)
3.端口調度器出隊API
int rte_sched_port_dequeue(struct rte_sched_port *port, struct rte_mbuf **pkts, uint32_t n_pkts)
4.使用舉例
1 /* File "application.c" */ 2 #define N_PKTS_RX 64 3 #define N_PKTS_TX 48 4 #define NIC_RX_PORT 0 5 #define NIC_RX_QUEUE 0 6 #define NIC_TX_PORT 1 7 #define NIC_TX_QUEUE 0 8 struct rte_sched_port *port = NULL; 9 struct rte_mbuf *pkts_rx[N_PKTS_RX], *pkts_tx[N_PKTS_TX]; 10 uint32_t n_pkts_rx, n_pkts_tx; 11 /* Initialization */ 12 <initialization code> 13 /* Runtime */ 14 while (1) { 15 /* Read packets from NIC RX queue */ 16 n_pkts_rx = rte_eth_rx_burst(NIC_RX_PORT, NIC_RX_QUEUE, pkts_rx, N_PKTS_RX); 17 /* Hierarchical scheduler enqueue */ 18 rte_sched_port_enqueue(port, pkts_rx, n_pkts_rx); 19 /* Hierarchical scheduler dequeue */ 20 rte_sched_port_enqueue(port, pkts_rx, n_pkts_rx); 21 /* Hierarchical scheduler dequeue */ 22 n_pkts_tx = rte_sched_port_dequeue(port, pkts_tx, N_PKTS_TX); 23 /* Write packets to NIC TX queue */ 24 rte_eth_tx_burst(NIC_TX_PORT, NIC_TX_QUEUE, pkts_tx, n_pkts_tx); 25 }
4.實現細節
每個端口的內部數據結構,下面是原理解釋圖:
# 數據結構 大小(bytes) #每個端口 入隊 出隊 功能描述
1 子端口表項 64 #端口的子端口數 . Rd,Wr 子端口的持久化數據(如積分,等等)
2 流水線表項 64 #端口的流水線數 . Rd,Wr 流水線和它的流分類依據隊列(積分)(這些會在運行期間被更新)的持久化數據。流水線配置參數在運行期間不支持修改。相同流水線的配置參數會多個流水線共享,所以它們不是流水線表的一部分。
3 隊列表項 4 #端口的隊列數 Rd,Wr Rd,Wr 隊列的持久化數據(比如讀寫指針)。所有隊列的每一個流分類,都是一樣的隊列大小。任何一個流水線的流水線表項都是存在同一個cache line里。
5.多核擴展策略
(1) 不同的物理端口運行不同的線程。同一個端口的入隊和出隊在同一個線程里執行。
(2) 不同的線程使用同一個物理端口,通過分隔同一個物理端口(虛擬端口)的不同的子端口集合來運行。同樣,一個子端口也能分隔成多個子端口集合,來運行不同的線程,出隊和入隊都是在同一個端口的同一個線程。唯一的要求是,考慮到性能,不能在一個核里處理全部的端口。
6.在同一個出端口入隊和出隊
在不同的CPU核上執行同一個出端口入隊和出隊的操作,會對調度器的性能有顯著影響,所以是不推薦這么做。
端口的入隊和出隊操作由如下的數據結構來共享訪問:
(1)報文描述符
(2)隊列表
(3)隊列存儲區域
(4)活動隊列的位圖(bitmap)
能預料到的性能損耗是由於:
(1)需要保證隊列和位圖操作是線程安全的,這就需要用鎖來保證原子訪問(比如自旋鎖/信號量)或者使用無鎖的原子訪問(比如:Test/Set, Compare And Swap(CAS)等等)。這比前面說的情況影響更大。
(2)在2個CPU核的緩存結構(滿足緩存一致性)里,雙向(Ping-pong)地在高速緩存線(cache line)存放共享數據結構
因此,調度器的入隊和出隊操作必須在同一個線程里執行,這樣隊列和位圖的操作就沒有線程安全的性能損耗以及可以保存調度器數據結構在同一個CPU核上。
7.性能調優
增加網卡端口的數量僅僅只需要成比例的增加用來流量調度的CPU核的數量。
8.入隊流水線
每一個報文的步驟順序是:
(1)mbuf里數據字段的訪問要求識別出報文的目的隊列。這些字段包括:端口,子端口,流分類和流分類的隊列,這些都是分類階段設置的。
(2)訪問隊列數據結構要識別出寫操作在隊列數組中的位置。如果隊列是滿的,那么這個報文會被丟棄。
(3)訪問隊列數組存儲報文的位置(比如:寫mbuf指針)
需要注意的是,這些步驟是有強相關的數據依賴的。比如,第2步和第3步不能在第1步和第2步的返回可用的結果之前啟動。這樣就阻止了CPU通過亂序執行來顯著提高性能的優化行為。
考慮到很高速率的報文接收以及很大數量的隊列,可以預料到在當前CPU,對當前報文入隊需要訪問的數據結構是不在L1,或者L2緩存的,因此會去內存中訪問,這樣會導致L1和L2數據緩存未命中(cache miss)。大量的L1、L2緩存未命中在性能上肯定是不可接受的。
一個解決方案是通過預先讀取需要的數據結構。預取操作有一個執行延遲,數據結構在當前預取的狀態時,CPU是不能訪問它的,這個時候,CPU可以去執行其他的工作。其他工作是去執行其他報文的入隊順序不同階段的操作,因此就實現了入隊操作的流水線。
下圖闡述了流水線4個階段的入隊操作實現,每個階段執行2個報文。沒有那個報文可以在同一時間處於大於一個流水線階段。
擁塞管理方案通過隊列流水線來描述就非常簡單了:
報文入隊直到特定的隊列滿了,這時,所有的報文都只能到同一個丟包隊列,直到報文被消耗完(通過出隊操作),這里可以通過使能RED/WRED來改進隊列流水線,通過查看隊列的使用情況和特定報文的報文優先級來決定出隊或丟棄。(這就與任意的讓所有報文都入隊/丟棄所有報文不同了。)
9.出隊狀態機
從當前流水線調度下一個報文的步驟順序是:
(1)使用bitmap掃描識別出下一個活動流水線,預取流水線。
(2)讀取流水線數據結構。更新當前流水線和它的子端口的積分。識別當前流水線的第一個活動流分類,通過WRR來選擇下一個隊列,預取當前流水線的16個隊列的隊列指針。
(3)從當前WRR隊列里讀取下一個元素並預取它的報文描述符。
(4)從報文描述符(mbuf數據結構)里讀取報文長度。基於報文長度和可用積分(如當前流水線,流水線流分類,子端口和子端口流分類),來執行或不執行調度。
為了避免緩存未命中,上述的數據結構(流水線,隊列,隊列數組,mbufs)都會在訪問之前預取過來。隱藏預取操作的延遲方案是在當前流水線的預取指令發出之后立即從當前流水線(流水線 grinder A)切換到另外一個流水線(grinder B)。這就給了足夠的時間在執行權切換回之前的流水線(grinder A)之前來完成預取操作。
出隊流水線狀態機利用數據在CPU高速緩存里會提高性能的原理,因此,在從同一個流水線的活動流分類移動到另外一個流水線之前,它會嘗試從一個流水線和流水線流分類里發送盡可能多的報文(直到最大的可用報文和積分)
10.時鍾同步
出端口在調度器調度數據傳輸的時候就像在模仿一個有很多槽位的傳送帶裝置。對於10GbE,上面有12.5億個字節槽位需要端口調度器每秒去填充。如果調度器不能夠快去填充,那么就有足夠的報文和積分繼續存在,這樣那些槽位就不會被使用,也就浪費了帶寬。
原則上,分層調度器出隊操作是有網卡TX觸發的。通常,一旦網卡Tx輸入隊列的使用降低到了一個預設的閥值,端口調度器就會被喚醒(基於中斷或者基於輪詢,通過持續的監視隊列的使用率)來將更多的報文加入到隊列。
(1)內部時鍾參考源
為了提升積分邏輯,調度器需要保持對時鍾跟蹤,因為積分的更新是基於時鍾。舉個栗子,子端口和流水線流量整形,流分類強制上限等等。
每當調度器決定發送一個報文給網卡TX的時候,調度器會相應地增加它的內部時鍾參考源。因此,以字節為單位保持內部時鍾參考源是很方便的,尤其是要求在物理接口的傳輸介質上發送一個字節來表示時間片的時候。這樣,當一個報文被調度到發送的時候,時鍾按(n + h)增加,n是報文長度的字節數,h是每個報文的幀前導字節數。
(2)內部時鍾參考源再同步
調度器需要調整對齊端口傳輸帶上的內部時鍾參考源。原因是為了防止丟包,調度器不能給網卡TX喂大於物理介質線速的字節數。(丟包處理通過調度器,因為網卡TX輸入隊列滿了,或者隨后在網卡TX)
調度器讀取每一個隊列調用的當前時間。CPU 時間戳可以通過讀取TSC(Time Stamp Counter)寄存器或者HPET(high precision event timer)寄存器來獲取。當前CPU時間戳是從CPU時鍾字節數轉換而來的,time_bytes = time_cycles/cycles_per_byte.cycles_per_byte是CPU周期數,等於在電纜中傳輸一個字節的時間。(舉個栗子:一個CPU頻率為2GHZ和一個10GbE端口,cycles_per_byte = 1.6)。
調度器維護了一個網卡時間的內核時鍾參考源。每當一個報文被調度時,網卡時間會按照報文長度(包含幀前導字節數)增加。每一個出隊調用時,調度器會檢查它的網卡時間的內部參考源,相對於當前時間:
1.如果網卡時間在將來(即:網卡時間 >= 當前時間),不需要調整網卡時間。這意味着調度器有能力在網卡實際需要多少報文時去調度網卡上的報文,因此網卡TX可以很多出路這些報文;
2.如果網卡時間在過去(即:網卡時間 < 當前時間),那么網卡時間應該調整到當前時間,這意味着調度器不能保持網卡字節傳輸帶上的速度,因此網卡帶寬會因為網卡TX處理較少報文而浪費。
(3)調度器的精度(accuracy)和粒度(granularity)
調度器的往返延遲(SRTD)是通過調度器在同一個流水線上的兩個連續檢測的時間(CPU周期的數量)。為了充分利用輸出端口(就是避免帶寬損失),調度器應該有能力在調度n個報文的速度上比網卡TX發送n個報文的速度要快。在假設流水線令牌桶沒有配置超額的情況下,調度器要跟得上每個獨立的流水線的速度。這意味着令牌桶的大小應該設置的足夠大才能防止因為過大的往返延遲(SRTD)溢出,因為這樣的話,會導致流水線的積分損失和帶寬損失。
11.積分規則(logic)
(1)調度決策
當下面全部條件都滿足時,發送下一個報文(從子端口S,流水線P,流分類TC,隊列Q)到報文發送完成的調度決策是最佳的:
1.子端口S的流水線P是從當前端口(grinders)選擇的
2.流分類TC是流水線P活動的流分類里優先級最高的
3.隊列Q是流水線P的流分類TC里通過WRR選擇的下一個隊列
4.子端口S有足夠的積分去發送這個報文
5.子端口S有足夠的積分給流分類TC來發送這個報文
6.流水線P有足夠的積分去發送這個報文
7.流水線P有足夠的積分給流分類TC來發送這個報文
如果上述條件都滿足了,那么在子端口S,子端口S的流分類TC,流水線P,流水線P的流分類TC上要發送的報文和需要積分就會被扣除。