一、上節回顧
上一節,我們一起學習了如何分析網絡丟包的問題,特別是從鏈路層、網絡層以及傳輸層等主要的協議棧中進行分析。
不過,通過前面這幾層的分析,我們還是沒有找出最終的性能瓶頸。看來,還是要繼續深挖才可以。今天,我們就來繼續分析這個未果的案例。
在開始下面的內容前,你可以先回憶一下上節課的內容,並且自己動腦想一想,除了我們提到的鏈路層、網絡層以及傳輸層之外,還有哪些潛在問題可能會導致丟包呢?
二、iptables
首先我們要知道,除了網絡層和傳輸層的各種協議,iptables 和內核的連接跟蹤機制也可能會導致丟包。所以,這也是發生丟包問題時,我們必須要排查的一個因素。
我們先來看看連接跟蹤,我已經在 如何優化 NAT 性能 文章中,給你講過連接跟蹤的優化思路。要確認是不是連接跟蹤導致的問題,其實只需要對比當前的連接跟蹤數和最大連接跟蹤數即可。
不過,由於連接跟蹤在 Linux 內核中是全局的(不屬於網絡命名空間),我們需要退出容器終端,回到主機中來查看。
你可以在容器終端中,執行 exit ;然后執行下面的命令,查看連接跟蹤數:
# 容器終端中執行 exit root@nginx:/# exit exit # 主機終端中查詢內核配置 $ sysctl net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_max = 262144 $ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_count = 182
實際測試代碼如下:
[root@luoahong ~]# sysctl net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_max = 262144 [root@luoahong ~]# sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_count = 11
從這兒你可以看到,連接跟蹤數只有 182,而最大連接跟蹤數則是 262144。顯然,這里的丟包,不可能是連接跟蹤導致的。
接着,再來看 iptables。回顧一下 iptables 的原理,它基於 Netfilter 框架,通過一系列的規則,對網絡數據包進行過濾(如防火牆)和修改(如 NAT)。
這些 iptables 規則,統一管理在一系列的表中,包括 filter(用於過濾)、nat(用於 NAT)、mangle(用於修改分組數據) 和 raw(用於原始數據包)等。而每張表又可以包括一系列的
鏈,用於對 iptables 規則進行分組管理。
對於丟包問題來說,最大的可能就是被 filter 表中的規則給丟棄了。要弄清楚這一點,就需要我們確認,那些目標為 DROP 和 REJECT 等會棄包的規則,有沒有被執行到。
你可以把所有的 iptables 規則列出來,根據收發包的特點,跟 iptables 規則進行匹配。不過顯然,如果 iptables 規則比較多,這樣做的效率就會很低。
當然,更簡單的方法,就是直接查詢 DROP 和 REJECT 等規則的統計信息,看看是否為 0。如果統計值不是 0 ,再把相關的規則拎出來進行分析。
我們可以通過 iptables -nvL 命令,查看各條規則的統計信息。比如,你可以執行下面的 dockerexec 命令,進入容器終端;然后再執行下面的 iptables 命令,就可以看到 filter 表的統計數據了:
# 在主機中執行 $ docker exec -it nginx bash # 在容器中執行 root@nginx:/# 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
實際測試代碼如下:
root@nginx:/# iptables -t filter -nvL Chain INPUT (policy ACCEPT 24 packets, 960 bytes) pkts bytes target prot opt in out source destination 7 280 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 23 packets, 1012 bytes) pkts bytes target prot opt in out source destination 8 352 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.29999999981 root@nginx:/#
從 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
實際測試代碼如下:
root@luoahong:~# hping3 -c 10 -S -p 80 192.168.118.85 HPING 192.168.118.85 (ens33 192.168.118.85): S set, 40 headers + 0 data bytes len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=65535 rtt=3.9 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=65535 rtt=1013.3 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=65535 rtt=8.7 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=65535 rtt=1017.5 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=65535 rtt=7.3 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=65535 rtt=5.0 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=8 win=65535 rtt=10.8 ms --- 192.168.118.85 hping statistic --- 10 packets transmitted, 7 packets received, 30% packet loss round-trip min/avg/max = 3.9/295.2/1017.5 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
實際測試代碼如下:
root@luoahong:~# curl --max-time 3 http://192.168.118.85 curl: (28) Operation timed out after 3002 milliseconds with 0 bytes received
從 curl 的輸出中,你可以發現,這次連接超時了。可是,剛才我們明明用 hping3 驗證了端口正常,現在卻發現 HTTP 連接超時,是不是因為 Nginx 突然異常退出了呢?
不妨再次運行 hping3 來確認一下:
hping3 -c 3 -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.8 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=5120 rtt=7.7 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=3.6 ms --- 192.168.0.30 hping statistic --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 3.6/6.4/7.8 ms
實際測試代碼如下:
root@luoahong:~# hping3 -c 10 -S -p 80 192.168.118.85 HPING 192.168.118.85 (ens33 192.168.118.85): S set, 40 headers + 0 data bytes len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=65535 rtt=3.9 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=65535 rtt=1013.3 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=65535 rtt=8.7 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=65535 rtt=1017.5 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=65535 rtt=7.3 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=65535 rtt=5.0 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=8 win=65535 rtt=10.8 ms --- 192.168.118.85 hping statistic --- 10 packets transmitted, 7 packets received, 30% packet loss round-trip min/avg/max = 3.9/295.2/1017.5 ms
奇怪,hping3 的結果顯示,Nginx 的 80 端口確確實實還是正常狀態。這該如何是好呢?別忘了,我們還有個大殺器——抓包操作。看來有必要抓包看看了。
三、tcpdump
接下來,我們切換回終端一,在容器終端中,執行下面的 tcpdump 命令,抓取 80 端口的包:
root@nginx:/# 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
實際測試代碼如下:
root@luoahong:~# curl --max-time 3 http://192.168.118.85 curl: (28) Operation timed out after 3002 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
實際測試代碼如下:
root@nginx:/# 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 08:48:31.746239 IP 192.168.118.77.47274 > 172.17.0.2.80: Flags [S], seq 3419504692, win 29200, options [mss 1460,sackOK,TS val 109552116 ecr 0,nop,wscale 7], length 0 08:48:31.746400 IP 172.17.0.2.80 > 192.168.118.77.47274: Flags [S.], seq 1891132195, ack 3419504693, win 65392, options [mss 256,sackOK,TS val 2205929180 ecr 109552116,nop,wscale 7], length 0 08:48:31.747134 IP 192.168.118.77.47274 > 172.17.0.2.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 109552117 ecr 2205929180], length 0 08:48:34.748411 IP 192.168.118.77.47274 > 172.17.0.2.80: Flags [F.], seq 79, ack 1, win 229, options [nop,nop,TS val 109555119 ecr 2205929180], length 0 08:48:34.748567 IP 172.17.0.2.80 > 192.168.118.77.47274: Flags [.], ack 1, win 511, options [nop,nop,TS val 2205932182 ecr 109552117,nop,nop,sack 1 {79:80}], length 0 ^C 5 packets captured 5 packets received by filter 0 packets dropped by kernel 22 packets dropped by interface
經過這么一系列的操作,從 tcpdump 的輸出中,我們就可以看到:
- 前三個包是正常的 TCP 三次握手,這沒問題;
- 但第四個包卻是在 3 秒以后了,並且還是客戶端(VM2)發送過來的 FIN 包,也就說明,客戶端的連接關閉了。
我想,根據 curl 設置的 3 秒超時選項,你應該能猜到,這是因為 curl 命令超時后退出了。我把這一過程,用 TCP 交互的流程圖(實際上來自 Wireshark 的 Flow Graph)來表示,你可
以更清楚地看到上面這個問題:
這里比較奇怪的是,我們並沒有抓取到 curl 發來的 HTTP GET 請求。那么,究竟是網卡丟包了,還是客戶端壓根兒就沒發過來呢?
我們可以重新執行 netstat -i 命令,確認一下網卡有沒有丟包問題:
root@nginx:/# 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 就可以了。我們繼續在容器終端中,執行下面的命令,把容器 eth0 的 MTU 改成 1500:
root@nginx:/# 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>
實際測試代碼如下:
root@luoahong:~# curl --max-time 3 http://192.168.118.85 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
非常不容易呀,這次終於看到了熟悉的 Nginx 響應,說明丟包的問題終於徹底解決了。
當然,案例結束前,不要忘記停止今天的 Nginx 應用。你可以切換回終端一,在容器終端中執行exit 命令,退出容器終端:
root@nginx:/# exit exit
最后,再執行下面的 docker 命令,停止並刪除 Nginx 容器:
docker rm -f nginx
四、小結
今天,我繼續帶你分析了網絡丟包的問題。特別是在時不時丟包的情況下,定位和優化都需要我們花心思重點投入。
網絡丟包問題的嚴重性不言而喻。碰到丟包問題時,我們還是要從 Linux 網絡收發的流程入手,結合 TCP/IP 協議棧的原理來逐層分析。