當前位置: 妍妍網 > 碼農

效能最佳化基礎:深入理解Linux網路

2024-05-08碼農

轉自:網路

同 CPU、記憶體以及 I/O 一樣,網路也是 Linux 系統最核心的功能。網路是一種把不同電腦或網路裝置連線到一起的技術,它本質上是一種行程間通訊方式,特別是跨系統的行程間通訊,必須要透過網路才能進行。

網路模型

多台伺服器透過網卡、交換機、路由器等網路裝置連線到一起,構成了相互連線的網路。由於網路裝置的異構性和網路協定的復雜性,國際標準化組織定義了一個七層的 OSI 網路模型,但是這個模型過於復雜,實際工作中的事實標準,是更為實用的 TCP/IP 模型。

在電腦網路時代初期,各大廠商推出了不同的網路架構和標準,為統一標準,國際標準化組織 ISO 推出了統一的 OSI 開放式系統互聯通訊參考模型 (Open System Interconnection Reference Model)。

網路分層解決了網路復雜的問題,在網路中傳輸數據中,我們對 不同裝置之間的傳輸數據的格式,需要定義一個數據標準,所以就有了網路協定。

為了解決網路互聯中異構裝置的相容性問題,並解耦復雜的網路包處理流程,OSI 模型把網路互聯的框架分為套用層、表示層、會話層、傳輸層、網路層、數據鏈路層以及實體層等七層,每個層負責不同的功能。其中,

  • • 套用層,負責為應用程式提供統一的介面。

  • • 表示層,負責把數據轉換成相容接收系統的格式。

  • • 會話層,負責維護電腦之間的通訊連線。

  • • 傳輸層,負責為數據加上傳輸表頭,形成封包。

  • • 網路層,負責數據的路由和轉發。

  • • 數據鏈路層,負責 MAC 尋址、錯誤偵測和改錯。

  • • 實體層,負責在物理網路中傳輸數據幀。

  • 但是 OSI 模型還是太復雜了,也沒能提供一個可實作的方法。所以,在 Linux 中,實際上使用的是另一個更實用的四層模型,即 TCP/IP 網路模型。

    TCP/IP 模型,把網路互聯的框架分為 套用層、傳輸層、網路層、網路介面層 等四層,其中,

  • • 套用層,負責向使用者提供一組應用程式,比如 HTTP、FTP、DNS 等。

  • • 傳輸層,負責端到端的通訊,比如 TCP、UDP 等。

  • • 網路層,負責網路包的封裝、尋址和路由,比如 IP、ICMP 等。

  • • 網路介面層,負責網路包在物理網路中的傳輸,比如 MAC 尋址、錯誤偵測以及透過網卡傳輸網路幀等。

  • TCP/IP 與 OSI 模型的關系如下圖:

    雖說 Linux 實際按照 TCP/IP 模型,實作了網路協定棧,但在平時的學習交流中,我們習慣上還是用 OSI 七層模型來描述。比如,說到七層和四層負載均衡,對應的分別是 OSI 模型中的套用層和傳輸層(而它們對應到 TCP/IP 模型中,實際上是四層和三層)。

    Linux網路棧

    有了 TCP/IP 模型後,在進行網路傳輸時,封包就會按照協定棧,對上一層發來的數據進行逐層處理;然後封裝上該層的協定頭,再發送給下一層。

    當然,網路包在每一層的處理邏輯,都取決於各層采用的網路協定。比如在套用層,一個提供 REST API 的套用,可以使用 HTTP 協定,把它需要傳輸的 JSON 數據封裝到 HTTP 協定中,然後向下傳遞給 TCP 層。

    而封裝做的事情就很簡單了,只是在原來的負載前後,增加固定格式的後設資料,原始的負載數據並不會被修改。

    比如,以透過 TCP 協定通訊的網路包為例,透過下面這張圖,我們可以看到,應用程式數據在每個層的封裝格式。

    其中:

  • • 傳輸層在應用程式數據前面增加了 TCP 頭;

  • • 網路層在 TCP 封包前增加了 IP 頭;

  • • 而網路介面層,又在 IP 封包前後分別增加了幀頭和幀尾。

  • 這些新增的頭部和尾部,增加了網路包的大小,但我們都知道,物理鏈路中並不能傳輸任意大小的封包。網路介面配置的最大傳輸單元(MTU),就規定了最大的 IP 包大小。在我們最常用的乙太網路中,MTU 預設值是 1500bytes(這也是 Linux 的預設值)。

    在Linux作業系統中執行 ifconfig 可以檢視到每個網卡的mtu值,有1450、1500等不同的值。

    [root@dev ~]# ifconfig
    cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450
    inet 10.244.0.1 netmask 255.255.255.0 broadcast 10.244.0.255
    inet6 fe80::6435:53ff:fea0:638b prefixlen 64 scopeid 0x20<link>
    ether 66:35:53:a0:63:8b txqueuelen 1000 (Ethernet)
    RX packets 124 bytes 12884 (12.5 KiB)
    RX errors 0 dropped 0 overruns 0 frame 0
    TX packets 122 bytes 29636 (28.9 KiB)
    TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
    docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
    inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
    ether 02:42:12:9c:9e:91 txqueuelen 0 (Ethernet)
    RX packets 0 bytes 0 (0.0 B)
    RX errors 0 dropped 0 overruns 0 frame 0
    TX packets 0 bytes 0 (0.0 B)
    TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
    ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
    inet 192.168.2.129 netmask 255.255.255.0 broadcast 192.168.2.255
    inet6 fe80::a923:989b:b165:8e3b prefixlen 64 scopeid 0x20<link>
    ether 00:0c:29:d9:5e:32 txqueuelen 1000 (Ethernet)
    RX packets 131 bytes 13435 (13.1 KiB)
    RX errors 0 dropped 0 overruns 0 frame 0
    TX packets 73 bytes 17977 (17.5 KiB)
    TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

    一旦網路包超過 MTU 的大小,就會在網路層分片,以保證分片後的 IP 包不大於 MTU 值。顯然,MTU 越大,需要的分包也就越少,自然,網路吞吐能力就越好。

    理解了 TCP/IP 網路模型和網路包的封裝原理後,對Linux 內核中的網路棧,其實也類似於 TCP/IP 的四層結構。如下圖所示,就是 Linux 通用 IP 網路棧的示意圖:

    從上到下來看這個網路棧,你可以發現,

  • • 最上層的應用程式,需要透過系統呼叫,來跟套接字介面進行互動;

  • • 套接字的下面,就是我們前面提到的傳輸層、網路層和網路介面層;

  • • 最底層,則是網卡驅動程式以及物理網卡裝置。

  • 網卡是發送和接收網路包的基本裝置。在系統啟動過程中,網卡透過內核中的網卡驅動程式註冊到系統中。而在網路收發過程中,內核透過中斷跟網卡進行互動。

    網路包的處理非常復雜,所以,網卡硬中斷只處理最核心的網卡數據讀取或發送,而協定棧中的大部份邏輯,都會放到軟中斷中處理。

    Linux網路包收發流程

    了解了 Linux 網路棧後,我們再來看看, Linux 到底是怎麽收發網路包的。

    PS:以下內容都以物理網卡為例。Linux 還支持眾多的虛擬網路裝置,而它們的網路收發流程會有一些差別。

    網路包的接收流程

    我們先來看網路包的接收流程。

    1. 1. 當一個網路幀到達網卡後,網卡會透過 DMA 方式,把這個網路包放到收包佇列中;然後透過硬中斷,告訴中斷處理常式已經收到了網路包。

    2. 2. 接著,網卡中斷處理常式會為網路幀分配內核數據結構(sk_buff),並將其拷貝到 sk_buff 緩沖區中;然後再透過軟中斷,通知內核收到了新的網路幀。

    3. 3. 接下來,內核協定棧從緩沖區中取出網路幀,並透過網路協定棧,從下到上逐層處理這個網路幀。比如,

    在鏈路層檢查報文的合法性,找出上層協定的型別( IPv4 還是 IPv6),再去掉幀頭、幀尾,然後交給網路層。

    1. 1. 網路層取出 IP 頭,判斷網路包下一步的走向,比如是交給上層處理還是轉發。當網路層確認這個包是要發送到本機後,就會取出上層協定的型別(TCP 還是 UDP),去掉 IP 頭,再交給傳輸層處理。

    2. 2. 傳輸層取出 TCP 頭或者 UDP 頭後,根據 < 源 IP、源埠、目的 IP、目的埠 > 四元組作為標識,找出對應的 Socket,並把數據拷貝到 Socket 的接收緩存中。

    3. 3. 最後,應用程式就可以使用 Socket 介面,讀取到新接收到的數據了。

    具體過程如下圖所示,這張圖的左半部份表示接收流程,而圖中的粉色箭頭則表示網路包的處理路徑。

    網路包的發送流程

    網路包的發送流程就是上圖的右半部份,很容易發現,網路包的發送方向,正好跟接收方向相反。

    首先,應用程式呼叫 Socket API(比如 sendmsg)發送網路包。

    由於這是一個系統呼叫,所以會陷入到內核態的套接字層中。套接字層會把封包放到 Socket 發送緩沖區中。

    接下來,網路協定棧從 Socket 發送緩沖區中,取出封包;再按照 TCP/IP 棧,從上到下逐層處理。比如,傳輸層和網路層,分別為其增加 TCP 頭和 IP 頭,執行路由尋找確認下一跳的 IP,並按照 MTU 大小進行分片。

    分片後的網路包,再送到網路介面層,進行實體位址尋址,以找到下一跳的 MAC 地址。然後添加幀頭和幀尾,放到發包佇列中。這一切完成後,會有軟中斷通知驅動程式:發包佇列中有新的網路幀需要發送。

    最後,驅動程式透過 DMA ,從發包佇列中讀出網路幀,並透過物理網卡把它發送出去。

    在不同的網路協定處理下,給我們的網路封包加上了各種頭部,這保證了網路數據在各層物理裝置的流轉下可以正確抵達目的地。收到處理後的網路封包後,接受端再透過網路協定將頭部欄位去除,得到原始的網路數據。

    下圖是客戶端與伺服器之間用網路協定連線通訊的過程:

    Linux 網路根據 TCP/IP 模型,構建其網路協定棧。TCP/IP 模型由套用層、傳輸層、網路層、網路介面層等四層組成,這也是 Linux 網路棧最核心的構成部份。

    應用程式透過套接字介面發送封包時,先要在網路協定棧中從上到下逐層處理,然後才最終送到網卡發送出去;而接收封包時,也要先經過網路棧從下到上的逐層處理,最後送到應用程式。

    了解 Linux 網路的基本原理和收發流程後,你肯定迫不及待想知道,如何去觀察網路的效能情況。具體而言,哪些指標可以用來衡量 Linux 的網路效能呢?

    常用網路相關命令

    分析網路問題的第一步,通常是檢視網路介面的配置和狀態。你可以使用 ifconfig 或者 ip 命令,來檢視網路的配置。

    ifconfig 和 ip 分別屬於軟體包 net-tools 和 iproute2,iproute2 是 net-tools 的下一代,通常情況下它們會在發行版中預設安裝。

    以網路介面 ens33 為例,可以執行下面的兩個命令,檢視它的配置和狀態:

    [root@dev ~]# ifconfig ens33
    ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
    inet 192.168.2.129 netmask 255.255.255.0 broadcast 192.168.2.255
    inet6 fe80::a923:989b:b165:8e3b prefixlen 64 scopeid 0x20<link>
    ether 00:0c:29:d9:5e:32 txqueuelen 1000 (Ethernet)
    RX packets 249 bytes 22199 (21.6 KiB)
    RX errors 0 dropped 0 overruns 0 frame 0
    TX packets 106 bytes 22636 (22.1 KiB)
    TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
    [root@dev ~]#
    [root@dev ~]# ip -s addr show ens33
    2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:0c:29:d9:5e:32 brd ff:ff:ff:ff:ff:ff
    inet 192.168.2.129/24 brd 192.168.2.255 scope global noprefixroute ens33
    valid_lft forever preferred_lft forever
    inet6 fe80::a923:989b:b165:8e3b/64 scope link noprefixroute
    valid_lft forever preferred_lft forever
    RX: bytes packets errors dropped overrun mcast
    24877 279 0 0 0 0
    TX: bytes packets errors dropped carrier collsns
    24616 123 0 0 0 0

    可以看到,ifconfig 和 ip 命令輸出的指標基本相同,只是顯示格式略微不同。比如,它們都包括了網路介面的狀態標誌、MTU 大小、IP、子網路、MAC 地址以及網路包收發的統計資訊。

    有幾個欄位可以重點關註下:

    第一,網路介面的狀態標誌。ifconfig 輸出中的 RUNNING ,或 ip 輸出中的 LOWER_UP ,都表示物理網路是連通的,即網卡已經連線到了交換機或者路由器中。如果你看不到它們,通常表示網線被拔掉了。

    第二,MTU 的大小。MTU 預設大小是 1500,根據網路架構的不同(比如是否使用了 VXLAN 等疊加網路),你可能需要調大或者調小 MTU 的數值。

    第三,網路介面的 IP 地址、子網路以及 MAC 地址。這些都是保障網路功能正常工作所必需的,你需要確保配置正確。

    第四,網路收發的字節數、包數、錯誤數以及丟包情況,特別是 TX ( Transmit發送 )和 RX(Receive接收 ) 部份的 errors、dropped、overruns、carrier 以及 collisions 等指標不為 0 時,通常表示出現了網路問題。其中:

  • • errors 表示發生錯誤的封包數,比如校驗錯誤、訊框同步錯誤等;

  • • dropped 表示丟棄的封包數,即封包已經收到了 Ring Buffer,但因為記憶體不足等原因丟包;

  • • overruns 表示超限封包數,即網路 I/O 速度過快,導致 Ring Buffer 中的封包來不及處理(佇列滿)而導致的丟包;

  • • carrier 表示發生 carrirer 錯誤的封包數,比如雙工模式不匹配、物理電纜出現問題等;

  • • collisions 表示碰撞封包數。

  • 套接字資訊

    套接字介面在網路程式功能中是內核與套用層之間的介面。TCP/IP 協定棧的所有數據和控制功能都來自於套接字介面,與 OSI 網路分層模型相比,TCP/IP 協定棧本身在傳輸層以上就不包含任何其他協定。

    在 Linux 作業系統中,替代傳輸層以上協定實體的標準介面,稱為套接字,它負責實作傳輸層以上所有的功能,可以說套接字是 TCP/IP 協定棧對外的視窗。

    ifconfig 和 ip 只顯示了網路介面收發封包的統計資訊,但在實際的效能問題中,網路協定棧中的統計資訊,我們也必須關註,可以用 netstat 或者 ss ,來檢視套接字、網路棧、網路介面以及路由表的資訊。

    我個人更推薦,使用 ss 來查詢網路的連線資訊,因為它比 netstat 提供了更好的效能(速度更快)。

    比如,你可以執行下面的命令,查詢套接字資訊:

    head -n 4 表示只顯示前面4行
    -l 表示只顯示監聽套接字
    -n 表示顯示數位地址和埠(而不是名字)
    -p 表示顯示行程資訊
    [root@dev ~]# netstat -nlp | head -n 4
    Active Internet connections (only servers)
    Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 952/sshd
    tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 11/master

    -l 表示只顯示監聽套接字
    -t 表示只顯示 TCP 套接字
    -n 表示顯示數位地址和埠(而不是名字)
    -p 表示顯示行程資訊
    ss -ltnp | head -n 4

    netstat 和 ss 的輸出也是類似的,都展示了套接字的狀態、接收佇列、發送佇列、本地地址、遠端地址、行程 PID 和行程名稱等。

    其中,接收佇列(Recv-Q)和發送佇列(Send-Q)需要你特別關註,它們通常應該是 0。當你發現它們不是 0 時,說明有網路包的堆積發生。當然還要註意,在不同套接字狀態下,它們的含義不同。

    當套接字處於 連線狀態(Established) 時,

  • • Recv-Q 表示套接字緩沖還沒有被應用程式取走的字節數(即接收佇列長度)。

  • • Send-Q 表示還沒有被遠端主機確認的字節數(即發送佇列長度)。

  • 當套接字處於 監聽狀態(Listening) 時,

  • • Recv-Q 表示全連線佇列的長度。

  • • Send-Q 表示全連線佇列的最大長度。

  • 所謂全連線,是指伺服器收到了客戶端的 ACK,完成了 TCP 三次握手,然後就會把這個連線挪到全連線佇列中。這些全連線中的套接字,還需要被 accept() 系統呼叫取走,伺服器才可以開始真正處理客戶端的請求。

    與全連線佇列相對應的,還有一個半連線佇列。所謂半連線是指還沒有完成 TCP 三次握手的連線,連線只進行了一半。伺服器收到了客戶端的 SYN 包後,就會把這個連線放到半連線佇列中,然後再向客戶端發送 SYN+ACK 包。

    連線統計資訊

    類似的,使用 netstat 或 ss ,也可以檢視協定棧的資訊:

    [root@dev ~]# netstat -s
    ...
    Tcp:
    1898 active connections openings
    1502 passive connection openings
    24 failed connection attempts
    1304 connection resets received
    178 connections established
    133459 segments received
    133428 segments send out
    22 segments retransmited
    0 bad segments received.
    1400 resets sent
    ...
    [root@dev ~]# ss -s
    Total: 1700 (kernel 2499)
    TCP: 340 (estab 178, closed 144, orphaned 0, synrecv 0, timewait 134/0), ports 0
    Transport Total IP IPv6
    * 2499 - -
    RAW 1 0 1
    UDP 5 3 2
    TCP 196 179 17
    INET 202 182 20
    FRAG 0 0 0

    這些協定棧的統計資訊都很直觀。ss 只顯示已經連線、關閉、孤兒套接字等簡要統計,而 netstat 則提供的是更詳細的網路協定棧資訊,展示了 TCP 協定的主動連線、被動連線、失敗重試、發送和接收的分段數量等各種資訊。

    環通度和延時

    通常使用 ping ,來測試遠端主機的環通度和延時,而ping基於 ICMP 協定。比如,執行下面的命令,你就可以測試本機到 192.168.2.129 這個 IP 地址的環通度和延時:

    [root@dev ~]# ping -c3 192.168.2.129
    PING 192.168.2.129 (192.168.2.129) 56(84) bytes of data.
    64 bytes from 192.168.2.129: icmp_seq=1 ttl=64 time=0.026 ms
    64 bytes from 192.168.2.129: icmp_seq=2 ttl=64 time=0.016 ms
    64 bytes from 192.168.2.129: icmp_seq=3 ttl=64 time=0.015 ms
    --- 192.168.2.129 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 1998ms
    rtt min/avg/max/mdev = 0.015/0.019/0.026/0.005 ms

    ping 的輸出,可以分為兩部份。

    1. 1. 第一部份,是每個 ICMP 請求的資訊,包括 ICMP 序列號(icmp_seq)、TTL(生存時間,或者跳數)以及往返延時。

    2. 2. 第二部份,則是三次 ICMP 請求的匯總。

    比如上面的範例顯示,發送了 3 個網路包,並且接收到 3 個響應,沒有丟包發生,這說明測試主機到 192.168.2.129是連通的;平均往返延時(RTT)是 0.026 ms,也就是從發送 ICMP 開始,到接收到主機回復的確認,總共經歷的時間。