想看能不能完整梳理一下收消息過程。從 NIC 收數據開始,到觸發軟中斷,交付數據包到 IP 層再經由路由機制到 TCP 層,最終交付用戶進程。會盡力介紹收消息過程當中的各類配置信息,以及各類監控數據。知道了收消息的完整過程,瞭解了各類配置,明白了各類監控數據後纔有可能在從此的工做中作優化配置。linux
全部參考內容會列在這個系列最後一篇文章中。git
Ring Buffer 相關的收消息過程大體以下:github
圖片來自參考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 中批量拉取收到的數據。主要事件及其順序以下:數據結構
poll
函數,用於後續從 Ring Buffer 拉取收到的數據poll
函數獲取收到的 Packetpoll
完全部數據以前不會再有新的 IRQ從上面的描述能夠看出來還缺一些東西,Ring Buffer 上的數據被 poll
走以後是怎麼交付上層網絡棧繼續處理的呢?以及被消耗掉的 sk_buff 是怎麼被從新分配從新放入 Ring Buffer 的呢?負載均衡
這兩個工做都在 poll
中完成,上面說過 poll
是個 driver 實現的函數,因此每一個 driver 實現可能都不相同。但 poll
的工做基本是一致的就是:electron
若是拿 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 在這裏無窮循環下去,要讓別的事情也可以被執行。循環內作的事情以下:
看到 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 相關的更多細節。
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
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。
有的 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,具體含義等須要用到的時候查吧。
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
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 大小:
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
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 可能不同。
分配數據包的時候是按照數據包內的某個字段來進行的,這個字段能進行調整。
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 看其含義,還有不少別的配置值。
經過 /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 後就均勻多了。
/proc/interrupts
能看到每一個 CPU 的 IRQ 統計。通常就是看看 NIC 有沒有支持 multiqueue 以及 NAPI 的 IRQ 合併機制是否生效。看看 IRQ 是否是增加的很快。