Linux 網路堆疊中的擴充套件¶
簡介¶
本文件描述了 Linux 網路堆疊中的一組互補技術,以提高多處理器系統的並行性和效能。
描述了以下技術
RSS:接收端縮放
RPS:接收資料包引導
RFS:接收流引導
加速接收流引導
XPS:傳送資料包引導
RSS:接收端縮放¶
現代網絡卡支援多個接收和傳送描述符佇列(多佇列)。在接收時,網絡卡可以將不同的資料包傳送到不同的佇列,以在 CPU 之間分配處理。網絡卡透過將過濾器應用於每個資料包來分配資料包,該過濾器將資料包分配給少量邏輯流之一。每個流的資料包被引導到單獨的接收佇列,而接收佇列又可以由單獨的 CPU 處理。這種機制通常被稱為“接收端縮放”(RSS)。RSS 和其他縮放技術的目標是統一提高效能。多佇列分配也可用於流量優先順序排序,但這並非這些技術的重點。
RSS 中使用的過濾器通常是網路和/或傳輸層標頭的雜湊函式——例如,資料包的 IP 地址和 TCP 埠上的 4 元組雜湊。RSS 最常見的硬體實現使用一個 128 項的間接表,其中每個條目儲存一個佇列號。資料包的接收佇列由遮蔽掉資料包計算出的雜湊的低七位(通常是 Toeplitz 雜湊)來確定,將此數字作為間接表的鍵並讀取相應的值。
某些網絡卡支援對稱 RSS 雜湊,如果 IP(源地址、目標地址)和 TCP/UDP(源埠、目標埠)元組被交換,則計算出的雜湊是相同的。這在某些監視 TCP/IP 流(IDS、防火牆等)並且需要流的兩個方向都落在同一個 Rx 佇列(和 CPU)上的應用程式中是有益的。“Symmetric-XOR”和“Symmetric-OR-XOR”是 RSS 演算法的型別,它們透過 XOR/OR IP 和/或 L4 協議的輸入源和目標欄位來實現這種雜湊對稱性。然而,這會導致輸入熵減少,並可能被利用。
具體來說,“Symmetric-XOR”演算法按如下方式對輸入進行 XOR
# (SRC_IP ^ DST_IP, SRC_IP ^ DST_IP, SRC_PORT ^ DST_PORT, SRC_PORT ^ DST_PORT)
另一方面,“Symmetric-OR-XOR”演算法按如下方式轉換輸入
# (SRC_IP | DST_IP, SRC_IP ^ DST_IP, SRC_PORT | DST_PORT, SRC_PORT ^ DST_PORT)
然後將結果饋送到底層 RSS 演算法。
一些高階網絡卡允許根據可程式設計過濾器將資料包引導到佇列。例如,繫結到 webserver 的 TCP 埠 80 資料包可以定向到它們自己的接收佇列。可以使用 ethtool(--config-ntuple)配置此類“n 元組”過濾器。
RSS 配置¶
支援多佇列的網絡卡的驅動程式通常提供一個核心模組引數,用於指定要配置的硬體佇列的數量。例如,在 bnx2x 驅動程式中,此引數稱為 num_queues。典型的 RSS 配置是為每個 CPU 提供一個接收佇列(如果裝置支援足夠的佇列),否則至少為每個記憶體域提供一個接收佇列,其中記憶體域是一組共享特定記憶體級別(L1、L2、NUMA 節點等)的 CPU。
RSS 裝置的間接表透過遮蔽雜湊來解析佇列,通常由驅動程式在初始化時進行程式設計。預設對映是將佇列均勻地分佈在表中,但可以使用 ethtool 命令(--show-rxfh-indir 和 --set-rxfh-indir)在執行時檢索和修改間接表。可以修改間接表以賦予不同的佇列不同的相對權重。
RSS IRQ 配置¶
每個接收佇列都有一個與其關聯的單獨的 IRQ。當新資料包到達給定的佇列時,網絡卡會觸發它以通知 CPU。 PCIe 裝置的信令路徑使用訊息訊號中斷 (MSI-X),可以將每個中斷路由到特定的 CPU。佇列到 IRQ 的活動對映可以從 /proc/interrupts 中確定。預設情況下,IRQ 可以在任何 CPU 上處理。由於資料包處理的很大一部分發生在接收中斷處理中,因此在 CPU 之間分散接收中斷是有利的。要手動調整每個中斷的 IRQ 親和性,請參閱SMP IRQ 親和性。某些系統將執行 irqbalance,這是一個動態最佳化 IRQ 分配的守護程式,因此可能會覆蓋任何手動設定。
建議的配置¶
當關注延遲或接收中斷處理形成瓶頸時,應啟用 RSS。在 CPU 之間分散負載會減少佇列長度。對於低延遲網路,最佳設定是分配與系統中 CPU 數量一樣多的佇列(如果更低,則為網絡卡最大值)。最高效的高速率配置可能是具有最小數量接收佇列的配置,其中沒有接收佇列由於飽和的 CPU 而溢位,因為在啟用中斷合併的預設模式下,中斷(以及工作)的總數會隨著每個額外的佇列而增長。
可以使用 mpstat 實用程式觀察每個 CPU 的負載,但請注意,在具有超執行緒 (HT) 的處理器上,每個超執行緒都表示為一個單獨的 CPU。對於中斷處理,HT 在初始測試中沒有顯示出任何好處,因此將佇列數量限制為系統中 CPU 核心的數量。
專用 RSS 上下文¶
現代網絡卡支援建立多個共存的 RSS 配置,這些配置基於顯式匹配規則進行選擇。當應用程式想要約束接收流量的佇列集(例如,對於特定目標埠或 IP 地址)時,這可能非常有用。下面的示例顯示瞭如何將所有流量定向到 TCP 埠 22 到佇列 0 和 1。
要建立額外的 RSS 上下文,請使用
# ethtool -X eth0 hfunc toeplitz context new
New RSS context is 1
核心報告回分配的上下文的 ID(預設的、始終存在的 RSS 上下文的 ID 為 0)。可以使用與預設上下文相同的 API 查詢和修改新上下文
# ethtool -x eth0 context 1
RX flow hash indirection table for eth0 with 13 RX ring(s):
0: 0 1 2 3 4 5 6 7
8: 8 9 10 11 12 0 1 2
[...]
# ethtool -X eth0 equal 2 context 1
# ethtool -x eth0 context 1
RX flow hash indirection table for eth0 with 13 RX ring(s):
0: 0 1 0 1 0 1 0 1
8: 0 1 0 1 0 1 0 1
[...]
要使用新的上下文,請使用 n 元組過濾器將流量定向到它
# ethtool -N eth0 flow-type tcp6 dst-port 22 context 1
Added rule with ID 1023
完成後,刪除上下文和規則
# ethtool -N eth0 delete 1023
# ethtool -X eth0 context 1 delete
RPS:接收資料包引導¶
接收資料包引導 (RPS) 在邏輯上是 RSS 的軟體實現。在軟體中,它必然在資料路徑中稍後被呼叫。RSS 選擇佇列,從而選擇將執行硬體中斷處理程式的 CPU,而 RPS 選擇 CPU 來執行中斷處理程式之上的協議處理。這是透過將資料包放置在所需 CPU 的積壓佇列中並喚醒 CPU 進行處理來完成的。RPS 比 RSS 有一些優勢
它可以與任何網絡卡一起使用
可以輕鬆新增軟體過濾器以對新協議進行雜湊處理
它不會增加硬體裝置中斷率(儘管它會引入處理器間中斷 (IPI))
當驅動程式使用 netif_rx() 或 netif_receive_skb() 將資料包傳送到網路堆疊時,會在接收中斷處理程式的下半部分呼叫 RPS。 這些呼叫 get_rps_cpu() 函式,該函式選擇應該處理資料包的佇列。
確定 RPS 目標 CPU 的第一步是計算資料包地址或埠上的流雜湊(2 元組或 4 元組雜湊,具體取決於協議)。這用作資料包關聯流的一致雜湊。雜湊要麼由硬體提供,要麼將在堆疊中計算。能夠勝任的硬體可以在資料包的接收描述符中傳遞雜湊;這通常是用於 RSS 的相同雜湊(例如,計算出的 Toeplitz 雜湊)。雜湊儲存在 skb->hash 中,並可以在堆疊中的其他地方用作資料包流的雜湊。
每個接收硬體佇列都有一個與其關聯的 CPU 列表,RPS 可以將資料包排隊到這些 CPU 以進行處理。對於每個接收到的資料包,從流雜湊模列表大小計算出列表中的索引。索引的 CPU 是處理資料包的目標,並且資料包被排隊到該 CPU 的積壓佇列的尾部。在下半部分例程結束時,將 IPI 傳送到任何已將資料包排隊到其積壓佇列的 CPU。 IPI 喚醒遠端 CPU 上的積壓處理,然後處理任何排隊的資料包到網路堆疊。
RPS 配置¶
RPS 需要一個使用 CONFIG_RPS kconfig 符號編譯的核心(預設情況下對於 SMP)。即使編譯後,RPS 仍然處於停用狀態,直到明確配置為止。可以使用 sysfs 檔案條目為每個接收佇列配置 RPS 可以轉發流量到的 CPU 列表
/sys/class/net/<dev>/queues/rx-<n>/rps_cpus
此檔案實現了一個 CPU 的點陣圖。當它為零(預設值)時,RPS 被停用,在這種情況下,資料包在中斷 CPU 上處理。SMP IRQ 親和性解釋瞭如何將 CPU 分配給點陣圖。
建議的配置¶
對於單佇列裝置,典型的 RPS 配置是將 rps_cpus 設定為與中斷 CPU 相同的記憶體域中的 CPU。如果 NUMA 區域性性不是問題,那麼這也可能是系統中的所有 CPU。在高中斷率下,最好將中斷 CPU 從對映中排除,因為它已經執行了大量工作。
對於多佇列系統,如果配置了 RSS,以便將硬體接收佇列對映到每個 CPU,那麼 RPS 可能是冗餘和不必要的。如果硬體佇列的數量少於 CPU 的數量,那麼如果每個佇列的 rps_cpus 與該佇列的中斷 CPU 共享相同的記憶體域,則 RPS 可能會受益。
RPS 流限制¶
RPS 在不引入重新排序的情況下跨 CPU 擴充套件核心接收處理。將來自同一流的所有資料包傳送到同一 CPU 的權衡是,如果流的資料包速率不同,則 CPU 負載會失衡。在極端情況下,單個流會控制流量。尤其是在具有許多併發連線的常見伺服器工作負載上,這種行為表明存在問題,例如配置錯誤或欺騙源拒絕服務攻擊。
流限制是一項可選的 RPS 功能,它透過在來自小流的資料包之前稍微丟棄來自大流的資料包來優先處理 CPU 爭用期間的小流。它僅在 RPS 或 RFS 目標 CPU 接近飽和時才處於活動狀態。一旦 CPU 的輸入資料包佇列超過最大佇列長度的一半(如 sysctl net.core.netdev_max_backlog 所設定的),核心就會在最後 256 個數據包上啟動每個流的資料包計數。如果一個流在新資料包到達時超過了這些資料包的設定比率(預設情況下,為一半),那麼新資料包將被丟棄。來自其他流的資料包只有在輸入資料包佇列達到 netdev_max_backlog 時才會被丟棄。當輸入資料包佇列長度低於閾值時,不會丟棄任何資料包,因此流限制不會完全切斷連線:即使是大型流也保持連線性。
介面¶
預設情況下,流限制是編譯的(CONFIG_NET_FLOW_LIMIT),但未啟用。它是為每個 CPU 獨立實現的(以避免鎖定和快取爭用),並透過在 sysctl net.core.flow_limit_cpu_bitmap 中設定相關位來按 CPU 切換。從 procfs 呼叫時,它會公開與 rps_cpus 相同的 CPU 點陣圖介面(參見上文)
/proc/sys/net/core/flow_limit_cpu_bitmap
每個流的速率是透過將每個資料包雜湊到雜湊表儲存桶中並遞增每個儲存桶的計數器來計算的。雜湊函式與在 RPS 中選擇 CPU 的雜湊函式相同,但由於儲存桶的數量可能遠大於 CPU 的數量,因此流限制可以更細粒度地識別大型流並減少誤報。預設表有 4096 個儲存桶。可以透過 sysctl 修改此值
net.core.flow_limit_table_len
僅在分配新表時才諮詢該值。修改它不會更新活動表。
建議的配置¶
流限制在具有許多併發連線的系統上非常有用,其中單個連線佔用 CPU 的 50% 表明存在問題。在這種環境中,在處理網路 rx 中斷的所有 CPU 上啟用該功能(如 /proc/irq/N/smp_affinity 中設定的)。
該功能取決於輸入資料包佇列長度超過流限制閾值 (50%) + 流歷史記錄長度 (256)。在實驗中,將 net.core.netdev_max_backlog 設定為 1000 或 10000 表現良好。
RFS:接收流引導¶
雖然 RPS 僅根據雜湊引導資料包,因此通常提供良好的負載分配,但它沒有考慮應用程式區域性性。這是透過接收流引導 (RFS) 完成的。RFS 的目標是透過將資料包的核心處理引導到消耗資料包的應用程式執行緒正在執行的 CPU,來提高資料快取命中率。RFS 依賴於相同的 RPS 機制將資料包排隊到另一個 CPU 的積壓中並喚醒該 CPU。
在 RFS 中,資料包不會直接透過其雜湊值轉發,而是使用雜湊作為流查詢表的索引。此表將流對映到正在處理這些流的 CPU。流雜湊(參見上面的 RPS 部分)用於計算此表的索引。每個條目中記錄的 CPU 是上次處理該流的 CPU。如果一個條目沒有儲存有效的 CPU,那麼對映到該條目的資料包將使用普通的 RPS 進行引導。多個表條目可能指向同一個 CPU。實際上,對於許多流和少量 CPU,單個應用程式執行緒很可能處理具有許多不同流雜湊的流。
rps_sock_flow_table 是一個全域性流表,其中包含流的期望 CPU:當前正在使用者空間中處理流的 CPU。每個表值都是一個 CPU 索引,該索引在呼叫 recvmsg 和 sendmsg 期間更新(具體來說,inet_recvmsg()、inet_sendmsg() 和 tcp_splice_read())。
當排程程式線上程在舊 CPU 上有未完成的接收資料包時將該執行緒移動到新的 CPU 時,資料包可能會亂序到達。為了避免這種情況,RFS 使用第二個流表來跟蹤每個流的未完成資料包:rps_dev_flow_table 是一個特定於每個裝置的每個硬體接收佇列的表。每個表值儲存一個 CPU 索引和一個計數器。CPU 索引表示當前 CPU,該流的資料包被排隊到該 CPU 以進行進一步的核心處理。理想情況下,核心和使用者空間處理發生在同一個 CPU 上,因此兩個表中的 CPU 索引相同。如果排程程式最近遷移了使用者空間執行緒,而核心仍然有資料包排隊到舊 CPU 上的核心處理,則這可能是不正確的。
rps_dev_flow_table 值中的計數器記錄了此流中的資料包上次排隊時當前 CPU 的積壓的長度。每個積壓佇列都有一個頭計數器,該計數器在出隊時遞增。尾計數器計算為頭計數器 + 佇列長度。換句話說,rps_dev_flow[i] 中的計數器記錄了已排隊到當前為流 i 指定的 CPU 上的流 i 中的最後一個元素(當然,條目 i 實際上是由雜湊選擇的,並且多個流可能雜湊到同一個條目 i)。
現在是避免亂序資料包的技巧:當從 get_rps_cpu() 選擇用於資料包處理的 CPU 時,將比較 rps_sock_flow 表和資料包接收佇列的 rps_dev_flow 表。如果流的期望 CPU(在 rps_sock_flow 表中找到)與當前 CPU(在 rps_dev_flow 表中找到)匹配,則資料包被排隊到該 CPU 的積壓中。如果它們不同,則如果滿足以下條件之一,則將當前 CPU 更新為與期望 CPU 匹配
當前 CPU 的佇列頭計數器 >= rps_dev_flow[i] 中記錄的尾計數器值
當前 CPU 未設定(>= nr_cpu_ids)
當前 CPU 處於離線狀態
在此檢查之後,資料包被髮送到(可能更新的)當前 CPU。這些規則旨在確保流僅在舊 CPU 上沒有未完成的資料包時才移動到新的 CPU,因為未完成的資料包可能會比那些即將要在新 CPU 上處理的資料包晚到達。
RFS 配置¶
只有在啟用了 kconfig 符號 CONFIG_RPS(預設情況下對於 SMP)時,RFS 才可用。該功能在顯式配置之前保持停用狀態。全域性流表中的條目數透過以下方式設定
/proc/sys/net/core/rps_sock_flow_entries
每個佇列的流表中的條目數透過以下方式設定
/sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt
建議的配置¶
在為接收佇列啟用 RFS 之前,需要設定這兩個值。兩個值都向上舍入到最接近的 2 的冪。建議的流計數取決於任何給定時間預期的活動連線數,這可能遠小於開啟的連線數。我們發現 rps_sock_flow_entries 的值 32768 在中等負載的伺服器上執行良好。
對於單佇列裝置,單佇列的 rps_flow_cnt 值通常配置為與 rps_sock_flow_entries 相同的值。對於多佇列裝置,每個佇列的 rps_flow_cnt 可以配置為 rps_sock_flow_entries / N,其中 N 是佇列的數量。因此,例如,如果 rps_sock_flow_entries 設定為 32768 並且配置了 16 個接收佇列,則每個佇列的 rps_flow_cnt 可以配置為 2048。
加速 RFS¶
加速 RFS 之於 RFS 就像 RSS 之於 RPS:一種硬體加速負載平衡機制,它使用軟狀態根據消耗每個流的資料包的應用程式執行緒的執行位置來引導流。加速 RFS 的效能應優於 RFS,因為資料包直接傳送到本地於消耗資料的執行緒的 CPU。目標 CPU 要麼是應用程式執行的同一個 CPU,要麼至少是在快取層次結構中本地於應用程式執行緒 CPU 的 CPU。
為了啟用加速 RFS,網路堆疊呼叫 ndo_rx_flow_steer 驅動程式函式來通訊與特定流匹配的資料包的期望硬體佇列。每次更新 rps_dev_flow_table 中的流條目時,網路堆疊都會自動呼叫此函式。然後,驅動程式使用裝置特定方法來程式設計網絡卡以引導資料包。
流的硬體佇列是從 rps_dev_flow_table 中記錄的 CPU 派生的。堆疊查詢 CPU 到硬體佇列的對映,該對映由網絡卡驅動程式維護。這是 /proc/interrupts 顯示的 IRQ 親和性表的自動生成的反向對映。驅動程式可以使用 cpu_rmap(“CPU 親和性反向對映”)核心庫中的函式來填充對映。或者,驅動程式可以透過呼叫 netif_enable_cpu_rmap() 將 cpu_rmap 管理委託給核心。對於每個 CPU,對映中相應的佇列設定為其處理 CPU 在快取區域性性中最近的佇列。
加速 RFS 配置¶
僅當使用 CONFIG_RFS_ACCEL 編譯核心並且 NIC 裝置和驅動程式提供支援時,加速 RFS 才可用。它還要求透過 ethtool 啟用 ntuple 過濾。CPU 到佇列的對映是從為每個接收佇列配置的 IRQ 親和性自動推匯出來的,因此不應需要額外的配置。
建議的配置¶
只要想要使用 RFS 並且 NIC 支援硬體加速,就應該啟用此技術。
XPS:傳送資料包引導¶
傳送資料包引導是一種用於在多佇列裝置上傳送資料包時智慧選擇要使用的傳送佇列的機制。這可以透過記錄兩種對映來實現,要麼是 CPU 到硬體佇列的對映,要麼是接收佇列到硬體傳送佇列的對映。
使用 CPU 對映的 XPS
此對映的目標通常是將佇列專門分配給 CPU 子集,其中這些佇列的傳送完成在集合中的 CPU 上處理。這種選擇提供了兩個好處。首先,裝置佇列鎖上的爭用顯著減少,因為更少的 CPU 爭用同一佇列(如果每個 CPU 都有自己的傳送佇列,則可以完全消除爭用)。其次,傳送完成時的快取未命中率降低,特別是對於儲存 sk_buff 結構的資料快取行。
使用接收佇列對映的 XPS
此對映用於根據管理員設定的接收佇列對映配置來選擇傳送佇列。一組接收佇列可以對映到一組傳送佇列(多對多),儘管常見的用例是 1:1 對映。這將能夠在傳送和接收時在同一佇列關聯上傳送資料包。這對於繁忙輪詢多執行緒工作負載非常有用,在這種工作負載中,將給定的 CPU 與給定的應用程式執行緒相關聯存在挑戰。應用程式執行緒未固定到 CPU,並且每個執行緒處理在單個佇列上接收的資料包。在此模型中,在與關聯的接收佇列相對應的同一傳送佇列上傳送資料包在保持低 CPU 開銷方面具有優勢。傳送完成工作被鎖定到給定的應用程式正在輪詢的同一佇列關聯中。這避免了在另一個 CPU 上觸發中斷的開銷。當應用程式在繁忙輪詢期間清理資料包時,傳送完成可以與同一執行緒上下文中的資料包一起處理,因此可以降低延遲。
透過設定可以使用該佇列進行傳送的 CPU/接收佇列的點陣圖來為每個傳送佇列配置 XPS。反向對映,從 CPU 到傳送佇列或從接收佇列到傳送佇列,為每個網路裝置計算和維護。在傳送流中的第一個資料包時,呼叫函式 get_xps_queue() 來選擇佇列。此函式使用套接字連線的接收佇列 ID 來匹配接收佇列到傳送佇列查詢表。或者,此函式還可以使用正在執行的 CPU 的 ID 作為 CPU 到佇列查詢表的鍵。如果 ID 與單個佇列匹配,則使用該佇列進行傳送。如果多個佇列匹配,則透過使用流雜湊來計算集合中的索引來選擇一個佇列。當基於接收佇列對映選擇傳送佇列時,不會針對接收裝置驗證傳送裝置,因為它需要在資料路徑中進行昂貴的查詢操作。
為傳送特定流選擇的佇列儲存在流的相應套接字結構中(例如,TCP 連線)。此傳送佇列用於在流上傳送後續資料包,以防止亂序 (ooo) 資料包。此選擇還在流中的所有資料包上分攤了呼叫 get_xps_queues() 的成本。為了避免 ooo 資料包,只有在為流中的資料包設定了 skb->ooo_okay 時,才能隨後更改流的佇列。此標誌指示流中沒有未完成的資料包,因此傳送佇列可以更改而不會產生亂序資料包的風險。傳輸層負責適當地設定 ooo_okay。例如,TCP 在連線的所有資料都已確認時設定該標誌。
XPS 配置¶
只有在啟用了 kconfig 符號 CONFIG_XPS(預設情況下對於 SMP)時,XPS 才可用。如果已編譯,則在裝置初始化時是否配置以及如何配置 XPS 取決於驅動程式。可以使用 sysfs 檢查和配置 CPU/接收佇列到傳送佇列的對映
對於基於 CPU 對映的選擇
/sys/class/net/<dev>/queues/tx-<n>/xps_cpus
對於基於接收佇列對映的選擇
/sys/class/net/<dev>/queues/tx-<n>/xps_rxqs
建議的配置¶
對於具有單個傳輸佇列的網路裝置,XPS 配置無效,因為在這種情況下沒有選擇。在多佇列系統中,最好將 XPS 配置為使每個 CPU 對映到一個佇列。如果系統的佇列數與 CPU 數相同,則每個佇列也可以對映到一個 CPU,從而導致不存在爭用的獨佔配對。如果佇列數少於 CPU 數,則共享給定佇列的最佳 CPU 可能是那些與處理該佇列的傳送完成(傳送中斷)的 CPU 共享快取的 CPU。
對於基於接收佇列的傳送佇列選擇,必須顯式配置 XPS,將接收佇列對映到傳送佇列。如果接收佇列對映的使用者配置不適用,則傳送佇列基於 CPU 對映選擇。
每個 TX 佇列速率限制¶
這些是由 HW 實現的速率限制機制,目前支援 max-rate 屬性,方法是將 Mbps 值設定為
/sys/class/net/<dev>/queues/tx-<n>/tx_maxrate
值為零表示停用,這是預設值。
更多資訊¶
RPS 和 RFS 在核心 2.6.35 中引入。 XPS 已合併到 2.6.38 中。原始補丁由 Tom Herbert 提交 (therbert@google.com)
加速 RFS 在 2.6.35 中引入。原始補丁由 Ben Hutchings 提交 (bwh@kernel.org)
作者
Tom Herbert (therbert@google.com)
Willem de Bruijn (willemb@google.com)