一、上節回顧
上一節,我們梳理了,應用程序容器化后性能下降的分析方法。一起先簡單回顧下。
容器利用 Linux 內核提供的命名空間技術,將不同應用程序的運行隔離起來,並用統一的鏡像,來管理應用程序的依賴環境。這為應用程序的管理和維護,帶來了極大的便捷性,並進一步催生
了微服務、雲原生等新一代技術架構。
不過,雖說有很多優勢,但容器化也會對應用程序的性能帶來一定影響。比如,上一節我們一起分析的 Java 應用,就容易發生啟動過慢、運行一段時間后 OOM 退出等問題。當你碰到這種問
題時,不要慌,我們前面四大基礎模塊中的各種思路,都依然適用。
實際上,我們專欄中的很多案例都在容器中運行。容器化后,應用程序會通過命名空間進行隔離。所以,你在分析時,不要忘了結合命名空間、cgroups、iptables 等來綜合分析。比如:
- cgroups 會影響容器應用的運行;
- iptables 中的 NAT,會影響容器的網絡性能;
- 疊加文件系統,會影響應用的 I/O 性能等。
關於 NAT 的影響,我在網絡模塊的 如何優化 NAT 性能 文章中,已經為你介紹了很多優化思路。今天,我們一起來看另一種情況,也就是丟包的分析方法。
所謂丟包,是指在網絡數據的收發過程中,由於種種原因,數據包還沒傳輸到應用程序中,就被丟棄了。這些被丟棄包的數量,除以總的傳輸包數,也就是我們常說的丟包率。丟包率是網絡性
能中最核心的指標之一。
丟包通常會帶來嚴重的性能下降,特別是對 TCP 來說,丟包通常意味着網絡擁塞和重傳,進而還會導致網絡延遲增大、吞吐降低。
接下來,我就以最常用的反向代理服務器 Nginx 為例,帶你一起看看,如何分析網絡丟包的問題。由於內容比較多,這個案例將分為上下兩篇來講解,今天我們先看第一部分內容。
二、案例准備
今天的案例需要用到兩台虛擬機,還是基於 Ubuntu 18.04,同樣適用於其他的 Linux 系統。我使用的案例環境如下所示:
- 機器配置:2 CPU,8GB 內存。
- 預先安裝 docker、curl、hping3 等工具,如 apt install docker.io curl hping3。
這些工具,我們在前面的案例中已經多次使用,這里就不再重復介紹。
現在,打開兩個終端,分別登錄到這兩台虛擬機中,並安裝上述工具。
注意,以下所有命令都默認以 root 用戶運行,如果你用普通用戶身份登陸系統,請運行 sudosu root 命令,切換到 root 用戶。
如果安裝過程有問題,你可以先上網搜索解決,實在解決不了的,記得在留言區向我提問。
到這里,准備工作就完成了。接下來,我們正式進入操作環節。
三、案例分析
我們今天要分析的案例是一個 Nginx 應用,如下圖所示,hping3 和 curl 是 Nginx 的客戶端。
為了方便你運行,我已經把它打包成了一個 Docker 鏡像,並推送到 Docker Hub 中。你可以直接按照下面的步驟來運行它。
在終端一中執行下面的命令,啟動 Nginx 應用,並在 80 端口監聽。如果一切正常,你應該可以看到如下的輸出:
docker run --name nginx --hostname nginx --privileged -p 80:80 -itd feisky/nginx:drop dae0202cc27e5082b282a6aeeb1398fcec423c642e63322da2a97b9ebd7538e
然后,執行 docker ps 命令,查詢容器的狀態,你會發現容器已經處於運行狀態(Up)了:
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES dae0202cc27e feisky/nginx:drop "/start.sh" 4 minutes ago Up 4 minutes 0.0.0.0:80->80/tcp nginx
不過,從 docker ps 的輸出,我們只能知道容器處於運行狀態,至於 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
實際測試代碼如下:
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=4 win=65535 rtt=6.7 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=65535 rtt=3095.3 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=65535 rtt=2.9 ms len=46 ip=192.168.118.85 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=65535 rtt=6.8 ms --- 192.168.118.85 hping statistic --- 10 packets transmitted, 4 packets received, 60% packet loss round-trip min/avg/max = 2.9/777.9/3095.3 m
從 hping3 的輸出中,我們可以發現,發送了 10 個請求包,卻只收到了 5 個回復,50% 的包都丟了。再觀察每個請求的 RTT 可以發現,RTT 也有非常大的波動變化,小的時候只有 3ms,而
大的時候則有 3s。
根據這些輸出,我們基本能判斷,已經發生了丟包現象。可以猜測,3s 的 RTT ,很可能是因為丟包后重傳導致的。那到底是哪里發生了丟包呢?
排查之前,我們可以回憶一下 Linux 的網絡收發流程,先從理論上分析,哪里有可能會發生丟包。你不妨拿出手邊的筆和紙,邊回憶邊在紙上梳理,思考清楚再繼續下面的內容。
在這里,為了幫你理解網絡丟包的原理,我畫了一張圖,你可以保存並打印出來使用:
從圖中你可以看出,可能發生丟包的位置,實際上貫穿了整個網絡協議棧。換句話說,全程都有丟包的可能。比如我們從下往上看:
- 在兩台 VM 連接之間,可能會發生傳輸失敗的錯誤,比如網絡擁塞、線路錯誤等;
- 在網卡收包后,環形緩沖區可能會因為溢出而丟包;
- 在鏈路層,可能會因為網絡幀校驗失敗、QoS 等而丟包;
- 在 IP 層,可能會因為路由失敗、組包大小超過 MTU 等而丟包;
- 在傳輸層,可能會因為端口未監聽、資源占用超過內核限制等而丟包;
- 在套接字層,可能會因為套接字緩沖區溢出而丟包;
- 在應用層,可能會因為應用程序異常而丟包;
此外,如果配置了 iptables 規則,這些網絡包也可能因為 iptables 過濾規則而丟包。
當然,上面這些問題,還有可能同時發生在通信的兩台機器中。不過,由於我們沒對 VM2 做任何修改,並且 VM2 也只運行了一個最簡單的 hping3 命令,這兒不妨假設它是沒有問題的。
為了簡化整個排查過程,我們還可以進一步假設, VM1 的網絡和內核配置也沒問題。這樣一來,有可能發生問題的位置,就都在容器內部了。
現在我們切換回終端一,執行下面的命令,進入容器的終端中:
docker exec -it nginx bash root@nginx:/#
在這里簡單說明一下,接下來的所有分析,前面帶有 root@nginx:/# 的操作,都表示在容器中進行。
那么, 接下來,我們就可以從協議棧中,逐層排查丟包問題。
四、鏈路層
首先,來看最底下的鏈路層。當緩沖區溢出等原因導致網卡丟包時,Linux 會在網卡收發數據的統計信息中,記錄下收發錯誤的次數。
你可以通過 ethtool 或者 netstat ,來查看網卡的丟包記錄。比如,可以在容器中執行下面的命令,查看丟包情況:
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 31 0 0 0 8 0 0 0 BMRU lo 65536 0 0 0 0 0 0 0 0 LRU
實際測試代碼如下:
[root@luoahong ~]# netstat -i Kernel Interface table Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg br-ad2616372f01 1500 6 0 0 0 27 0 0 0 BMU docker0 1500 6 0 0 0 27 0 0 0 BMRU eth0 1500 532 0 0 0 292 0 0 0 BMRU lo 65536 0 0 0 0 0 0 0 0 LRU veth5a876cb 1500 6 0 0 0 38 0 0 0 BM
輸出中的 RX-OK、RX-ERR、RX-DRP、RX-OVR ,分別表示接收時的總包數、總錯誤數、進入Ring Buffer 后因其他原因(如內存不足)導致的丟包數以及 Ring Buffer 溢出導致的丟包數。
TX-OK、TX-ERR、TX-DRP、TX-OVR 也代表類似的含義,只不過是指發送時對應的各個指標。
注意,由於 Docker 容器的虛擬網卡,實際上是一對 veth pair,一端接入容器中用作 eth0,另一端在主機中接入 docker0 網橋中。veth 驅動並沒有實現網絡統
計的功能,所以使用 ethtool -S 命令,無法得到網卡收發數據的匯總信息。
從這個輸出中,我們沒有發現任何錯誤,說明容器的虛擬網卡沒有丟包。不過要注意,如果用 tc
等工具配置了 QoS,那么 tc 規則導致的丟包,就不會包含在網卡的統計信息中。
所以接下來,我們還要檢查一下 eth0 上是否配置了 tc 規則,並查看有沒有丟包。我們繼續容器終端中,執行下面的 tc 命令,不過這次注意添加 -s 選項,以輸出統計信息:
root@nginx:/# 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
實際測試代碼:
[root@luoahong ~]# tc -s qdisc show dev eth0 qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1 Sent 35109 bytes 304 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0
從 tc 的輸出中可以看到, eth0 上面配置了一個網絡模擬排隊規則(qdisc netem),並且配置了丟包率為 30%(loss 30%)。再看后面的統計信息,發送了 8 個包,但是丟了 4 個。
看來,應該就是這里,導致 Nginx 回復的響應包,被 netem 模塊給丟了。
既然發現了問題,解決方法也就很簡單了,直接刪掉 netem 模塊就可以了。我們可以繼續在容
器終端中,執行下面的命令,刪除 tc 中的 netem 模塊:
root@nginx:/# 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 命令,就可以看到協議的收發匯總,以及錯誤信息了:
root@nginx:/# 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 重傳數 ...
實際測試代碼:
root@luoahong:~# netstat -s Ip: Forwarding: 2 478 total packets received 4 with invalid addresses 0 forwarded 0 incoming packets discarded 474 incoming packets delivered 329 requests sent out 20 outgoing packets dropped Icmp: 40 ICMP messages received 0 input ICMP message failed ICMP input histogram: destination unreachable: 40 42 ICMP messages sent 0 ICMP messages failed ICMP output histogram: destination unreachable: 42 IcmpMsg: InType3: 40 OutType3: 42 Tcp: 2 active connection openings 1 passive connection openings 0 failed connection attempts 0 connection resets received 1 connections established 266 segments received 172 segments sent out 2 segments retransmitted 0 bad segments received 11 resets sent Udp: 52 packets received 42 packets to unknown port received 0 packet receive errors 93 packets sent 0 receive buffer errors 0 send buffer errors IgnoredMulti: 73 UdpLite: TcpExt: 3 delayed acks sent 66 packet headers predicted 20 acknowledgments not containing data payload received 30 predicted acknowledgments TCPTimeouts: 1 TCPLossProbes: 1 TCPRcvCoalesce: 145 TCPOFOQueue: 4 TCPOrigDataSent: 68 TCPKeepAlive: 2 IpExt: InBcastPkts: 73 InOctets: 530734 OutOctets: 28599 InBcastOctets: 10601 InNoECTPkts: 657
netstat 匯總了 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 協議有多次超時和失敗重試,並且主要錯誤是半連接重置。換句話說,主要的失敗,都是三次握手失敗。
不過,雖然在這兒看到了這么多失敗,但具體失敗的根源還是無法確定。所以,我們還需要繼續順着協議棧來分析。接下來的幾層又該如何分析呢?你不妨自己先來思考操作一下,下一節我們
繼續來一起探討。
六、小結
網絡丟包,通常會帶來嚴重的性能下降,特別是對 TCP 來說,丟包通常意味着網絡擁塞和重傳,進一步還會導致網絡延遲增大、吞吐降低。
今天的這個案例,我們學會了如何從鏈路層、網絡層和傳輸層等入手,分析網絡丟包的問題。不過,案例最后,我們還沒有找出最終的性能瓶頸,下一節,我將繼續為你講解。