簡單的 HTTP 調(diào)用,為什么時延這么大?

2021-02-27    分類: 網(wǎng)站建設(shè)

1. 背景

最近項目測試遇到個奇怪的現(xiàn)象,在測試環(huán)境通過 Apache HttpClient 調(diào)用后端的 HTTP 服務(wù),平均耗時居然接近 39.2ms??赡苣阏б豢从X得這不是很正常嗎,有什么好奇怪的?其實不然,我再來說下一些基本信息,該后端的 HTTP 服務(wù)并沒有什么業(yè)務(wù)邏輯,只是將一段字符串轉(zhuǎn)成大寫然后返回,字符串長度也僅只有 100 字符,另外網(wǎng)絡(luò) ping 延時只有 1.9ms左右。因此,理論上該調(diào)用耗時應(yīng)該在 2-3ms 左右,但為什么平均耗時 39.2ms 呢?

由于工作原因,調(diào)用耗時的問題,對我來說,已經(jīng)見怪不怪了,經(jīng)常會幫業(yè)務(wù)解決內(nèi)部 RPC 框架調(diào)用超時的相關(guān)問題,但是 HTTP 調(diào)用耗時***次遇到。不過,排查問題的套路是一樣的。主要方法論無外乎由外而內(nèi)、至上而下等排查方法。我們先來看看外圍的一些指標,看能否發(fā)現(xiàn)蛛絲馬跡。

2. 外圍指標

2.1 系統(tǒng)指標

主要看外圍的一些系統(tǒng)指標(注意:調(diào)用與被調(diào)用的機器都要看)。例如負載、CPU。只需一個 top 命令就能一覽無余。

因此,確認了下 CPU 和負載都很空閑。由于當時沒有截圖,這里就不放圖了。

2.2 進程指標

Java 程序進程指標主要看 GC、線程堆棧情況(注意:調(diào)用與被調(diào)用的機器都要看)。

Young GC 都非常少,而且耗時也在 10ms 以內(nèi),因此沒有長時間的 STW。

因為平均調(diào)用時間 39.2ms,比較大,如果耗時是代碼導(dǎo)致,線程堆棧應(yīng)該能發(fā)現(xiàn)點啥??戳酥笠粺o所獲,服務(wù)的相關(guān)線程堆棧主要表現(xiàn)是線程池的線程在等任務(wù),這就意味著線程并不忙。

是不是感覺黔驢技窮了,接下來該怎么辦呢?

3. 本地復(fù)現(xiàn)

如果本地(本地是 MAC 系統(tǒng))能復(fù)現(xiàn),對排查問題也是極好的。

因此在本地使用 Apache HttpClient 寫了個簡單 Test 程序,直接調(diào)用后端的 HTTP 服務(wù),發(fā)現(xiàn)平均耗時在 55ms 左右。咦,怎么跟測試環(huán)境 39.2ms 的結(jié)果有點區(qū)別。主要是本地與測試環(huán)境的后端的 HTTP 服務(wù)機器跨地區(qū)了,ping 時延在 26ms 左右,所以延時增大了。不過本地確實也是存在問題的,因為ping 時延是 26ms,后端 HTTP 服務(wù)邏輯簡單,幾乎不耗時,因此本地調(diào)用平均耗時應(yīng)該在 26ms 左右,為什么是 55ms?

是不是越來越迷惑,一頭霧水,不知如何下手?

期間懷疑過 Apache HttpClient 是不是有什么地方使用的不對,因此使用 JDK 自帶的 HttpURLConnection 寫了簡單的程序,做了測試,結(jié)果一樣。

4. 診斷

4.1 定位

其實從外圍的系統(tǒng)指標、進程指標,以及本地復(fù)現(xiàn)來看,大致能夠斷定不是程序上的原因。那 TCP 協(xié)議層面呢?

有網(wǎng)絡(luò)編程經(jīng)驗的同學(xué)一定知道 TCP 什么參數(shù)會引起這個現(xiàn)象。對,你猜的沒錯,就是 TCP_NODELAY。

那調(diào)用方和被調(diào)用方哪邊的程序沒有設(shè)置呢?

調(diào)用方使用的是 Apache HttpClient ,tcpNoDelay 默認設(shè)置的就是 true。我們再來看看被調(diào)用方,也就是我們的后端 HTTP 服務(wù),這個 HTTP 服務(wù)用的是 JDK自帶的 HttpServer

  1. HttpServer server = HttpServer.create(new InetSocketAddress(config.getPort()), BACKLOGS); 

居然沒看到直接設(shè)置 tcpNoDelay 接口,翻了下源碼。哦,原來在這里,在 ServerConfig 的類中有這段靜態(tài)塊,用來獲取啟動參數(shù),默認 ServerConfig.noDelay 為 false。

  1. static { 
  2.  
  3. AccessController.doPrivileged(new PrivilegedAction() { 
  4.  
  5. public Void run() { 
  6.  
  7. ServerConfig.idleInterval = Long.getLong("sun.net.httpserver.idleInterval", 30L) * 1000L; 
  8.  
  9. ServerConfig.clockTick = Integer.getInteger("sun.net.httpserver.clockTick", 10000); 
  10.  
  11. ServerConfig.maxIdleConnections = Integer.getInteger("sun.net.httpserver.maxIdleConnections", 200); 
  12.  
  13. ServerConfig.drainAmount = Long.getLong("sun.net.httpserver.drainAmount", 65536L); 
  14.  
  15. ServerConfig.maxReqHeaders = Integer.getInteger("sun.net.httpserver.maxReqHeaders", 200); 
  16.  
  17. ServerConfig.maxReqTime = Long.getLong("sun.net.httpserver.maxReqTime", -1L); 
  18.  
  19. ServerConfig.maxRspTime = Long.getLong("sun.net.httpserver.maxRspTime", -1L); 
  20.  
  21. ServerConfig.timerMillis = Long.getLong("sun.net.httpserver.timerMillis", 1000L); 
  22.  
  23. ServerConfig.debug = Boolean.getBoolean("sun.net.httpserver.debug"); 
  24.  
  25. ServerConfig.noDelay = Boolean.getBoolean("sun.net.httpserver.nodelay"); 
  26.  
  27. return null; 
  28.  
  29.  
  30. }); 
  31.  

4.2 驗證

在后端 HTTP 服務(wù),加上啟動"-Dsun.net.httpserver.nodelay=true"參數(shù),再試試。效果很明顯,平均耗時從39.2ms 降到 2.8ms。

問題是解決了,但是到這里如果你就此止步,那就太便宜了這個案例了,簡直暴殄天物。因為還有一堆疑惑等著你呢?

  • 為什么加了 TCP_NODELAY ,時延就從 39.2ms 降低到 2.8ms?
  • 為什么本地測試的平均時延是 55ms,而不是 ping 的時延 26ms?
  • TCP 協(xié)議究竟是怎么發(fā)送數(shù)據(jù)包的?

來,我們接著乘熱打鐵。

5. 解惑

5.1 TCP_NODELAY 何許人也?

在 Socket 編程中,TCP_NODELAY 選項是用來控制是否開啟 Nagle 算法。在 Java 中,為 ture 表示關(guān)閉 Nagle 算法,為 false 表示打開 Nagle 算法。你一定會問 Nagle 算法是什么?

5.2 Nagle 算法是什么鬼?

Nagle 算法是一種通過減少通過網(wǎng)絡(luò)發(fā)送的數(shù)據(jù)包數(shù)量來提高 TCP/IP 網(wǎng)絡(luò)效率的方法。它使用發(fā)明人 John Nagle 的名字來命名的,John Nagle 在 1984 年***用這個算法來嘗試解決福特汽車公司的網(wǎng)絡(luò)擁塞問題。

試想如果應(yīng)用程序每次產(chǎn)生 1 個字節(jié)的數(shù)據(jù),然后這 1 個字節(jié)數(shù)據(jù)又以網(wǎng)絡(luò)數(shù)據(jù)包的形式發(fā)送到遠端服務(wù)器,那么就很容易導(dǎo)致網(wǎng)絡(luò)由于太多的數(shù)據(jù)包而過載。在這種典型情況下,傳送一個只擁有1個字節(jié)有效數(shù)據(jù)的數(shù)據(jù)包,卻要花費 40 個字節(jié)長包頭(即 IP 頭部 20 字節(jié) + TCP 頭部 20 字節(jié))的額外開銷,這種有效載荷(payload)的利用率是極其低下。

Nagle 算法的內(nèi)容比較簡單,以下是偽代碼:

  1. if there is new data to send 
  2.  
  3. if the window size >= MSS and available data is >= MSS 
  4.  
  5. send complete MSS segment now 
  6.  
  7. else 
  8.  
  9. if there is unconfirmed data still in the pipe 
  10.  
  11. enqueue data in the buffer until an acknowledge is received 
  12.  
  13. else 
  14.  
  15. send data immediately 
  16.  
  17. end if 
  18.  
  19. end if 
  20.  
  21. end if 

具體的做法就是:

  • 如果發(fā)送內(nèi)容大于等于 1 個 MSS, 立即發(fā)送;
  • 如果之前沒有包未被 ACK, 立即發(fā)送;
  • 如果之前有包未被 ACK, 緩存發(fā)送內(nèi)容;
  • 如果收到 ACK, 立即發(fā)送緩存的內(nèi)容。(MSS 為 TCP 數(shù)據(jù)包每次能夠傳輸?shù)?**數(shù)據(jù)分段)

5.3 Delayed ACK 又是什么玩意?

大家都知道 TCP 協(xié)議為了保證傳輸?shù)目煽啃?,?guī)定在接受到數(shù)據(jù)包時需要向?qū)Ψ桨l(fā)送一個確認。只是單純的發(fā)送一個確認,代價會比較高(IP 頭部 20 字節(jié) + TCP 頭部 20 字節(jié))。TCP Delayed ACK(延遲確認)就是為了努力改善網(wǎng)絡(luò)性能,來解決這個問題的,它將幾個 ACK 響應(yīng)組合合在一起成為單個響應(yīng),或者將 ACK 響應(yīng)與響應(yīng)數(shù)據(jù)一起發(fā)送給對方,從而減少協(xié)議開銷。

具體的做法是:

  • 當有響應(yīng)數(shù)據(jù)要發(fā)送時,ACK 會隨響應(yīng)數(shù)據(jù)立即發(fā)送給對方;
  • 如果沒有響應(yīng)數(shù)據(jù),ACK 將會延遲發(fā)送,以等待看是否有響應(yīng)數(shù)據(jù)可以一起發(fā)送。在 Linux 系統(tǒng)中,默認這個延遲時間是 40ms;
  • 如果在等待發(fā)送 ACK 期間,對方的第二個數(shù)據(jù)包又到達了,這時要立即發(fā)送 ACK。但是如果對方的三個數(shù)據(jù)包相繼到達,第三個數(shù)據(jù)段到達時是否立即發(fā)送 ACK,則取決于以上兩條。

5.4 Nagle 與 Delayed ACK 一起會發(fā)生什么化學(xué)反應(yīng)?

Nagle 與 Delayed ACK 都能提高網(wǎng)絡(luò)傳輸?shù)男?,但在一起會好心辦壞事。例如,以下這個場景:

A 和 B 進行數(shù)據(jù)傳輸 : A 運行 Nagle 算法,B 運行 Delayed ACK 算法。

如果 A 向 B 發(fā)一個數(shù)據(jù)包,B 由于 Delayed ACK 不會立即響應(yīng)。而 A 使用 Nagle 算法,A 就會一直等 B 的 ACK,ACK 不來一直不發(fā)送第二個數(shù)據(jù)包,如果這兩個數(shù)據(jù)包是應(yīng)對同一個請求,那這個請求就會被耽誤了 40ms。

5.5 抓個包玩玩吧

我們來抓個包驗證下吧,在后端HTTP服務(wù)上執(zhí)行以下腳本,就可以輕松完成抓***程。

  1. sudo tcpdump -i eth0 tcp and host 10.48.159.165 -s 0 -w traffic.pcap 

如下圖,這是使用 Wireshark 分析包內(nèi)容的展示,紅框內(nèi)是一個完整的 POST 請求處理過程,看 130 序號和 149 序號之間相差 40ms(0.1859 - 0.1448 = 0.0411s = 41ms),這個就是 Nagle 與 Delayed ACK 一起發(fā)送的化學(xué)反應(yīng),其中 10.48.159.165 運行的是 Delayed ACK,10.22.29.180 運行的是 Nagle 算法。10.22.29.180 在等 ACK,而 10.48.159.165 觸發(fā)了 Delayed ACK,這樣傻傻等了 40ms。

這也就解釋了為什么測試環(huán)境耗時是 39.2ms,因為大部分都被 Delayed ACK 的 40ms 給耽誤了。

但是本地復(fù)現(xiàn)時,為什么本地測試的平均時延是 55ms,而不是 ping 的時延 26ms?我們也來抓個包吧。

如下圖,紅框內(nèi)是一個完整的 POST 請求處理過程,看 8 序號和 9 序號之間相差 25ms 左右,再減去網(wǎng)絡(luò)延時約是ping延時的一半 13ms,因此 Delayed Ack 約 12ms 左右(由于本地是 MAC 系統(tǒng)與 Linux 有些差異)。

1. Linux 使用的是 /proc/sys/net/ipv4/tcp_delack_min 這個系統(tǒng)配置來控制 Delayed ACK 的時間,Linux 默認是 40ms;

2. MAC 是通過 net.inet.tcp.delayed_ack 系統(tǒng)配置來控制 Delayed ACK 的。

delayed_ack=0 responds after every packet (OFF) delayed_ack=1 always employs delayed ack, 6 packets can get 1 ack delayed_ack=2 immediate ack after 2nd packet, 2 packets per ack (Compatibility Mode) delayed_ack=3 should auto detect when to employ delayed ack, 4packets per ack. (DEFAULT)設(shè)置為 0 表示禁止延遲 ACK,設(shè)置為 1 表示總是延遲 ACK,設(shè)置為 2 表示每兩個數(shù)據(jù)包回復(fù)一個 ACK,設(shè)置為 3 表示系統(tǒng)自動探測回復(fù) ACK 的時機。

5.6 為什么 TCP_NODELAY 能夠解決問題?

TCPNODELAY 關(guān)閉了 Nagle 算法,即使上個數(shù)據(jù)包的 ACK 沒有到達,也會發(fā)送下個數(shù)據(jù)包,進而打破 Delayed ACK 造成的影響。一般在網(wǎng)絡(luò)編程中,強烈建議開啟 TCPNODELAY,來提升響應(yīng)速度。

當然也可以通過 Delayed ACK 相關(guān)系統(tǒng)的配置來解決問題,但由于需要修改機器配置,很不方便,因此,這種方式不太推薦。

6. 總結(jié)

本文是從一個簡單的 HTTP 調(diào)用,時延比較大而引發(fā)的一次問題排查過程。過程中,首先由外而內(nèi)的分析了相關(guān)問題,然后定位問題并驗證解決方案。***刨根問底對 TCP 傳輸?shù)闹械?Nagle 與 Delayed ACK 做了全面的講解,更加透測的剖析了該問題案例。

作者:滌生,姓名殷琦,目前就職于某大型互聯(lián)網(wǎng)公司基礎(chǔ)架構(gòu)部,主要負責微服務(wù)框架、服務(wù)治理、Serverless 相關(guān)工作。 《高可用可伸縮微服務(wù)架構(gòu):基于Dubbo、Spring Cloud和Service Mesh》的作者之一。

網(wǎng)頁標題:簡單的 HTTP 調(diào)用,為什么時延這么大?
網(wǎng)站鏈接:http://muchs.cn/news/103240.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供App開發(fā)、網(wǎng)站建設(shè)電子商務(wù)、做網(wǎng)站、網(wǎng)站策劃、移動網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

綿陽服務(wù)器托管