Linux網絡數據包的揭祕以及常見的調優方式總結
可直接點擊上方藍字
(網易遊戲運維平臺)
關注我們,獲一手遊戲運維方案
lott
網易遊戲業務 SRE, 專注於業務運維的質量和效率 , 喜歡研究 Linux 系統原理。目前負責《一夢江湖》、《獵魂覺醒》、《非人學園》等產品的運維工作。
總結是進步的階梯、分享是快樂的源泉 , 技術人就是要不斷總結、不斷分享。
作爲業務 SRE,我們所運維的業務,常常以 Linux+TCP/UDP daemon 的形式對外提供服務。SRE 需要對服務器數據包的接收和發送路徑有全面的瞭解,以方便在服務異常時能快速定位問題。
以 tcp 協議爲例,本文將對 Linux 內核網絡數據包接收的路徑進行整理和說明,希望對大家所有幫助。
Linux 數據包接收路徑的整體說明
接收數據包是一個複雜的過程,涉及很多底層的技術細節 , 這裏先做一下大概的說明 :
NIC (network interface card) 在系統啓動過程中會向系統註冊自己的各種信息,系統會分配專門的內存緩衝區,
NIC 接收到數據包之後,就會存放在內存緩衝區,通過硬件中斷通知內核有新的數據包需要處理 .
內核從緩衝區取走 NIC 接收過來的數據,交給 TCP/IP 協議棧處理。
內核的 TCP/IP 協議棧代碼進行處理後,更新協議的各種狀態,然後交給應用程序的 socket buffer。
然後應用程序就可以通過 read() 系統調用,從對應的 socket 文件中,讀取數據。
對內核數據包接收的路徑做一下分層,總體可分爲三層 :
-
網卡層面
-
1.1 網卡接收到數據包
-
1.2 將數據包從網卡硬件轉移到主機內存中 .
內核層面
-
2.1 TCP/IP 協議逐層處理
應用程序層面
-
3.1 應用程序通過 read() 系統調用 , 從 socket buffer 讀取數據
如下圖 :
接下來解釋一下什麼是 NAPI
什麼是 NAPI
系統啓動時會爲網卡分配 Ring Buffer (環形緩衝區 ), Ring Buffer 放的是一個個 Packet Descriptor(數據包描述符),是實際數據包的指針。實際的數據包是存放在另一塊內存區域中(由網卡 Driver 預先申請好),稱爲 sk_buffers, sk_buffers 是可以由 DMA(https://en.wikipedia.org/wiki/DMA) 直接訪問的 .
Ring Buffer 裏的 Packet Descriptor ,有兩種狀態:ready 和 used 。初始時 Descriptor 是空的,指向一個空的 sk_buffer,處在 ready 狀態。當有數據時,DMA 負責從 NIC 取數據,並在 Ring Buffer 上按順序找到下一個 ready 的 Descriptor,將數據存入該 Descriptor 指向的 sk_buffer 中,並標記 Descriptor 爲 used。因爲是按順序找 ready 的 Descriptor, 所以 Ring Buffer 是個 FIFO 的隊列。
內核採用 struct sk_buffer(https://elixir.bootlin.com/linux/v4.4/source/include/linux/skbuff.h#L545) 來描述一個收到的數據包, sk_buffer 內有個 data 指針會指向實際的物理內存。
當通過 DMA 機制存放完數據之後,NIC 會觸發一個 IRQ(硬件中斷) 讓 CPU 去處理收到的數據。因爲每次觸發 IRQ 後 CPU 都要花費時間去處理 Interrupt Handler,如果 NIC 每收到一個 Packet 都觸發一個 IRQ 會導致 CPU 花費大量的時間執行 Interrupt Handler,而每次執行只能從 Ring Buffer 中拿出一個 Packet,雖然 Interrupt Handler 執行時間很短,但這麼做非常低效,並會給 CPU 帶來很多負擔。所以目前都是採用一個叫做 New API(NAPI)(https://wiki.linuxfoundation.org/networking/napi) 的機制,去對 IRQ 做合併以減少 IRQ 次數,目前大部分網卡 Driver 都支持 NAPI 機制。NAPI 機制是如何合併和減少 IRQ 次數的 , 可以簡單理解爲: 中斷 + 輪詢 。在數據量大時,一次中斷後通過輪詢接收一定數量數據包再返回,避免產生多次中斷 , 具體細節大家可以參考這篇文章 (https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/).
概括一下網卡層面整個數據包的接收過程:
-
驅動程序事先在內存中分配一片緩衝區來接收數據包 , 叫做 sk_buffers.
-
將上述緩衝區的地址和大小(即數據包描述符),加入到 rx ring buffer。描述符中的緩衝區地址是 DMA 使用的物理地址 ;
-
驅動程序通知網卡有新的描述符 (或者說有空閒可用的描述符 )
-
網卡從 rx ring buffer 中取出描述符 , 從而獲取緩衝區的地址和大小 .
-
當一個新的數據包到達,網卡 (NIC) 調用 DMA engine,把數據包放入 sk_buffer.
如果整個過程正常 , 網卡會發起中斷,通知內核的中斷程序將數據包傳遞給 IP 層,進入 TCP/IP 協議棧處理。
每個數據包經過 TCP 層一系列複雜的步驟,更新 TCP 狀態機,最終到達 socket 的 recv Buffer,等待被應用程序接收處理。
然後 , 內核應該會把剛佔用掉的描述符重新放入 ring buffer,這樣網卡就可以繼續使用描述符了。
我們可以使用 ethtool 命令,進行 Ring Buffer 的查看和設置 .
1 查看網卡當前的設置(包括Ring Buffer): ethtool -g eth1 2 改變Ring Buffer大小: ethtool -G eth1 rx 4096 tx 4096
四 中斷處理程序如何把數據包傳遞給網絡協議層
我們通過一張圖來說明下 ,
上圖中涉及到非常多的技術細節,限於篇幅我們只做總體的說明 :
-
NIC 發起的硬件中斷(也稱爲中斷處理的上半部),被內核執行之後,開啓了軟中斷(中斷處理的下半部),並馬上退出硬件中斷處理程序 , 以便其他硬件可以繼續發起硬件中斷 .
-
軟中斷處理程序中,通過 poll 循環把數據從 Ring Buffer 取走,傳給網絡協議層處理,然後重新開啓之前已經禁用的網卡硬件中斷 .
-
當有新的數據包到達網卡時 , 回到第 1 步 .
這裏有幾點需要額外說明 :
什麼是中斷處理的上半部和下半部
我們知道中斷隨時可能發生,因此中斷處理程序也就隨時可能執行。所以必須保證中斷處理程序能夠快速執行,這樣才能儘快恢復被中斷的代碼。因此儘管對硬件而言,操作系統能迅速對其中斷進行服務非常重要,而對於系統其他部分而言,讓中斷處理程序儘可能在短時間內完成運行也同樣重要。所以我們一般把中斷處理切爲 2 個部分,上半部在接收到一箇中斷時立刻開始執行,但他只做必要的工作,例如對接收的中斷進行應答或復位硬件,這些工作都是在所有中斷被禁止的情況下完成的。而那些允許被稍後執行的工作,都會推到下半部去,下半部並不會馬上執行,而是會在稍後適當的時機執行。
網卡的軟中斷處理
現在的網卡基本都支持 RSS(Receive Side Scaling)(https://en.wikipedia.org/wiki/Network_interface_controller#RSS),也就是多對列技術。一張網卡有多個隊列,每個隊列都有各自的 IRQ 號和 Ring Buffer,但是默認情況下網卡的軟中斷都是在 CPU0 上處理,在流量大的時候,會造成 CPU0 負載打滿,引起丟包. 我們可以通過綁定中斷和 CPU 的親和性,把中斷處理均衡到多核心上 (https://www.vpsee.com/2010/07/load-balancing-with-irq-smp-affinity/),提升系統整體性能 .
什麼是 RPS
RPS 全稱是 Receive Packet Steering, 採用軟件模擬的方式,實現了多隊列網卡所提供的功能,分散了在多 CPU 系統上數據接收時的軟中斷負載, 把軟中斷分到各個 CPU 處理,而不需要硬件支持,在多核 CPU 和單隊列網卡的情況下,開啓 RPS 可以大大提升網絡性能 .
如果系統開了 RPS, 數據包會被緩衝在 TCP 層之前的隊列中 , 我們可以通過 net.core.netdev_max_backlog 適當加大這個隊列的長度,以保證上層的處理時間 .
TCP/IP 協議棧層面
此時數據包已經接入內核處理區域,由內核的 TCP/IP 協議棧處理
(一) 連接建立
大家知道,兩個基於 tcp 協議的 socket 要通信,首先要進行連接建立的過程,然後纔是數據傳輸的過程。
我們先簡單看下連接的建立過程,客戶端向 server 發送 SYN 包,server 回覆 SYN+ACK,同時將這個處於 SYN_RECV 狀態的連接保存到半連接隊列。客戶端返回 ACK 包完成三次握手,server 將 ESTABLISHED 狀態的連接移入 accept 隊列,等待應用調用 accept()。
可以看到建立連接涉及兩個隊列:
-
半連接隊列 (SYN Queue): 保存 SYN_RECV 狀態的連接。隊列長度由 net.ipv4.tcp_max_syn_backlog 設置
-
完整連接隊列 (ACCEPT Queue): 保存 ESTABLISHED 狀態的連接。隊列長度爲 min(net.core.somaxconn, backlog)。其中 backlog 是我們創建 ServerSocket(int port,int backlog) 時指定的參數,最終會傳遞給 listen 方法:
#include int listen(int sockfd, int backlog);
如果我們設置的 backlog 大於 net.core.somaxconn,完整連接隊列的長度將被設置爲 net.core.somaxconn。
注意:不同的編程語言都有相應的 socket 申請方法 , 比如 Python 是 socket 模塊.在服務端監聽一個端口,底層都要經過 3 個步驟:
申請 socket、bind 相應的 IP 和 port、調用 listen 方法進行監聽。這個 listen 方法 python 會進行封裝,別的編程語言也會進行封裝,但最終都是調用系統的 listen() 調用
我們對這兩個隊列做一下總結 :
(二) 數據傳輸
連接建立後 , 就到了 socket 數據傳輸的層面。此時 kernel 能夠爲應用程序做的,就是通過 socket Recv Buffer 緩存數據 , 儘量保證上層處理時間 .
1 Recv Buffer 自動調節機制
kernel 可以根據實際情況,自動調節 Recv Buffer 的大小 , 以期找到性能和資源的平衡點 .
當 net.ipv4.tcp_moderate_rcvbuf 設置爲 1 時,自動調節機制生效,每個 TCP 連接的 recv Buffer 由下面的 3 元數組指定 (min, default, max):
net.ipv4.tcp_rmem = 4096 87380 16777216
最初 Recv Buffer 被設置爲 87380,同時這個缺省值會覆蓋 net.core.rmem_default 的設置 , 隨後 recv buffer 根據實際情況在最大值和最小值之間動態調節。
當 net.ipv4.tcp_moderate_rcvbuf 被設置爲 0,或者設置了 socket 選項 SO_RCVBUF,緩衝的動態調節機制被關閉。
如果緩衝的動態調節機制被關閉 , 同時 socket 自己也沒有設置 SO_RCVBUF 選項,那麼一個 socket 的默認 Buffer 大小將由 net.core.rmem_default 決定,但是應用程序仍然可以通過 setsockopt() 系統調用,加大自己的 Recv Buffer, 最大不能超過 net.core.rmem_max 的設定 .
因此,我們可以得出如下總結 :
-
沒有特殊情況 , 建議打開 net.ipv4.tcp_moderate_rcvbuf=1, 這樣 kernel 會自動調整每個 socket 的 Recv Buffer
-
我們應該把 net.ipv4.tcp_rmem 中 max 值和 net.core.rmem_max 值設置成一致,這樣假設應用程序沒有關注到這個點,仍然可以由 kernel 把它自動調節成系統最大的 Recv Buffer.
-
Recv Buffer 的默認值可以適當進行提高 , 包括 net.core.rmem_default 和 net.ipv4.tcp_rmem 中的 default 設置 , 以更加激進的方式傳輸數據 .
關於 Linux 接收數據包鏈路優化的整體總結
參考文章
-
linux 網絡之數據包的接受過程
https://www.jianshu.com/p/e6162bc984c8
-
Linux 網絡協議棧收消息過程 -Ring Buffer
https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/
-
Linux 網絡協議棧收消息過程 -Per CPU Backlog
https://ylgrgyq.github.io/2017/07/24/linux-receive-packet-2/
-
網卡收發包總結
https://www.zybuluo.com/myecho/note/1068383
-
/proc/sys/net 文檔說明
https://www.kernel.org/doc/Documentation/sysctl/net.txt
-
NAPI
https://wiki.linuxfoundation.org/networking/napi
-
Linux 技巧 : 多核下綁定網卡中斷到不同 CPU(core)總結
https://blog.csdn.net/benpaobagzb/article/details/51044420
-
Network interface controller
https://en.wikipedia.org/wiki/Network_interface_controller#RSS