1. 程式人生 > 實用技巧 >40 | 案例篇:網路請求延遲變大了,我該怎麼辦?

40 | 案例篇:網路請求延遲變大了,我該怎麼辦?

上一節,學習了碰到分散式拒絕服務(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-get install 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 埠延遲
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 埠效能
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 響應(圖中藍色行)。

TCP 延遲確認

看到 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)。

Nagle

第二個分組沒跟前一個分組(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。
  • 同時,由於 Server 端開啟了 Nagle,而這時還沒收到第一個分組的 ACK,Server 也會在這裡一直等著。
  • 直到 40ms 超時後,Client 才會回覆 ACK,然後,Server 才會繼續傳送第二個分組。
既然可能是 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 映象中,在終端一中執行下面的命令,你就可以啟動它:
# 刪除案例應用
docker rm -f nginx
 
# 啟動優化後的應用
docker run --name nginx --network=host -itd feisky/nginx:nodelay
接著,切換到終端二,重新執行 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。 案例最後,不要忘記停止這兩個容器應用。在終端一中,執行下面的命令,就可以刪除案例應用:

docker rm -f nginx good

小結

今天,我們學習了網路延遲增大後的分析方法。網路延遲,是最核心的網路效能指標。由於網路傳輸、網路包處理等各種因素的影響,網路延遲不可避免。但過大的網路延遲,會直接影響使用者的體驗。 所以,在發現網路延遲增大後,你可以用 traceroute、hping3、tcpdump、Wireshark、strace 等多種工具,來定位網路中的潛在問題。比如,
  • 使用 hping3 以及 wrk 等工具,確認單次請求和併發請求情況的網路延遲是否正常。
  • 使用 traceroute,確認路由是否正確,並檢視路由中每一跳閘道器的延遲。
  • 使用 tcpdump 和 Wireshark,確認網路包的收發是否正常。
  • 使用 strace 等,觀察應用程式對網路套接字的呼叫情況是否正常。
這樣,你就可以依次從路由、網路包的收發、再到應用程式等,逐層排查,直到定位問題根源。 思考 最後,我想邀請你一起來聊聊,你所理解的網路延遲,以及在發現網路延遲增大時,你又是怎麼分析的呢?你可以結合今天的內容,和你自己的操作記錄,來總結思路。 網上查了Nagle演算法的定義:任意時刻,最多隻能有一個未被確認的小段。所謂“小段”,指的是小於MSS尺寸的資料塊,所謂“未被確認”,是指一個數據塊傳送出去後,沒有收到對方傳送的ACK確認該資料已收到。 對比80埠和8080埠的報文,80埠的報文中,響應訊息再發送完header後立刻傳送body部分;8080埠的報文,響應訊息再發送完header後,需要獲得ACK響應後,才會傳送body部分。 8080埠報文中server端在獲得到ACK響應後才傳送body部分,就是因為Nagle演算法必須確認這個資料塊被收到的原因。client在40ms後傳送ACK是因為客戶端沒有開啟TCP_QUICKACK的緣故。