47講案例篇:服務器總是時不時丟包,我該怎么辦(上)


上⼀節,我們梳理了,應⽤程序容器化后性能下降的分析⽅法。⼀起先簡單回顧下。
 
容器利⽤ 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 ⽤戶運⾏,如果你⽤普通⽤戶身份登陸系統,請運⾏ sudo su 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 
dae0202cc27e5082b282a6aeeb1398fcec423c642e63322da2a97b9ebd7538e0
然后,執⾏ 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
不過,從 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
從 hping3 的輸出中,我們可以發現,發送了 10 個請求包,卻只收到了 5 個回復,50% 的包都丟了。再觀察每個請求的 RTT
可以發現,RTT 也有⾮常⼤的波動變化,⼩的時候只有 3ms,⽽⼤的時候則有 3s。
根據這些輸出,我們基本能判斷,已經發⽣了丟包現象。可以猜測,3s 的 RTT ,很可能是因為丟包后重傳導致的。那到底是
哪⾥發⽣了丟包呢?
 
排查之前,我們可以回憶⼀下 Linux 的⽹絡收發流程,先從理論上分析,哪⾥有可能會發⽣丟包。你不妨拿出⼿邊的筆和紙,
邊回憶邊在紙上梳理,思考清楚再繼續下⾯的內容。
在這⾥,為了幫你理解⽹絡丟包的原理,我畫了⼀張圖,你可以保存並打印出來使⽤:
從 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
輸出中的 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
從 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重傳數 
...
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 來說,丟包通常意味着⽹絡擁塞和重傳,進⼀步還會導致⽹絡延遲增
⼤、吞吐降低。
今天的這個案例,我們學會了如何從鏈路層、⽹絡層和傳輸層等⼊⼿,分析⽹絡丟包的問題。不過,案例最后,我們還沒有找
出最終的性能瓶頸,下⼀節,我將繼續為你講解。
 
思考
最后,給你留⼀個思考題,也是案例最后提到的問題。
今天我們只分析了鏈路層、⽹絡層以及傳輸層等。⽽根據 TCP/IP 協議棧和 Linux ⽹絡收發原理,還有很多我們沒分析到的地
⽅。那么,接下來,我們⼜該如何分析,才能破獲這個案例,找出“真凶”呢?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM