所謂丟包,是指在網絡數據的收發過程中,由于種種原因,數據包還沒傳輸到應用程序中,就被丟棄了。這些被丟棄包的數量,除以總的傳輸包數,也就是我們常說的丟包率。丟包率是網絡性能中最核心的指標之一。丟包通常會帶來嚴重的性能下降,特別是對 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 的輸出:
1400.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 1400.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 1400.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 1403.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 1403.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 來表示,
你可以更清楚地看到上面這個問題:
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/ #輸出 ...Thank?you?for?using?nginx.