可直接點擊上方藍字

(網易遊戲運維平臺)

關注我們,獲一手遊戲運維方案

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 網卡接收到數據包

  • 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/).

    概括一下網卡層面整個數據包的接收過程:

    1. 驅動程序事先在內存中分配一片緩衝區來接收數據包 , 叫做 sk_buffers.

    2. 將上述緩衝區的地址和大小(即數據包描述符),加入到 rx ring buffer。描述符中的緩衝區地址是 DMA 使用的物理地址 ;

    3. 驅動程序通知網卡有新的描述符 (或者說有空閒可用的描述符 )

    4. 網卡從 rx ring buffer 中取出描述符 , 從而獲取緩衝區的地址和大小 .

    5. 當一個新的數據包到達,網卡 (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
    

    四 中斷處理程序如何把數據包傳遞給網絡協議層

    我們通過一張圖來說明下 ,

    上圖中涉及到非常多的技術細節,限於篇幅我們只做總體的說明 :

    1. NIC 發起的硬件中斷(也稱爲中斷處理的上半部),被內核執行之後,開啓了軟中斷(中斷處理的下半部),並馬上退出硬件中斷處理程序 , 以便其他硬件可以繼續發起硬件中斷 .

    2. 軟中斷處理程序中,通過 poll 循環把數據從 Ring Buffer 取走,傳給網絡協議層處理,然後重新開啓之前已經禁用的網卡硬件中斷 .

    3. 當有新的數據包到達網卡時 , 回到第 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 接收數據包鏈路優化的整體總結

    參考文章

    1. linux 網絡之數據包的接受過程

      https://www.jianshu.com/p/e6162bc984c8

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

      https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/

    3. Linux 網絡協議棧收消息過程 -Per CPU Backlog

      https://ylgrgyq.github.io/2017/07/24/linux-receive-packet-2/

    4. 網卡收發包總結

      https://www.zybuluo.com/myecho/note/1068383

    5. /proc/sys/net 文檔說明

      https://www.kernel.org/doc/Documentation/sysctl/net.txt

    6. NAPI

      https://wiki.linuxfoundation.org/networking/napi

    7. Linux 技巧 : 多核下綁定網卡中斷到不同 CPU(core)總結

      https://blog.csdn.net/benpaobagzb/article/details/51044420

    8. Network interface controller

      https://en.wikipedia.org/wiki/Network_interface_controller#RSS

    相關文章