影響HTTP效能的常見因素

昀溪發表於2019-04-27

影響HTTP效能的常見因素

我們這裡討論HTTP效能是建立在一個最簡單模型之上就是單臺伺服器的HTTP效能,當然對於大規模負載均衡叢集也適用畢竟這種叢集也是由多個HTTTP伺服器的個體所組成。另外我們也排除客戶端或者伺服器本身負載過高或者HTTP協議實現的軟體使用不同的IO模型,另外我們也忽略DNS解析過程和web應用程式開發本身的缺陷。

從TCP/IP模型來看,HTTP下層就是TCP層,所以HTTP的效能很大程度上取決於TCP效能,當然如果是HTTPS的就還要加上TLS/SSL層,不過可以肯定的是HTTPS效能肯定比HTTP要差,其通訊過程這裡都不多說總之層數越多效能損耗就越嚴重。

在上述條件下,最常見的影響HTTP效能的包括:

  • TCP連線建立,也就是三次握手階段

  • TCP慢啟動

  • TCP延遲確認

  • Nagle演算法

  • TIME_WAIT積累與埠耗盡

  • 服務端埠耗盡

  • 服務端HTTP程式開啟檔案數量達到最大

TCP連線建立

通常如果網路穩定TCP連線建立不會消耗很多時間而且也都會在合理耗時範圍內完成三次握手過程,但是由於HTTP是無狀態的屬於短連線,一次HTTP會話接受後會斷開TCP連線,一個網頁通常有很多資源這就意味著會進行很多次HTTP會話,而相對於HTTP會話來說TCP三次握手建立連線就會顯得太耗時了,當然可以透過重用現有連線來減少TCP連線建立次數。

TCP慢啟動

TCP擁塞控制手段[1],在TCP剛建立好之後的最初傳輸階段會限制連線的最大傳輸速度,如果資料傳輸成功,後續會逐步提高傳輸速度,這就TCP慢啟動。慢啟動限制了某一時刻可以傳輸的IP分組[2]數量。那麼為什麼會有慢啟動呢?主要是為了避免網路因為大規模的資料傳輸而癱瘓,在網際網路上資料傳輸很重要的一個環節就是路由器,而路由器本身的速度並不快加之網際網路上的很多流量都可能傳送過來要求其進行路由轉發,如果在某一時間內到達路由器的資料量遠大於其傳送的數量那麼路由器在本地快取耗盡的情況下就會丟棄資料包,這種丟棄行為被稱作擁塞,一個路由器出現這種狀況就會影響很多條鏈路,嚴重的會導致大面積癱瘓。所以TCP通訊的任何一方需要進行擁塞控制,而慢啟動就是擁塞控制的其中一種演算法或者叫做機制。
你設想一種情況,我們知道TCP有重傳機制,假設網路中的一個路由器因為擁塞而出現大面積丟包情況,作為資料的傳送方其TCP協議棧肯定會檢測到這種情況,那麼它就會啟動TCP重傳機制,而且該路由器影響的傳送方肯定不止你一個,那麼大量的傳送方TCP協議棧都開始了重傳那這就等於在原本擁塞的網路上傳送更多的資料包,這就等於火上澆油。

透過上面的描述得出即便是在正常的網路環境中,作為HTTP報文的傳送方與每個請求建立TCP連線都會受到慢啟動影響,那麼又根據HTTP是短連線一次會話結束就會斷開,可以想象客戶端發起HTTP請求剛獲取完web頁面上的一個資源,HTTP就斷開,有可能還沒有經歷完TCP慢啟動過程這個TCP連線就斷開了,那web頁面其他的後續資源還要繼續建立TCP連線,而每個TCP連線都會有慢啟動階段,這個效能可想而知,所以為了提升效能,我們可以開啟HTTP的持久連線也就是後面要說的keepalive。

另外我們知道TCP中有視窗的概念,這個視窗在傳送方和接收方都有,視窗的作用一方面保證了傳送和接收方在管理分組時變得有序;另外在有序的基礎上可以傳送多個分組從而提高了吞吐量;還有一點就是這個視窗大小可以調整,其目的是避免傳送方傳送資料的速度比接收方接收的速度快。視窗雖然解決了通訊雙發的速率問題,可是網路中會經過其他網路裝置,傳送方怎麼知道路由器的接收能力呢?所以就有了上面介紹的擁塞控制。

TCP延遲確認

首先要知道什麼是確認,它的意思就是傳送方給接收方傳送一個TCP分段,接收方收到以後要回傳一個確認表示收到,如果在一定時間內傳送方沒有收到該確認那麼就需要重發該TCP分段。

確認報文通常都比較小,也就是一個IP分組可以承載多個確認報文,所以為了避免過多的傳送小報文,那麼接收方在回傳確認報文的時候會等待看看有沒有發給接收方的其他資料,如果有那麼就把確認報文和資料一起放在一個TCP分段中傳送過去,如果在一定時間內容通常是100-200毫秒沒有需要傳送的其他資料那麼就將該確認報文放在單獨的分組中傳送。其實這麼做的目的也是為了儘可能降低網路負擔。

一個通俗的例子就是物流,卡車的載重是一定的,假如是10噸載重,從A城市到B城市,你肯定希望它能儘可能的裝滿一車貨而不是來了一個小包裹你就立刻起身開向B城市。

所以TCP被設計成不是來一個資料包就馬上返回ACK確認,它通常都會在快取中積攢一段時間,如果還有相同方向的資料就捎帶把前面的ACK確認回傳過去,但是也不能等的時間過長否則對方會認為丟包了從而引發對方的重傳。

對於是否使用以及如何使用延遲確認不同作業系統會有不同,比如Linux可以啟用也可以關閉,關閉就意味著來一個就確認一個,這也是快速確認模式。

需要注意的是是否啟用或者說設定多少毫秒,也要看場景。比如線上遊戲場景下肯定是儘快確認,SSH會話可以使用延遲確認。

對於HTTP來說我們可以關閉或者調整TCP延遲確認。

Nagle演算法

這個演算法其實也是為了提高IP分組利用率以及降低網路負擔而設計,這裡面依然涉及到小報文和全尺寸報文(按乙太網的標準MTU1500位元組一個的報文計算,小於1500的都算非全尺寸報文),但是無論小報文怎麼小也不會小於40個位元組,因為IP首部和TCP首部就各佔用20個位元組。如果你傳送一個50個位元組小報文,其實這就意味著有效資料太少,就像延遲確認一樣小尺寸包在區域網問題不大,主要是影響廣域網。

這個演算法其實就是如果傳送方當前TCP連線中有發出去但還沒有收到確認的報文的時候,那麼此時如果傳送方還有小報文要傳送的話就不能傳送而是要放到緩衝區等待之前發出報文的確認,收到確認之後,傳送方會收集快取中同方向的小報文組裝成一個報文進行傳送。其實這也就意味著接收方返回ACK確認的速度越快,傳送方傳送資料也就越快。

現在我們說說延遲確認和Nagle演算法結合將會帶來的問題。其實很容易看出,因為有延遲確認,那麼接收方則會在一段時間內積攢ACK確認,而傳送方在這段時間內收不到ACK那麼也就不會繼續傳送剩下的非全尺寸資料包(資料被分成多個IP分組,傳送方要傳送的響應資料的分組數量不可能一定是1500的整數倍,大機率會發生資料尾部的一些資料就是小尺寸IP分組),所以你就看出這裡的矛盾所在,那麼這種問題在TCP傳輸中會影響傳輸效能那麼HTTP又依賴TCP所以自然也會影響HTTP效能,通常我們會在伺服器端禁用該演算法,我們可以在作業系統上禁用或者在HTTP程式中設定TCP_NODELAY來禁用該演算法。比如在Nginx中你可以使用tcp_nodelay on;來禁用。

TIME_WAIT積累與埠耗盡[3]

這裡指的是作為客戶端的一方或者說是在TCP連線中主動關閉的一方,雖然伺服器也可以主動發起關閉,但是我們這裡討論的是HTTP效能,由於HTTP這種連線的特性,通常都是客戶端發起主動關閉,

客戶端發起一個HTTP請求(這裡說的是對一個特定資源的請求而不是開啟一個所謂的主頁,一個主頁有N多資源所以會導致有N個HTTP請求的發起)這個請求結束後就會斷開TCP連線,那麼該連線在客戶端上的TCP狀態會出現一種叫做TIME_WAIT的狀態,從這個狀態到最終關閉通常會經過2MSL[4]的時長,我們知道客戶端訪問服務端的HTTP服務會使用自己本機隨機高位埠來連線伺服器的80或者443埠來建立HTTP通訊(其本質就是TCP通訊)這就意味著會消耗客戶端上的可用埠數量,雖然客戶端斷開連線會釋放這個隨機埠,不過客戶端主動斷開連線後,TCP狀態從TIME_WAIT到真正CLOSED之間的這2MSL時長內,該隨機埠不會被使用(如果客戶端又發起對相同伺服器的HTTP訪問),其目的之一是為了防止相同TCP套接字上的髒資料。透過上面的結論我們就知道如果客戶端對伺服器的HTTP訪問過於密集那麼就有可能出現埠使用速度高於埠釋放速度最終導致因沒有可用隨機埠而無法建立連線。

上面我們說過通常都是客戶端主動關閉連線,

TCP/IP詳解 卷1 第二版,P442,最後的一段寫到 對於互動式應用程式而言,客戶端通常執行主動關閉操作並進入TIME_WAIT狀態,伺服器通常執行被動關閉操作並且不會直接進入TIME_WAIT狀態。

不過如果web伺服器並且開啟了keep-alive的話,當達到超時時長伺服器也會主動關閉。(我這裡並不是說TCP/IP詳解錯了,而是它在那一節主要是針對TCP來說,並沒有引入HTTP,而且它說的是通常而不是一定)

我使用Nginx做測試,並且在配置檔案中設定了keepalive_timeout 65s;,Nginx的預設設定是75s,設定為0表示禁用keepalive,如下圖:

下面我使用Chrom瀏覽器訪問這個Nginx預設提供的主頁,並透過抓包程式來監控整個通訊過程,如下圖:

從上圖可以看出來在有效資料傳送完畢後,中間出現了Keep-Alive標記的通訊,並且在65秒內沒有請求後伺服器主動斷開連線,這種情況你在Nginx的伺服器上就會看到TIME_WAIT的狀態。

服務端埠耗盡

有人說Nginx監聽80或者443,客戶端都是連線這個埠,服務端怎麼會埠耗盡呢?就像下圖一樣(忽略圖中的TIME_WAIT,產生這個的原因上面已經說過了是因為Nginx的keepalive_timeout設定導致的)

其實,這取決於Nginx工作模式,我們使用Nginx通常都是讓其工作在代理模式,這就意味著真正的資源或者資料在後端Web應用程式上,比如Tomcat。代理模式的特點是代理伺服器代替使用者去後端獲取資料,那麼此時相對於後端伺服器來說,Nginx就是一個客戶端,這時候Nginx就會使用隨機埠來向後端發起請求,而系統可用隨機埠範圍是一定的,可以使用sysctl net.ipv4.ip_local_port_range命令來檢視伺服器上的隨機埠範圍。

透過我們之前介紹的延遲確認、Nagle演算法以及代理模式下Nginx充當後端的客戶端角色並使用隨機埠連線後端,這就意味著服務端的埠耗盡風險是存在的。隨機埠釋放速度如果比與後端建立連線的速度慢就有可能出現。不過一般不會出現這個情況,至少我們公司的Nginx我沒有發現有這種現象產生。因為首先是靜態資源都在CDN上;其次後端大部分都是REST介面提供使用者認證或者資料庫操作,這些操作其實後端如果沒有瓶頸的話基本都很快。不過話說回來如果後端真的有瓶頸且擴容或者改架構成本比較高的話,那麼當面對大量併發的時候你應該做的是限流防止後端被打死。

服務端HTTP程式開啟檔案數量達到最大

我們說過HTTP通訊依賴TCP連線,一個TCP連線就是一個套接字,對於類Unix系統來說,開啟一個套接字就是開啟一個檔案,如果有100個請求連線服務端,那麼一旦連線建立成功服務端就會開啟100個檔案,而Linux系統中一個程式可以開啟的檔案數量是有限的ulimit -f,所以如果這個數值設定的太小那麼也會影響HTTP連線。而對以代理模式執行的Nginx或者其他HTTP程式來說,通常一個連線它就要開啟2個套接字也就會佔用2個檔案(命中Nginx本地快取或者Nginx直接返回資料的除外)。所以對於代理伺服器這個程式可開啟的檔案數量也要設定的大一點。

持久連線Keepalive

首先我們要知道keepalive可以設定在2個層面上,且2個層面意義不同。TCP的keepalive是一種探活機制,比如我們常說的心跳資訊,表示對方還線上,而這種心跳資訊的傳送由有時間間隔的,這就意味著彼此的TCP連線要始終保持開啟狀態;而HTTP中的keep-alive是一種複用TCP連線的機制,避免頻繁建立TCP連線。所以一定明白TCP的Keepalive和HTTP的Keep-alive不是一回事

HTTP的keep-alive機制

非持久連線會在每個HTTP事務完成後斷開TCP連線,下一個HTTP事務則會再重新建立TCP連線,這顯然不是一種高效機制,所以在HTTP/1.1以及HTTP/1.0的增強版本中允許HTTP在事務結束後將TCP連線保持開啟狀態,以便後續的HTTP事務可以複用這個連線,直到客戶端或者伺服器主動關閉該連線。持久連線減少了TCP連線建立的次數同時也最大化的規避了TCP慢啟動帶來的流量限制。

再來看一下這張圖,圖中的keepalive_timeout 65s設定了開啟http的keep-alive特性並且設定了超時時長為65秒,其實還有比較重要的選項是keepalive_requests 100;它表示同一個TCP連線最多可以發起多少個HTTP請求,預設是100個。

在HTTP/1.0中keep-alive並不是預設使用的,客戶端傳送HTTP請求時必須帶有Connection: Keep-alive的首部來試圖啟用keep-alive,如果伺服器不支援那麼將無法使用,所有請求將以常規形式進行,如果伺服器支援那麼在響應頭中也會包括Connection: Keep-alive的資訊。

在HTTP/1.1中預設就使用Keep-alive,除非特別說明,否則所有連線都是持久的。如果要在一個事務結束後關閉連線,那麼HTTP的響應頭中必須包含Connection: CLose首部,否則該連線會始終保持開啟狀態,當然也不能總是開啟,也必須關閉空閒連線,就像上面Nginx的設定一樣最多保持65秒的空閒連線,超過後服務端將會主動斷開該連線。

TCP的keepalive

在Linux上沒有一個統一的開關去開啟或者關閉TCP的Keepalive功能,檢視系統keepalive的設定sysctl -a | grep tcp_keepalive,如果你沒有修改過,那麼在Centos系統上它會顯示:

net.ipv4.tcp_keepalive_intvl = 75   # 兩次探測直接間隔多少秒
net.ipv4.tcp_keepalive_probes = 9   # 探測頻率
net.ipv4.tcp_keepalive_time = 7200  # 表示多長時間進行一次探測,單位秒,這裡也就是2小時

按照預設設定,那麼上面的整體含義就是2小時探測一次,如果第一次探測失敗,那麼過75秒再探測一次,如果9次都失敗就主動斷開連線。

如何開啟Nginx上的TCP層面的Keepalive,在Nginx中有一個語句叫做listen它是server段裡面用於設定Nginx監聽在哪個埠的語句,其實它後面還有其他引數就是用來設定套接字屬性的,看下面幾種設定:

# 表示開啟,TCP的keepalive引數使用系統預設的
listen       80 default_server so_keepalive=on;
# 表示顯式關閉TCP的keepalive
listen       80 default_server so_keepalive=off;
# 表示開啟,設定30分鐘探測一次,探測間隔使用系統預設設定,總共探測10次,這裡的設
# 置將會覆蓋上面系統預設設定
listen       80 default_server so_keepalive=30m::10;

所以是否要在Nginx上設定這個so_keepalive,取決於特定場景,千萬不要把TCP的keepalive和HTTP的keepalive搞混淆,因為Nginx不開啟so_keepalive也不影響你的HTTP請求使用keep-alive特性。如果客戶端和Nginx直接或者Nginx和後端伺服器之間有負載均衡裝置的話而且是響應和請求都會經過這個負載均衡裝置,那麼你就要注意這個so_keepalive了。比如在LVS的直接路由模式下就不受影響,因為響應不經過
LVS,不過要是NAT模式就需要留意,因為LVS保持TCP會話也有一個時長,如果該時長小於後端返回資料的時長那麼LVS就會在客戶端還沒有收到資料的情況下斷開這條TCP連線。


  1. TCP擁塞控制有一些演算法,其中就包括TCP慢啟動、擁塞避免等演算法 ↩︎

  2. 有些地方也叫做IP分片但都是一個意思,至於為什麼分片簡單來說就是受限於資料鏈路層,不同的資料鏈路其MTU不同,乙太網的是1500位元組,在某些場景會是1492位元組;FDDI的MTU又是另外一種大小,單純考慮IP層那麼IP資料包最大是65535位元組 ↩︎

  3. 在《HTTP權威指南》P90頁中並沒有說的特別清楚,這種情況是相對於客戶端來說還是服務端,因為很有可能讓人誤解,當然並不是說服務端不會出現埠耗盡的情況,所以我這裡才增加了2項內容 ↩︎

  4. 最長不會超過2分鐘 ↩︎

相關文章