天天看點

Linux 性能優化實戰-網絡丢包問題分析

作者:老李講安全

所謂丢包,是指在網絡資料的收發過程中,由于種種原因,資料包還沒傳輸到應用程式中,就被丢棄了。這些被丢棄包的數量,除以總的傳輸包數,也就是我們常說的丢包率。丢包率是網絡性能中最核心的名額之一。丢包通常會帶來嚴重的性能下降,特别是對 TCP 來說,丢包通常意味着網絡擁塞和重傳,進而還會導緻網絡延遲增大、吞吐降低。

一、 哪裡可能丢包

接下來,我就以最常用的反向代理伺服器 Nginx 為例,帶你一起看看如何分析網絡丢包的問題。執行下面的 hping3 指令,進一步驗證 Nginx 是不是可以正常通路。這裡我沒有使用 ping,是因為 ping 基于 ICMP 協定,而 Nginx 使用的是 TCP 協定。

# -c表示發送10個請求,-S表示使用TCP SYN,-p指定端口為80
hping3 -c 10 -S -p 80 192.168.0.30
 
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=5120 rtt=7.5 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=5120 rtt=7.4 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=3.3 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=5120 rtt=3.0 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=3027.2 ms
 
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 5 packets received, 50% packet loss
round-trip min/avg/max = 3.0/609.7/3027.2 ms
           

從 hping3 的輸出中,我們可以發現,發送了 10 個請求包,卻隻收到了 5 個回複,50%的包都丢了。再觀察每個請求的 RTT 可以發現,RTT 也有非常大的波動變化,小的時候隻有 3ms,而大的時候則有 3s。根據這些輸出,我們基本能判斷,已經發生了丢包現象。可以猜測,3s 的 RTT ,很可能是因為丢包後重傳導緻的。

那到底是哪裡發生了丢包呢?排查之前,我們可以回憶一下 Linux 的網絡收發流程,先從理論上分析,哪裡有可能會發生丢包。你不妨拿出手邊的筆和紙,邊回憶邊在紙上梳理,思考清楚再繼續下面的内容。在這裡,為了幫你了解網絡丢包的原理,我畫了一張圖,你可以儲存并列印出來使用

從圖中你可以看出,可能發生丢包的位置,實際上貫穿了整個網絡協定棧。換句話說,全程都有丢包的可能。

  • 在兩台 VM 連接配接之間,可能會發生傳輸失敗的錯誤,比如網絡擁塞、線路錯誤等;
  • 在網卡收包後,環形緩沖區可能會因為溢出而丢包;
  • 在鍊路層,可能會因為網絡幀校驗失敗、QoS 等而丢包;
  • 在 IP 層,可能會因為路由失敗、組包大小超過 MTU 等而丢包;
  • 在傳輸層,可能會因為端口未監聽、資源占用超過核心限制等而丢包;
  • 在套接字層,可能會因為套接字緩沖區溢出而丢包;
  • 在應用層,可能會因為應用程式異常而丢包;
  • 此外,如果配置了 iptables 規則,這些網絡包也可能因為 iptables 過濾規則而丢包

當然,上面這些問題,還有可能同時發生在通信的兩台機器中。不過,由于我們沒對 VM2做任何修改,并且 VM2 也隻運作了一個最簡單的 hping3 指令,這兒不妨假設它是沒有問題的。為了簡化整個排查過程,我們還可以進一步假設, VM1 的網絡和核心配置也沒問題。接下來,就可以從協定棧中,逐層排查丢包問題。

二、 鍊路層

當鍊路層由于緩沖區溢出等原因導緻網卡丢包時,Linux 會在網卡收發資料的統計資訊中記錄下收發錯誤的次數。可以通過 ethtool 或者 netstat ,來檢視網卡的丢包記錄。

netstat -i
 
Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0       100       31      0      0 0             8      0      0      0 BMRU
lo       65536        0      0      0 0             0      0      0      0 LRU
           

RX-OK、RX-ERR、RX-DRP、RX-OVR ,分别表示接收時的總包數、總錯誤數、進入 Ring Buffer 後因其他原因(如記憶體不足)導緻的丢包數以及 Ring Buffer 溢出導緻的丢包數。

TX-OK、TX-ERR、TX-DRP、TX-OVR 也代表類似的含義,隻不過是指發送時對應的各個名額。

這裡我們沒有發現任何錯誤,說明虛拟網卡沒有丢包。不過要注意,如果用 tc 等工具配置了 QoS,那麼 tc 規則導緻的丢包,就不會包含在網卡的統計資訊中。是以接下來,我們還要檢查一下 eth0 上是否配置了 tc 規則,并檢視有沒有丢包。添加 -s 選項,以輸出統計資訊:

tc -s qdisc show dev eth0
 
qdisc netem 800d: root refcnt 2 limit 1000 loss 30%
 Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
           

可以看到, eth0 上配置了一個網絡模拟排隊規則(qdisc netem),并且配置了丢包率為 30%(loss 30%)。再看後面的統計資訊,發送了 8 個包,但是丢了 4個。看來應該就是這裡導緻 Nginx 回複的響應包被 netem 子產品給丢了。

既然發現了問題,解決方法也很簡單,直接删掉 netem 子產品就可以了。執行下面的指令,删除 tc 中的 netem 子產品:

tc qdisc del dev eth0 root netem loss 30%
           

删除後,重新執行之前的 hping3 指令,看看現在還有沒有問題:

hping3 -c 10 -S -p 80 192.168.0.30
 
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=7.9 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=1003.8 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=7.6 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=7.4 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=3.0 ms
 
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 5 packets received, 50% packet loss
round-trip min/avg/max = 3.0/205.9/1003.8 ms
           

不幸的是,從 hping3 的輸出中可以看到還是 50% 的丢包,RTT 的波動也仍舊很大,從 3ms 到 1s。顯然,問題還是沒解決,丢包還在繼續發生。不過,既然鍊路層已經排查完了,我們就繼續向上層分析,看看網絡層和傳輸層有沒有問題。

三、 網絡層和傳輸層

在網絡層和傳輸層中,引發丢包的因素非常多。不過,其實想确認是否丢包,是非常簡單的事,因為 Linux 已經為我們提供了各個協定的收發彙總情況。執行 netstat -s 指令,可以看到協定的收發彙總,以及錯誤資訊:

netstat -s
#輸出
Ip:
    Forwarding: 1          //開啟轉發
    31 total packets received    //總收包數
    0 forwarded            //轉發包數
    0 incoming packets discarded  //接收丢包數
    25 incoming packets delivered  //接收的資料包數
    15 requests sent out      //發出的資料包數
Icmp:
    0 ICMP messages received    //收到的ICMP包數
    0 input ICMP message failed    //收到ICMP失敗數
    ICMP input histogram:
    0 ICMP messages sent      //ICMP發送數
    0 ICMP messages failed      //ICMP失敗數
    ICMP output histogram:
Tcp:
    0 active connection openings  //主動連接配接數
    0 passive connection openings  //被動連接配接數
    11 failed connection attempts  //失敗連接配接嘗試數
    0 connection resets received  //接收的連接配接重置數
    0 connections established    //建立連接配接數
    25 segments received      //已接收封包數
    21 segments sent out      //已發送封包數
    4 segments retransmitted    //重傳封包數
    0 bad segments received      //錯誤封包數
    0 resets sent          //發出的連接配接重置數
Udp:
    0 packets received
    ...
TcpExt:
    11 resets received for embryonic SYN_RECV sockets  //半連接配接重置數
    0 packet headers predicted
    TCPTimeouts: 7    //逾時數
    TCPSynRetrans: 4  //SYN重傳數
  ...
           

etstat 彙總了 IP、ICMP、TCP、UDP 等各種協定的收發統計資訊。不過,我們的目的是排查丢包問題,是以這裡主要觀察的是錯誤數、丢包數以及重傳數。可以看到,隻有 TCP 協定發生了丢包和重傳,分别是:

  • 11 次連接配接失敗重試(11 failed connection attempts)
  • 4 次重傳(4 segments retransmitted)
  • 11 次半連接配接重置(11 resets received for embryonic SYN_RECV sockets)
  • 4 次 SYN 重傳(TCPSynRetrans)
  • 7 次逾時(TCPTimeouts)

這個結果告訴我們,TCP 協定有多次逾時和失敗重試,并且主要錯誤是半連接配接重置。換句話說,主要的失敗,都是三次握手失敗。不過,雖然在這兒看到了這麼多失敗,但具體失敗的根源還是無法确定。是以,我們還需要繼續順着協定棧來分析。接下來的幾層又該如何分析呢?

四、 iptables

首先,除了網絡層和傳輸層的各種協定,iptables 和核心的連接配接跟蹤機制也可能會導緻丢包。是以,這也是發生丢包問題時我們必須要排查的一個因素。

先來看看連接配接跟蹤,要确認是不是連接配接跟蹤導緻的問題,隻需要對比目前的連接配接跟蹤數和最大連接配接跟蹤數即可。

# 主機終端中查詢核心配置
$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144
$ sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 182
           

可以看到,連接配接跟蹤數隻有 182,而最大連接配接跟蹤數則是 262144。顯然,這裡的丢包,不可能是連接配接跟蹤導緻的。

接着,再來看 iptables。回顧一下 iptables 的原理,它基于 Netfilter 架構,通過一系列的規則,對網絡資料包進行過濾(如防火牆)和修改(如 NAT)。這些 iptables 規則,統一管理在一系列的表中,包括 filter、nat、mangle(用于修改分組資料) 和 raw(用于原始資料包)等。而每張表又可以包括一系列的鍊,用于對 iptables 規則進行分組管理。

對于丢包問題來說,最大的可能就是被 filter 表中的規則給丢棄了。要弄清楚這一點,就需要我們确認,那些目标為 DROP 和 REJECT 等會棄包的規則,有沒有被執行到。可以直接查詢 DROP 和 REJECT 等規則的統計資訊,看看是否為0。如果不是 0 ,再把相關的規則拎出來進行分析。

iptables -t filter -nvL
#輸出
Chain INPUT (policy ACCEPT 25 packets, 1000 bytes)
 pkts bytes target     prot opt in     out     source               destination
    6   240 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981
 
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 
Chain OUTPUT (policy ACCEPT 15 packets, 660 bytes)
 pkts bytes target     prot opt in     out     source               destination
    6   264 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981
           

從 iptables 的輸出中,你可以看到,兩條 DROP 規則的統計數值不是 0,它們分别在INPUT 和 OUTPUT 鍊中。這兩條規則實際上是一樣的,指的是使用 statistic 子產品,進行随機 30% 的丢包。0.0.0.0/0 表示比對所有的源 IP 和目的 IP,也就是會對所有包都進行随機 30% 的丢包。看起來,這應該就是導緻部分丢包的“罪魁禍首”了。

執行下面的兩條 iptables 指令,删除這兩條 DROP 規則。

root@nginx:/# iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP
root@nginx:/# iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP
           

再次執行剛才的 hping3 指令,看看現在是否正常

hping3 -c 10 -S -p 80 192.168.0.30
#輸出
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=11.9 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=5120 rtt=7.8 ms
...
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=15.0 ms
 
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 10 packets received, 0% packet loss
round-trip min/avg/max = 3.3/7.9/15.0 ms
           

這次輸出你可以看到,現在已經沒有丢包了,并且延遲的波動變化也很小。看來,丢包問題應該已經解決了。

不過,到目前為止,我們一直使用的 hping3 工具,隻能驗證案例 Nginx 的 80 端口處于正常監聽狀态,卻還沒有通路 Nginx 的 HTTP 服務。是以,不要匆忙下結論結束這次優化,我們還需要進一步确認,Nginx 能不能正常響應 HTTP 請求。我們繼續在終端二中,執行如下的 curl 指令,檢查 Nginx 對 HTTP 請求的響應:

$ curl --max-time 3 http://192.168.0.30
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received
           

奇怪,hping3 的結果顯示Nginx 的 80 端口是正常狀态,為什麼還是不能正常響應 HTTP 請求呢?别忘了,我們還有個大殺器——抓包操作。看來有必要抓包看看了。

五、 tcpdump

執行下面的 tcpdump 指令,抓取 80 端口的包

tcpdump -i eth0 -nn port 80
#輸出
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
           

然後,切換到終端二中,再次執行前面的 curl 指令:

curl --max-time 3 http://192.168.0.30
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received
           

等到 curl 指令結束後,再次切換回終端一,檢視 tcpdump 的輸出:

14:40:00.589235 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [S], seq 332257715, win 29200, options [mss 1418,sackOK,TS val 486800541 ecr 0,nop,wscale 7], length 0
14:40:00.589277 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [S.], seq 1630206251, ack 332257716, win 4880, options [mss 256,sackOK,TS val 2509376001 ecr 486800541,nop,wscale 7], length 0
14:40:00.589894 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 486800541 ecr 2509376001], length 0
14:40:03.589352 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [F.], seq 76, ack 1, win 229, options [nop,nop,TS val 486803541 ecr 2509376001], length 0
14:40:03.589417 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [.], ack 1, win 40, options [nop,nop,TS val 2509379001 ecr 486800541,nop,nop,sack 1 {76:77}], length 0
           

從 tcpdump 的輸出中,我們就可以看到:

  • 前三個包是正常的 TCP 三次握手,這沒問題;
  • 但第四個包卻是在 3 秒以後了,并且還是用戶端(VM2)發送過來的 FIN 包,說明用戶端的連接配接關閉了

根據 curl 設定的 3 秒逾時選項,你應該能猜到,這是因為 curl 指令逾時後退出了。用 Wireshark 的 Flow Graph 來表示,

你可以更清楚地看到上面這個問題:

Linux 性能優化實戰-網絡丢包問題分析

img

這裡比較奇怪的是,我們并沒有抓取到 curl 發來的 HTTP GET 請求。那究竟是網卡丢包了,還是用戶端就沒發過來呢?

可以重新執行 netstat -i 指令,确認一下網卡有沒有丢包問題:

netstat -i
 
Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0       100      157      0    344 0            94      0      0      0 BMRU
lo       65536        0      0      0 0             0      0      0      0 LRU
           

從 netstat 的輸出中,你可以看到,接收丢包數(RX-DRP)是 344,果然是在網卡接收時丢包了。不過問題也來了,為什麼剛才用 hping3 時不丢包,現在換成 GET 就收不到了呢?還是那句話,遇到搞不懂的現象,不妨先去查查工具和方法的原理。我們可以對比一下這兩個工具:

  • hping3 實際上隻發送了 SYN 包;
  • curl 在發送 SYN 包後,還會發送 HTTP GET 請求。HTTP GET本質上也是一個 TCP 包,但跟 SYN 包相比,它還攜帶了 HTTP GET 的資料。

通過這個對比,你應該想到了,這可能是 MTU 配置錯誤導緻的。為什麼呢?

其實,仔細觀察上面 netstat 的輸出界面,第二列正是每個網卡的 MTU 值。eth0 的 MTU隻有 100,而以太網的 MTU 預設值是 1500,這個 100 就顯得太小了。當然,MTU 問題是很好解決的,把它改成 1500 就可以了。

ifconfig eth0 mtu 1500
           

修改完成後,再切換到終端二中,再次執行 curl 指令,确認問題是否真的解決了:

curl --max-time 3 http://192.168.0.30/
#輸出
<!DOCTYPE html>
<html>
...
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
           

非常不容易呀,這次終于看到了熟悉的 Nginx 響應,說明丢包的問題終于徹底解決了。

來源:網絡

繼續閱讀