一、上節回顧
上一節,我們學習了碰到分布式拒絕服務(DDoS)的緩解方法。簡單回顧一下,DDoS利用大量的偽造請求,導致目標服務要耗費大量資源,來處理這些無效請求,進而無法正
常響應正常用戶的請求。
由於 DDoS 的分布式、大流量、難追蹤等特點,目前確實還沒有方法,能夠完全防御DDoS 帶來的問題,我們只能設法緩解 DDoS 帶來的影響。
比如,你可以購買專業的流量清洗設備和網絡防火牆,在網絡入口處阻斷惡意流量,只保留正常流量進入數據中心的服務器。
在 Linux 服務器中,你可以通過內核調優、DPDK、XDP 等多種方法,增大服務器的抗攻擊能力,降低 DDoS 對正常服務的影響。而在應用程序中,你可以利用各級緩存、
WAF、CDN 等方式,緩解 DDoS 對應用程序的影響。
不過要注意,如果 DDoS 的流量,已經到了 Linux 服務器中,那么,即使應用層做了各種優化,網絡服務的延遲一般還是會比正常情況大很多。
所以,在實際應用中,我們通常要讓 Linux 服務器,配合專業的流量清洗以及網絡防火牆設備,一起來緩解這一問題。
除了 DDoS 會帶來網絡延遲增大外,我想,你肯定見到過不少其他原因導致的網絡延遲,比如
- 網絡傳輸慢,導致延遲;
- Linux 內核協議棧報文處理慢,導致延遲;
- 應用程序數據處理慢,導致延遲等等。
那么,當碰到這些原因的延遲時,我們該怎么辦呢?又該如何定位網絡延遲的根源呢?今天,我就通過一個案例,帶你一起看看這些問題
二、網絡延遲
我相信,提到網絡延遲時,你可能輕松想起它的含義——網絡數據傳輸所用的時間。不過要注意,這個時間可能是單向的,指從源地址發送到目的地址的單程時間;也可能是雙向
的,即從源地址發送到目的地址,然后又從目的地址發回響應,這個往返全程所用的時間。
通常,我們更常用的是雙向的往返通信延遲,比如 ping 測試的結果,就是往返延時RTT(Round-Trip Time)。
除了網絡延遲外,另一個常用的指標是應用程序延遲,它是指,從應用程序接收到請求,再到發回響應,全程所用的時間。通常,應用程序延遲也指的是往返延遲,是網絡數據傳
輸時間加上數據處理時間的和。
在 Linux 網絡基礎篇 中,我曾經介紹到,你可以用 ping 來測試網絡延遲。ping 基於ICMP 協議,它通過計算 ICMP 回顯響應報文與 ICMP 回顯請求報文的時間差,來獲得往
返延時。這個過程並不需要特殊認證,常被很多網絡攻擊利用,比如端口掃描工具nmap、組包工具 hping3 等等。
所以,為了避免這些問題,很多網絡服務會把 ICMP 禁止掉,這也就導致我們無法用 ping,來測試網絡服務的可用性和往返延時。這時,你可以用 traceroute 或 hping3 的 TCP
和 UDP 模式,來獲取網絡延遲。
比如,以 baidu.com 為例,你可以執行下面的 hping3 命令,測試你的機器到百度搜索服務器的網絡延遲:
# -c 表示發送 3 次請求,-S 表示設置 TCP SYN,-p 表示端口號為 80 $ hping3 -c 3 -S -p 80 baidu.com HPING baidu.com (eth0 123.125.115.110): S set, 40 headers + 0 data bytes len=46 ip=123.125.115.110 ttl=51 id=47908 sport=80 flags=SA seq=0 win=8192 rtt=20.9 ms len=46 ip=123.125.115.110 ttl=51 id=6788 sport=80 flags=SA seq=1 win=8192 rtt=20.9 ms len=46 ip=123.125.115.110 ttl=51 id=37699 sport=80 flags=SA seq=2 win=8192 rtt=20.9 ms --- baidu.com hping statistic --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 20.9/20.9/20.9 ms
從 hping3 的結果中,你可以看到,往返延遲 RTT 為 20.9ms。
當然,我們用 traceroute ,也可以得到類似結果:
# --tcp 表示使用 TCP 協議,-p 表示端口號,-n 表示不對結果中的 IP 地址執行反向域名解析 $ traceroute --tcp -p 80 -n baidu.com traceroute to baidu.com (123.125.115.110), 30 hops max, 60 byte packets 1 * * * 2 * * * 3 * * * 4 * * * 5 * * * 6 * * * 7 * * * 8 * * * 9 * * * 10 * * * 11 * * * 12 * * * 13 * * * 14 123.125.115.110 20.684 ms * 20.798 ms
traceroute 會在路由的每一跳發送三個包,並在收到響應后,輸出往返延時。如果無響應或者響應超時(默認 5s),就會輸出一個星號。
知道了基於 TCP 測試網絡服務延遲的方法后,接下來,我們就通過一個案例,來學習網絡延遲升高時的分析思路。
三、案例准備
下面的案例仍然基於 Ubuntu 18.04,同樣適用於其他的 Linux 系統。我使用的案例環境是這樣的:
- 機器配置:2 CPU,8GB 內存。
- 預先安裝 docker、hping3、tcpdump、curl、wrk、Wireshark 等工具,比如 apt-getinstall docker.io hping3 tcpdump curl。
這里的工具你應該都比較熟悉了,其中 wrk 的安裝和使用方法在 怎么評估系統的網絡性能中曾經介紹過。如果你還沒有安裝,請執行下面的命令來安裝它:
https://github.com/wg/wrk $ cd wrk $ apt-get install build-essential -y $ make $ sudo cp wrk /usr/local/bin/
由於 Wireshark 需要圖形界面,如果你的虛擬機沒有圖形界面,就可以把 Wireshark 安裝到其他的機器中(比如 Windows 筆記本)。
本次案例用到兩台虛擬機,我畫了一張圖來表示它們的關系。
接下來,我們打開兩個終端,分別 SSH 登錄到兩台機器上(以下步驟,假設終端編號與圖示 VM 編號一致),並安裝上面提到的這些工具。注意, curl 和 wrk 只需要安裝在客戶
端 VM(即 VM2)中。
同以前的案例一樣,下面的所有命令都默認以 root 用戶運行,如果你是用普通用戶身份登陸系統,請運行 sudo su root 命令切換到 root 用戶。
如果安裝過程中有什么問題,同樣鼓勵你先自己搜索解決,解決不了的,可以在留言區向我提問。如果你以前已經安裝過了,就可以忽略這一點了。
接下來,我們就進入到案例操作的環節。
四、案例分析
為了對比得出延遲增大的影響,首先,我們來運行一個最簡單的 Nginx,也就是用官方的Nginx 鏡像啟動一個容器。在終端一中,執行下面的命令,運行官方 Nginx,它會在 80端口監聽:
docker run --network=host --name=good -itd nginx fb4ed7cb9177d10e270f8320a7fb64717eac3451114c9fab3c50e02be2e88ba2
繼續在終端一中,執行下面的命令,運行案例應用,它會監聽 8080 端口:
docker run --name nginx --network=host -itd feisky/nginx:latency b99bd136dcfd907747d9c803fdc0255e578bad6d66f4e9c32b826d75b6812724
然后,在終端二中執行 curl 命令,驗證兩個容器已經正常啟動。如果一切正常,你將看到如下的輸出:
# 80 端口正常 $ curl http://192.168.0.30 <!DOCTYPE html> <html> ... <p><em>Thank you for using nginx.</em></p> </body> </html> # 8080 端口正常 $ curl http://192.168.0.30:8080 ... <p><em>Thank you for using nginx.</em></p> </body> </html>
接着,我們再用上面提到的 hping3 ,來測試它們的延遲,看看有什么區別。還是在終端二,執行下面的命令,分別測試案例機器 80 端口和 8080 端口的延遲:
# 測試 80 端口延遲 $ 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=64 DF id=0 sport=80 flags=SA seq=0 win=29200 rtt=7.8 ms len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=80 flags=SA seq=1 win=29200 rtt=7.7 ms len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=80 flags=SA seq=2 win=29200 rtt=7.6 ms --- 192.168.0.30 hping statistic --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 7.6/7.7/7.8 ms
8080端口
# 測試 8080 端口延遲 $ hping3 -c 3 -S -p 8080 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=64 DF id=0 sport=8080 flags=SA seq=0 win=29200 rtt=7.7 ms len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=8080 flags=SA seq=1 win=29200 rtt=7.6 ms len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=8080 flags=SA seq=2 win=29200 rtt=7.3 ms --- 192.168.0.30 hping statistic --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 7.3/7.6/7.7 ms
從這個輸出你可以看到,兩個端口的延遲差不多,都是 7ms。不過,這只是單個請求的情況。換成並發請求的話,又會怎么樣呢?接下來,我們就用 wrk 試試。
這次在終端二中,執行下面的新命令,分別測試案例機器並發 100 時, 80 端口和 8080端口的性能:
# 測試 80 端口性能 $ # wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30/ Running 10s test @ http://192.168.0.30/ 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.19ms 12.32ms 319.61ms 97.80% Req/Sec 6.20k 426.80 8.25k 85.50% Latency Distribution 50% 7.78ms 75% 8.22ms 90% 9.14ms 99% 50.53ms 123558 requests in 10.01s, 100.15MB read Requests/sec: 12340.91 Transfer/sec: 10.00MB
8080端口性能:
# 測試 8080 端口性能 $ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/ Running 10s test @ http://192.168.0.30:8080/ 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 43.60ms 6.41ms 56.58ms 97.06% Req/Sec 1.15k 120.29 1.92k 88.50% Latency Distribution 50% 44.02ms 75% 44.33ms 90% 47.62ms 99% 48.88ms 22853 requests in 10.01s, 18.55MB read Requests/sec: 2283.31 Transfer/sec: 1.85MB
從上面兩個輸出可以看到,官方 Nginx(監聽在 80 端口)的平均延遲是 9.19ms,而案例 Nginx 的平均延遲(監聽在 8080 端口)則是 43.6ms。從延遲的分布上來看,官方
Nginx 90% 的請求,都可以在 9ms 以內完成;而案例 Nginx 50% 的請求,就已經達到了 44 ms。
再結合上面 hping3 的輸出,我們很容易發現,案例 Nginx 在並發請求下的延遲增大了很多,這是怎么回事呢?
分析方法我想你已經想到了,上節課學過的,使用 tcpdump 抓取收發的網絡包,分析網絡的收發過程有沒有問題。
接下來,我們在終端一中,執行下面的 tcpdump 命令,抓取 8080 端口上收發的網絡包,並保存到 nginx.pcap 文件:
tcpdump -nn tcp port 8080 -w nginx.pcap
然后切換到終端二中,重新執行 wrk 命令:
# 測試 8080 端口性能 $ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
當 wrk 命令結束后,再次切換回終端一,並按下 Ctrl+C 結束 tcpdump 命令。然后,再把抓取到的 nginx.pcap ,復制到裝有 Wireshark 的機器中(如果 VM1 已經帶有圖形界
面,那么可以跳過復制步驟),並用 Wireshark 打開它。
由於網絡包的數量比較多,我們可以先過濾一下。比如,在選擇一個包后,你可以單擊右鍵並選擇 “Follow” -> “TCP Stream”,如下圖所示
然后,關閉彈出來的對話框,回到 Wireshark 主窗口。這時候,你會發現 Wireshark 已經自動幫你設置了一個過濾表達式 tcp.stream eq 24。如下圖所示(圖中省去了源和目的 IP地址):
實際測試截圖:
從這里,你可以看到這個 TCP 連接從三次握手開始的每個請求和響應情況。當然,這可能還不夠直觀,你可以繼續點擊菜單欄里的 Statics -> Flow Graph,選中 “Limit to
display filter” 並設置 Flow type 為 “TCP Flows”:
實際測試截圖:
注意,這個圖的左邊是客戶端,而右邊是 Nginx 服務器。通過這個圖就可以看出,前面三次握手,以及第一次 HTTP 請求和響應還是挺快的,但第二次 HTTP 請求就比較慢了,特
別是客戶端在收到服務器第一個分組后,40ms 后才發出了 ACK 響應(圖中藍色行)。看到 40ms 這個值,你有沒有想起什么東西呢?實際上,這是 TCP 延遲確認(Delayed
ACK)的最小超時時間。
這里我解釋一下延遲確認。這是針對 TCP ACK 的一種優化機制,也就是說,不用每次請求都發送一個 ACK,而是先等一會兒(比如 40ms),看看有沒有“順風車”。如果這段
時間內,正好有其他包需要發送,那就捎帶着 ACK 一起發送過去。當然,如果一直等不到其他包,那就超時后單獨發送 ACK。
因為案例中 40ms 發生在客戶端上,我們有理由懷疑,是客戶端開啟了延遲確認機制。而這兒的客戶端,實際上就是前面運行的 wrk。
查詢 TCP 文檔(執行 man tcp),你就會發現,只有 TCP 套接字專門設置了TCP_QUICKACK ,才會開啟快速確認模式;否則,默認情況下,采用的就是延遲確認機制:
TCP_QUICKACK (since Linux 2.4.4) Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent imme‐ diately, rather than delayed if needed in accordance to normal TCP operation. This flag is not perma‐ nent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol will once again enter/leave quickack mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer. This option should not be used in code intended to be portable.
為了驗證我們的猜想,確認 wrk 的行為,我們可以用 strace ,來觀察 wrk 為套接字設置了哪些 TCP 選項。
比如,你可以切換到終端二中,執行下面的命令:
strace -f wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/ ... setsockopt(52, SOL_TCP, TCP_NODELAY, [1], 4) = 0 ...
這樣,你可以看到,wrk 只設置了 TCP_NODELAY 選項,而沒有設置 TCP_QUICKACK。這說明 wrk 采用的正是延遲確認,也就解釋了上面這個 40ms 的問題。
不過,別忘了,這只是客戶端的行為,按理來說,Nginx 服務器不應該受到這個行為的影響。那是不是我們分析網絡包時,漏掉了什么線索呢?讓我們回到 Wireshark 重新觀察一下。
實際測試截圖:
仔細觀察 Wireshark 的界面,其中, 1173 號包,就是剛才說到的延遲 ACK 包;下一行的 1175 ,則是 Nginx 發送的第二個分組包,它跟 697 號包組合起來,構成一個完整的
HTTP 響應(ACK 號都是 85)。
第二個分組沒跟前一個分組(697 號)一起發送,而是等到客戶端對第一個分組的 ACK后(1173 號)才發送,這看起來跟延遲確認有點像,只不過,這兒不再是 ACK,而是發
送數據。
看到這里,我估計你想起了一個東西—— Nagle 算法(納格算法)。進一步分析案例前,我先簡單介紹一下這個算法。
Nagle 算法,是 TCP 協議中用於減少小包發送數量的一種優化算法,目的是為了提高實際帶寬的利用率。
舉個例子,當有效負載只有 1 字節時,再加上 TCP 頭部和 IP 頭部分別占用的 20 字節,整個網絡包就是 41 字節,這樣實際帶寬的利用率只有 2.4%(1/41)。往大了說,如果整
個網絡帶寬都被這種小包占滿,那整個網絡的有效利用率就太低了。
Nagle 算法正是為了解決這個問題。它通過合並 TCP 小包,提高網絡帶寬的利用率。Nagle 算法規定,一個 TCP 連接上,最多只能有一個未被確認的未完成分組;在收到這個
分組的 ACK 前,不發送其他分組。這些小分組會被組合起來,並在收到 ACK 后,用同一個分組發送出去。
顯然,Nagle 算法本身的想法還是挺好的,但是知道 Linux 默認的延遲確認機制后,你應該就不這么想了。因為它們一起使用時,網絡延遲會明顯。如下圖所示:
當 Sever 發送了第一個分組后,由於 Client 開啟了延遲確認,就需要等待 40ms 后才會回復 ACK。
既然可能是 Nagle 的問題,那該怎么知道,案例 Nginx 有沒有開啟 Nagle 呢?查詢 tcp 的文檔,你就會知道,只有設置了 TCP_NODELAY 后,Nagle 算法才會禁用。
所以,我們只需要查看 Nginx 的 tcp_nodelay 選項就可以了。
TCP_NODELAY If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor uti‐ lization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
我們回到終端一中,執行下面的命令,查看案例 Nginx 的配置:
docker exec nginx cat /etc/nginx/nginx.conf | grep tcp_nodelay tcp_nodelay off;
果然,你可以看到,案例 Nginx 的 tcp_nodelay 是關閉的,將其設置為 on ,應該就可以解決了。
# 刪除案例應用 $ docker rm -f nginx # 啟動優化后的應用 $ docker run --name nginx --network=host -itd feisky/nginx:nodelay
改完后,問題是否就解決了呢?自然需要驗證我們一下。修改后的應用,我已經打包到了Docker 鏡像中,在終端一中執行下面的命令,你就可以啟動它:
接着,切換到終端二,重新執行 wrk 測試延遲:
wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/ Running 10s test @ http://192.168.0.30:8080/ 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.58ms 14.98ms 350.08ms 97.91% Req/Sec 6.22k 282.13 6.93k 68.50% Latency Distribution 50% 7.78ms 75% 8.20ms 90% 9.02ms 99% 73.14ms 123990 requests in 10.01s, 100.50MB read Requests/sec: 12384.04 Transfer/sec: 10.04MB
果然,現在延遲已經縮短成了 9ms,跟我們測試的官方 Nginx 鏡像是一樣的(Nginx 默認就是開啟 tcp_nodelay 的) 。
作為對比,我們用 tcpdump ,抓取優化后的網絡包(這兒實際上抓取的是官方 Nginx 監聽的 80 端口)。你可以得到下面的結果:
從圖中你可以發現,由於 Nginx 不用再等 ACK,536 和 540 兩個分組是連續發送的;而客戶端呢,雖然仍開啟了延遲確認,但這時收到了兩個需要回復 ACK 的包,所以也不用等
40ms,可以直接合並回復 ACK。
案例最后,不要忘記停止這兩個容器應用。在終端一中,執行下面的命令,就可以刪除案例應用:
五、小結
今天,我們學習了網絡延遲增大后的分析方法。網絡延遲,是最核心的網絡性能指標。由於網絡傳輸、網絡包處理等各種因素的影響,網絡延遲不可避免。但過大的網絡延遲,會
直接影響用戶的體驗。
所以,在發現網絡延遲增大后,你可以用 traceroute、hping3、tcpdump、Wireshark、strace 等多種工具,來定位網絡中的潛在問題。比如,
- 使用 hping3 以及 wrk 等工具,確認單次請求和並發請求情況的網絡延遲是否正常。
- 使用 traceroute,確認路由是否正確,並查看路由中每一跳網關的延遲。
- 使用 tcpdump 和 Wireshark,確認網絡包的收發是否正常。
- 使用 strace 等,觀察應用程序對網絡套接字的調用情況是否正常。
這樣,你就可以依次從路由、網絡包的收發、再到應用程序等,逐層排查,直到定位問題根源。