Linux 網絡協議棧收消息過程-Ring Buffer

想看能不能完整梳理一下收消息過程。從 NIC 收數據開始,到觸發軟中斷,交付數據包到 IP 層再經由路由機制到 TCP 層,最終交付用戶進程。會盡力介紹收消息過程當中的各類配置信息,以及各類監控數據。知道了收消息的完整過程,瞭解了各類配置,明白了各類監控數據後纔有可能在從此的工做中作優化配置。linux

全部參考內容會列在這個系列最後一篇文章中。git

Ring Buffer 相關的收消息過程大體以下:github

图片来自参考1,对 raise softirq 的函数名做了修改,改为了 napi_schedule

圖片來自參考1,對 raise softirq 的函數名作了修改,改成了 napi_schedulec#

NIC (network interface card) 在系統啓動過程當中會向系統註冊本身的各類信息,系統會分配 Ring Buffer 隊列也會分配一塊專門的內核內存區域給 NIC 用於存放傳輸上來的數據包。struct sk_buff 是專門存放各類網絡傳輸數據包的內存接口,在收到數據存放到 NIC 專用內核內存區域後,sk_buff 內有個 data 指針會指向這塊內存。Ring Buffer 隊列內存放的是一個個 Packet Descriptor ,其有兩種狀態: ready 和 used 。初始時 Descriptor 是空的,指向一個空的 sk_buff,處在 ready 狀態。當有數據時,DMA 負責從 NIC 取數據,並在 Ring Buffer 上按順序找到下一個 ready 的 Descriptor,將數據存入該 Descriptor 指向的 sk_buff 中,並標記槽爲 used。由於是按順序找 ready 的槽,因此 Ring Buffer 是個 FIFO 的隊列。api

當 DMA 讀完數據以後,NIC 會觸發一個 IRQ 讓 CPU 去處理收到的數據。由於每次觸發 IRQ 後 CPU 都要花費時間去處理 Interrupt Handler,若是 NIC 每收到一個 Packet 都觸發一個 IRQ 會致使 CPU 花費大量的時間在處理 Interrupt Handler,處理完後又只能從 Ring Buffer 中拿出一個 Packet,雖然 Interrupt Handler 執行時間很短,但這麼作也很是低效,並會給 CPU 帶去不少負擔。因此目前都是採用一個叫作 New API(NAPI)的機制,去對 IRQ 作合併以減小 IRQ 次數。網絡

接下來介紹一下 NAPI 是怎麼作到 IRQ 合併的。它主要是讓 NIC 的 driver 能註冊一個 poll 函數,以後 NAPI 的 subsystem 能經過 poll 函數去從 Ring Buffer 中批量拉取收到的數據。主要事件及其順序以下:數據結構

  1. NIC driver 初始化時向 Kernel 註冊 poll 函數,用於後續從 Ring Buffer 拉取收到的數據
  2. driver 註冊開啓 NAPI,這個機制默認是關閉的,只有支持 NAPI 的 driver 纔會去開啓
  3. 收到數據後 NIC 經過 DMA 將數據存到內存
  4. NIC 觸發一個 IRQ,並觸發 CPU 開始執行 driver 註冊的 Interrupt Handler
  5. driver 的 Interrupt Handler 經過 napi_schedule 函數觸發 softirq (NET_RX_SOFTIRQ) 來喚醒 NAPI subsystem,NET_RX_SOFTIRQ 的 handler 是 net_rx_action 會在另外一個線程中被執行,在其中會調用 driver 註冊的 poll 函數獲取收到的 Packet
  6. driver 會禁用當前 NIC 的 IRQ,從而能在 poll 完全部數據以前不會再有新的 IRQ
  7. 當全部事情作完以後,NAPI subsystem 會被禁用,而且會從新啓用 NIC 的 IRQ
  8. 回到第三步

從上面的描述能夠看出來還缺一些東西,Ring Buffer 上的數據被 poll 走以後是怎麼交付上層網絡棧繼續處理的呢?以及被消耗掉的 sk_buff 是怎麼被從新分配從新放入 Ring Buffer 的呢?負載均衡

這兩個工做都在 poll 中完成,上面說過 poll 是個 driver 實現的函數,因此每一個 driver 實現可能都不相同。但 poll 的工做基本是一致的就是:electron

  1. 從 Ring Buffer 中將收到的 sk_buff 讀取出來
  2. 對 sk_buff 作一些基本檢查,可能會涉及到將幾個 sk_buff 合併由於可能同一個 Frame 被分散放在多個 sk_buff 中
  3. 將 sk_buff 交付上層網絡棧處理
  4. 清理 sk_buff,清理 Ring Buffer 上的 Descriptor 將其指向新分配的 sk_buff 並將狀態設置爲 ready
  5. 更新一些統計數據,好比收到了多少 packet,一共多少字節等

若是拿 intel igb 這個網卡的實現來看,其 poll 函數在這裏:linux/drivers/net/ethernet/intel/igb/igb_main.c - Elixir - Free Electronstcp

首先是看到有 tx.ring 和 rx.ring,說明收發消息都會走到這裏。發消息先無論,先看收消息,收消息走的是 igb_clean_rx_irq。收完消息後執行 napi_complete_done 退出 polling 模式,並開啓 NIC 的 IRQ。從而咱們知道大部分工做是在 igb_clean_rx_irq 中完成的,其實現大體上仍是比較清晰的,就是上面描述的幾步。裏面有個 while 循環經過 buget 控制,從而在 Packet 特別多的時候不要讓 CPU 在這裏無窮循環下去,要讓別的事情也可以被執行。循環內作的事情以下:

  1. 先批量清理已經讀出來的 sk_buff 並分配新的 buffer 從而避免每次讀一個 sk_buff 就清理一個,很低效
  2. 找到 Ring Buffer 上下一個須要被讀取的 Descriptor ,並檢查描述符狀態是否正常
  3. 根據 Descriptor 找到 sk_buff 讀出來
  4. 檢查是不是 End of packet,是的話說明 sk_buff 內有 Frame 的所有內容,不是的話說明 Frame 數據比 sk_buff 大,須要再讀一個 sk_buff,將兩個 sk_buff 數據合併起來
  5. 經過 Frame 的 Header 檢查 Frame 數據完整性,是否正確之類的
  6. 記錄 sk_buff 的長度,讀了多少數據
  7. 設置 Hash、checksum、timestamp、VLAN id 等信息,這些信息是硬件提供的。
  8. 經過 napi_gro_receive 將 sk_buff 交付上層網絡棧
  9. 更新一堆統計數據
  10. 回到 1,若是沒數據或者 budget 不夠就退出循環

看到 budget 會影響到 CPU 執行 poll 的時間,budget 越大當數據包特別多的時候能夠提升 CPU 利用率並減小數據包的延遲。可是 CPU 時間都花在這裏會影響別的任務的執行。

budget 默認 300,能夠調整
sysctl -w net.core.netdev_budget=600

napi_gro_receive會涉及到 GRO 機制,稍後再說,大體上就是會對多個數據包作聚合,napi_gro_receive 最終是將處理好的 sk_buff 經過調用 netif_receive_skb,將數據包送至上層網絡棧。執行完 GRO 以後,基本能夠認爲數據包正式離開 Ring Buffer,進入下一個階段了。在記錄下一階段的處理以前,補充一下收消息階段 Ring Buffer 相關的更多細節。

Generic Receive Offloading(GRO)

GRO 是 Large receive offload 的一個實現。網絡上大部分 MTU 都是 1500 字節,開啓 Jumbo Frame 後能到 9000 字節,若是發送的數據超過 MTU 就須要切割成多個數據包。LRO 就是在收到多個數據包的時候將同一個 Flow 的多個數據包按照必定的規則合併起來交給上層處理,這樣就能減小上層須要處理的數據包數量。

不少 LRO 機制是在 NIC 上實現的,沒有實現 LRO 的 NIC 就少了上述合併數據包的能力。而 GRO 是 LRO 在軟件上的實現,從而能讓全部 NIC 都支持這個功能。

napi_gro_receive 就是在收到數據包的時候合併多個數據包用的,若是收到的數據包須要被合併,napi_gro_receive 會很快返回。當合並完成後會調用 napi_skb_finish ,將由於數據包合併而再也不用到的數據結構釋放。最終會調用到 netif_receive_skb 將數據包交到上層網絡棧繼續處理。netif_receive_skb 上面說過,就是數據包從 Ring Buffer 出來後到上層網絡棧的入口。

能夠經過 ethtool 查看和設置 GRO:

查看 GRO
ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on
設置開啓 GRO
ethtool -K eth0 gro on

多 CPU 下的 Ring Buffer 處理 (Receive Side Scaling)

NIC 收到數據的時候產生的 IRQ 只可能被一個 CPU 處理,從而只有一個 CPU 會執行 napi_schedule 來觸發 softirq,觸發的這個 softirq 的 handler 也仍是會在這個產生 softIRQ 的 CPU 上執行。因此 driver 的 poll 函數也是在最開始處理 NIC 發出 IRQ 的那個 CPU 上執行。因而一個 Ring Buffer 上同一個時刻只有一個 CPU 在拉取數據。

從上面描述能看出來分配給 Ring Buffer 的空間是有限的,當收到的數據包速率大於單個 CPU 處理速度的時候 Ring Buffer 可能被佔滿,佔滿以後再來的新數據包會被自動丟棄。而如今機器都是有多個 CPU,同時只有一個 CPU 去處理 Ring Buffer 數據會很低效,這個時候就產生了叫作 Receive Side Scaling(RSS) 或者叫作 multiqueue 的機制來處理這個問題。WIKI 對 RSS 的介紹挺好的,簡潔幹練能夠看看: Network interface controller - Wikipedia

簡單說就是如今支持 RSS 的網卡內部會有多個 Ring Buffer,NIC 收到 Frame 的時候能經過 Hash Function 來決定 Frame 該放在哪一個 Ring Buffer 上,觸發的 IRQ 也能夠經過操做系統或者手動配置 IRQ affinity 將 IRQ 分配到多個 CPU 上。這樣 IRQ 能被不一樣的 CPU 處理,從而作到 Ring Buffer 上的數據也能被不一樣的 CPU 處理,從而提升數據的並行處理能力。

RSS 除了會影響到 NIC 將 IRQ 發到哪一個 CPU 以外,不會影響別的邏輯了。收消息過程跟以前描述的是同樣的。

若是支持 RSS 的話,NIC 會爲每一個隊列分配一個 IRQ,經過 /proc/interrupts 能進行查看。你能夠經過配置 IRQ affinity 指定 IRQ 由哪一個 CPU 來處理中斷。先經過 /proc/interrupts 找到 IRQ 號以後,將但願綁定的 CPU 號寫入 /proc/irq/IRQ_NUMBER/smp_affinity,寫入的是 16 進制的 bit mask。好比看到隊列 rx_0 對應的中斷號是 41 那就執行:

echo 6 > /proc/irq/41/smp_affinity
6 表示的是 CPU2 和 CPU1

0 號 CPU 的掩碼是 0x1 (0001),1 號 CPU 掩碼是 0x2 (0010),2 號 CPU 掩碼是 0x4 (0100),3 號 CPU 掩碼是 0x8 (1000) 依此類推。

另外須要注意的是設置 smp_affinity 的話不能開啓 irqbalance 或者須要爲 irqbalance 設置 –banirq 列表,將設置了 smp_affinity 的 IRQ 排除。否則 irqbalance 機制運做時會忽略你設置的 IRQ affinity 配置。

Receive Packet Steering(RPS) 是在 NIC 不支持 RSS 時候在軟件中實現 RSS 相似功能的機制。其好處就是對 NIC 沒有要求,任何 NIC 都能支持 RPS,但缺點是 NIC 收到數據後 DMA 將數據存入的仍是一個 Ring Buffer,NIC 觸發 IRQ 仍是發到一個 CPU,仍是由這一個 CPU 調用 driver 的 poll 來將 Ring Buffer 的數據取出來。RPS 是在單個 CPU 將數據從 Ring Buffer 取出來以後纔開始起做用,它會爲每一個 Packet 計算 Hash 以後將 Packet 發到對應 CPU 的 backlog 中,並經過 Inter-processor Interrupt(IPI) 告知目標 CPU 來處理 backlog。後續 Packet 的處理流程就由這個目標 CPU 來完成。從而實現將負載分到多個 CPU 的目的。

RPS 默認是關閉的,當機器有多個 CPU 而且經過 softirqs 的統計 /proc/softirqs 發現 NET_RX 在 CPU 上分佈不均勻或者發現網卡不支持 mutiqueue 時,就能夠考慮開啓 RPS。開啓 RPS 須要調整 /sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus 的值。好比執行:

echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

表示的含義是處理網卡 eth0 的 rx-0 隊列的 CPU 數設置爲 f 。即設置有 15 個 CPU 來處理 rx-0 這個隊列的數據,若是你的 CPU 數沒有這麼多就會默認使用全部 CPU 。甚至有人爲了方便都是直接將 echo fff > /sys/class/net/eth0/queues/rx-0/rps_cpus 寫到腳本里,這樣基本能覆蓋全部類型的機器,無論機器 CPU 數有多少,都能覆蓋到。從而就能讓這個腳本在任意機器都能執行。

注意:若是 NIC 不支持 mutiqueue,RPS 不是徹底不用思考就能打開的,由於其開啓以後會加劇全部 CPU 的負擔,在一些場景下好比 CPU 密集型應用上並不必定能帶來好處。因此得測試一下。

Receive Flow Steering(RFS) 通常和 RPS 配合一塊兒工做。RPS 是將收到的 packet 發配到不一樣的 CPU 以實現負載均衡,可是可能同一個 Flow 的數據包正在被 CPU1 處理,但下一個數據包被髮到 CPU2,會下降 CPU cache hit 比率而且會讓數據包要從 CPU1 發到 CPU2 上。RFS 就是保證同一個 flow 的 packet 都會被路由到正在處理當前 Flow 數據的 CPU,從而提升 CPU cache 比率。這篇文章 把 RFS 機制介紹的挺好的。基本上就是收到數據後根據數據的一些信息作個 Hash 在這個 table 的 entry 中找到當前正在處理這個 flow 的 CPU 信息,從而將數據發給這個正在處理該 Flow 數據的 CPU 上,從而作到提升 CPU cache hit 率,避免數據在不一樣 CPU 之間拷貝。固然還有不少細節,請看上面連接。

RFS 默認是關閉的,必須主動配置才能生效。正常來講開啓了 RPS 都要再開啓 RFS,以獲取更好的性能。這篇文章也有說該怎麼去開啓 RFS 以及推薦的配置值。一個是要配置 rps_sock_flow_entries

sysctl -w net.core.rps_sock_flow_entries=32768

這個值依賴於系統指望的活躍鏈接數,注意是同一時間活躍的鏈接數,這個鏈接數正常來講會大大小於系統能承載的最大鏈接數,由於大部分鏈接不會同時活躍。該值建議是 32768,能覆蓋大多數狀況,每一個活躍鏈接會分配一個 entry。除了這個以外還要配置 rps_flow_cnt,這個值是每一個隊列負責的 flow 最大數量,若是隻有一個隊列,則 rps_flow_cnt 通常是跟 rps_sock_flow_entries 的值一致,可是有多個隊列的時候 rps_flow_cnt 值就是 rps_sock_flow_entries / N, N 是隊列數量。

echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

Accelerated Receive Flow Steering (aRFS) 相似 RFS 只是由硬件協助完成這個工做。aRFS 對於 RFS 就和 RSS 對於 RPS 同樣,就是把 CPU 的工做挪到了硬件來作,從而不用浪費 CPU 時間,直接由 NIC 完成 Hash 值計算並將數據發到目標 CPU,因此快一點。NIC 必須暴露出來一個 ndo_rx_flow_steer 的函數用來實現 aRFS。

adaptive RX/TX IRQ coalescing

有的 NIC 支持這個功能,用來動態的將 IRQ 進行合併,以作到在數據包少的時候減小數據包的延遲,在數據包多的時候提升吞吐量。查看方法:

ethtool -c eth1
Coalesce parameters for eth1:
Adaptive RX: off  TX: off
stats-block-usecs: 0
.....

開啓 RX 隊列的 adaptive coalescing 執行:

ethtool -C eth0 adaptive-rx on

而且有四個值須要設置:rx-usecs、rx-frames、rx-usecs-irq、rx-frames-irq,具體含義等須要用到的時候查吧。

Ring Buffer 相關監控及配置

收到數據包統計

ethtool -S eh0
NIC statistics:
     rx_packets: 792819304215
     tx_packets: 778772164692
     rx_bytes: 172322607593396
     tx_bytes: 201132602650411
     rx_broadcast: 15118616
     tx_broadcast: 2755615
     rx_multicast: 0
     tx_multicast: 10

RX 就是收到數據,TX 是發出數據。還會展現 NIC 每一個隊列收發消息狀況。其中比較關鍵的是帶有 drop 字樣的統計和 fifo_errors 的統計 :

tx_dropped: 0
rx_queue_0_drops: 93
rx_queue_1_drops: 874
....
rx_fifo_errors: 2142
tx_fifo_errors: 0

看到發送隊列和接收隊列 drop 的數據包數量顯示在這裏。而且全部 queue_drops 加起來等於 rx_fifo_errors。因此整體上能經過 rx_fifo_errors 看到 Ring Buffer 上是否有丟包。若是有的話一方面是看是否須要調整一下每一個隊列數據的分配,或者是否要加大 Ring Buffer 的大小。

/proc/net/dev是另外一個數據包相關統計,不過這個統計比較難看:

cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo: 14472296365706 10519818839    0    0    0     0          0         0 14472296365706 10519818839    0    0    0     0       0          0
  eth1: 164650683906345 785024598362    0    0 2142     0          0         0 183711288087530 704887351967    0    0    0     0       0          0

調整 Ring Buffer 隊列數量

ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:             0
TX:             0
Other:          1
Combined:       8
Current hardware settings:
RX:             0
TX:             0
Other:          1
Combined:       8

看的是 Combined 這一欄是隊列數量。Combined 按說明寫的是多功能隊列,猜測是能用做 RX 隊列也能當作 TX 隊列,但數量一共是 8 個?

若是不支持 mutiqueue 的話上面執行下來會是:

Channel parameters for eth0:
Cannot get device channel parameters
: Operation not supported

看到上面 Ring Buffer 數量有 maximums 和 current settings,因此能本身設置 Ring Buffer 數量,但最大不能超過 maximus 值:

sudo ethtool -L eth0 combined 8

若是支持對特定類型 RX 或 TX 設置隊列數量的話能夠執行:

sudo ethtool -L eth0 rx 8

須要注意的是,ethtool 的設置操做可能都要重啓一下才能生效。

調整 Ring Buffer 隊列大小

先查看當前 Ring Buffer 大小:

ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:   4096
RX Mini:  0
RX Jumbo: 0
TX:   4096
Current hardware settings:
RX:   512
RX Mini:  0
RX Jumbo: 0
TX:   512

看到 RX 和 TX 最大是 4096,當前值爲 512。隊列越大丟包的可能越小,但數據延遲會增長

設置 RX 隊列大小:

ethtool -G eth0 rx 4096

調整 Ring Buffer 隊列的權重

NIC 若是支持 mutiqueue 的話 NIC 會根據一個 Hash 函數對收到的數據包進行分發。能調整不一樣隊列的權重,用於分配數據。

ethtool -x eth0
RX flow hash indirection table for eth0 with 8 RX ring(s):
    0:      0     0     0     0     0     0     0     0
    8:      0     0     0     0     0     0     0     0
   16:      1     1     1     1     1     1     1     1
   ......
   64:      4     4     4     4     4     4     4     4
   72:      4     4     4     4     4     4     4     4
   80:      5     5     5     5     5     5     5     5
   ......
  120:      7     7     7     7     7     7     7     7

個人 NIC 一共有 8 個隊列,一個有 128 個不一樣的 Hash 值,上面就是列出了每一個 Hash 值對應的隊列是什麼。最左側 0 8 16 是爲了能讓你快速的找到某個具體的 Hash 值。好比 Hash 值是 76 的話咱們能當即找到 72 那一行:」72: 4 4 4 4 4 4 4 4」,從左到右第一個是 72 數第 5 個就是 76 這個 Hash 值對應的隊列是 4 。

ethtool -X eth0 weight 6 2 8 5 10 7 1 5

設置 8 個隊列的權重。加起來不能超過 128 。128 是 indirection table 大小,每一個 NIC 可能不同。

更改 Ring Buffer Hash Field

分配數據包的時候是按照數據包內的某個字段來進行的,這個字段能進行調整。

ethtool -n eth0 rx-flow-hash tcp4
TCP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
L4 bytes 0 & 1 [TCP/UDP src port]
L4 bytes 2 & 3 [TCP/UDP dst port]

查看 tcp4 的 Hash 字段。

也能夠設置 Hash 字段:

ethtool -N eth0 rx-flow-hash udp4 sdfn

sdfn 須要查看 ethtool 看其含義,還有不少別的配置值。

softirq 數統計

經過 /proc/softirqs 能看到每一個 CPU 上 softirq 數量統計:

cat /proc/softirqs
                    CPU0       CPU1       
          HI:          1          0
       TIMER: 1650579324 3521734270
      NET_TX:   10282064   10655064
      NET_RX: 3618725935       2446
       BLOCK:          0          0
BLOCK_IOPOLL:          0          0
     TASKLET:      47013      41496
       SCHED: 1706483540 1003457088
     HRTIMER:    1698047   11604871
         RCU: 4218377992 3049934909

看到 NET_RX 就是收消息時候觸發的 softirq,通常看這個統計是爲了看看 softirq 在每一個 CPU 上分佈是否均勻,不均勻的話可能就須要作一些調整。好比上面看到 CPU0 和 CPU1 兩個差距很大,緣由是這個機器的 NIC 不支持 RSS,沒有多個 Ring Buffer。開啓 RPS 後就均勻多了。

IRQ 統計

/proc/interrupts 能看到每一個 CPU 的 IRQ 統計。通常就是看看 NIC 有沒有支持 multiqueue 以及 NAPI 的 IRQ 合併機制是否生效。看看 IRQ 是否是增加的很快。

相關文章
相關標籤/搜索