真正「搞」懂HTTP協定07之隊頭阻塞真的很煩人

2023-01-11 06:00:58

  這一篇文章,我們核心要聊的事情就是HTTP的對頭阻塞問題,因為HTTP的核心改進其實就是在解決HTTP的隊頭阻塞。所以,我們會講的理論多一些,而實踐其實很少,要學習的頭欄位也只有一個,我會在最開始就講完這個頭欄位,然後我們安心的去學習接下來的理論知識,嗯……這些理論知識很重要。

  那我們就先來看看我們本篇要學的這個唯一的頭欄位是什麼吧。

一、Connection頭欄位及其範例

  其實在聊這個欄位之前,我們要先學一些前置知識才好,但是我想先講這個欄位,後面再帶著疑問去學理論,嗯……就這樣。

  Connection欄位其實很好理解,就是用來開啟長連結的,長連結的意思就是會重複利用TCP開啟的通道,不會在請求一次後關閉。長連結可以這樣開啟Connection: keep-alive。這個東東在HTTP/1.1中是預設開啟的,也就是說你啥也不干它就開啟,當然,如果你想關閉的話可以這樣Connection: close

  不僅僅如此,使用者端和伺服器都可以通過Keep-alive: timeout=value來限定長連結的超時時間,但是伺服器和使用者端往往都不一定遵守,約束力並不是很強。大家瞭解下就好了,另外,一些代理伺服器比如Nginx,也會針對該欄位有一些特殊的策略,比如該通道多長時間沒有傳送資料就關閉,比如該通道傳送了多少次資料後就關閉等等。

  那麼下面我們就來看看具體的例子,來實踐一下。使用者端和伺服器的程式碼都很簡單,基本的程式碼在上一篇文章中都接觸過,我就不再複製程式碼了,可以在這裡看。我們直接看下請求的結果。

  關於Connection的有三個欄位,其中Proxy-Connection從詞意上來講就是指代理的通道連線方式。然後你看,Connection是Keep-alive,Keep-Alive欄位的超時時間設定為4,也就是預設設定了四秒沒有在該通道上傳輸資料就關閉TCP通道。那怎麼驗證呢?我們還得用下Wireshark。先請求下資料,然後看看四秒後會不會有TCP的四次揮手斷開連線。

   我們可以清楚的看到三次握手後(也就是96、97、98三個id的tcp)的HTTP請求,過了四秒,就四次揮手(就是344、345、346、347四次)斷開連線了,大家有興趣自行嘗試體驗一下。

  例子就這麼簡單~我們接下來要學習理論知識了,這些理論知識很重要,這個例子就當個開胃小菜吧。

二、長短連線說短長

  我在上面的例子中說到,HTTP/1.1會預設開啟長連結,那為什麼要開啟長連結?什麼是長連結?那既然有長連結是不是還有短連線呢?嗯……你聽我慢慢說。

一)短連線是什麼? 

   我們知道,HTTP/0.9和HTTP/1.0都是十分簡單的協定,它的底層是基於TCP的,在每次請求傳送前都需要通過三次握手來和伺服器建立連線,響應結束後會通過四次揮手斷開連線。這就是短連線,在早期的時候也會被稱為是無連線的。而這種操作是十分浪費資源的,效率就很低下。

  為什麼早期的HTTP會是這個樣子呢?因為在當時,大家知道大多數的網站都是靜態頁面,一個頁面上能放幾個gif圖那都是很酷炫的事情了。所以,在這樣的場景下,沒有那麼多的請求需要,這樣設計似乎也無可厚非,但是隨著網際網路頁面的極速發展,一個頁面可能有幾個甚至幾十個請求,幾百個外部資原始檔,每次都開啟、關閉、開啟、關閉,浪費的資源可不是一點半點。

  所以,我們就需要對短連線進行改進,於是長連線出現了。

二)時代的寵兒:長連線

  因為短連線實在是無法適應時代的需要,太浪費了,所以為了解決短連線帶來問題,在HTTP/1.1中就增加了持久連結的方法,它的特點就是可以在一個TCP連線上傳輸多個HTTP請求,只要瀏覽器或者伺服器沒有明確斷開,那麼就會一直保持連線狀態。雖然這樣做並沒有改善TCP的連線效率,但是由於開啟和斷開的次數少了,把整個開啟和斷開的時間平均到了多次請求中,每個請求和應答的無效時間就少了很多,從而增加了整體傳輸的效率。

  在目前的瀏覽器中,同一個域名可以開啟六個TCP連線,不過這裡稍微要注意的是,其實HTTP規範約定的TCP連線數量是2個,但是各大瀏覽器廠商覺得肯定不夠用,所以在實現層面上來說,每個域名可以開啟6個TCP連線,HTTP規範在新的版本RFC7230中也就順水推舟,約定可以是6到8個連線。

  那如果六個TCP連線還是不夠用呢?嗯……我們可以多開幾個域名,比如a.zaking.com,b.zaking.com,c.zaking.com,每一個域名都指向同一臺伺服器,說白了就是用數量來解決質量的方式,而這種解決思路也有個高大上的名詞,叫做「域名分片」。

三、初識隊頭阻塞

  隊頭阻塞是本篇的重點,也是一件比較有趣的事情,有趣在哪裡呢?因為它解決不了。我們下面就來看看什麼是隊頭阻塞。

  因為HTTP是基於「請求—應答」模型的,在這個模型的基礎上,HTTP規定報文必須是一發一收的,這就形成了一個先進先出的序列佇列,如果你不知道什麼是佇列的話,請看這裡。既然是佇列,就存在一個這樣的問題,佇列裡的請求沒有優先順序,誰先進來誰就先出去。但是假如某一個排在前面的請求卡住了,沒有返回,那後面的所有佇列中的請求都要等著那個卡住的請求結束,結果就是我分擔了本來不應該由我來承擔的時間損耗。

  那要怎麼解決這個問題呢?誒?你不是說這個問題是解決不了的麼?嗯……從規範上,從設計上來說確實無法解決,既然是佇列就必然是這樣的,但是上有政策下有對策,我大不了多開幾個域名唄,多開幾個佇列,讓它堵的可能性小點,你是不是想到了啥?嗯,就是我們上面說到的「域名分片」技術。

  其實很好理解,就好像我們在一個汽車在單車道上跑,堵車的可能性就很大,堵車了我也沒辦法,只能等前面解決了繼續走,但是假設我是6車道,18車道,是不是就能在一定程度上解決這個問題了。

  當然,你並沒有從根本解決隊頭阻塞。只是使了點小手段罷了。

  我在demo程式碼裡寫了點小例子,大家可以點選試試。坦白說我並不知道底層的實現是什麼,但是大概能猜到原因。

 

  當你傳送很多無響應的HTTP請求後,等一會,再點有響應的HTTP請求,你會發現卡死了,我猜就是因為那些無響應的HTTP請求佔用全部六個TCP連線。當然,你也可以通過Wireshark來驗證這一點。不多說啦~

  完了嘛?還沒……

  在HTTP/1.1中,也曾試圖通過「管線化」 的技術來解決隊頭阻塞的問題,管線化就是指將多個HTTP請求整批傳送給伺服器的技術,雖然可以整批傳送,但是伺服器還是要按照佇列的順序返回結果,得~~~白玩了。所以最後這玩意沒啥用,大家瞭解下就行了

四、多路複用的HTTP/2

   我們回顧一下上面的三個部分,發現HTTP/1.1為了優化做了哪些努力,一個是長連線,一個是每個域名可以同時維護6個TCP長連線,一個是就是域名分片技術。一共三種,但是這些手段都沒有從根本上解決隊頭阻塞的問題,HTTP資料包文在傳輸的某一條連線上堵塞了,還是要等待,沒辦法。

  雖然使用這些手段一定程度上緩解了HTTP/1.0和HTTP/0.9所帶來的問題,但是其實問題還是不少的,效能還可以進一步的壓縮。

  其中關於TCP的問題有慢啟動問題,以及頻寬競爭問題。在TCP進入到傳輸資料的狀態時,會處於一種遞增的狀態,就像開車一樣,緩慢的從0加速到多少時速,這樣做是為了減少網路擁塞,但是有些資料本身就很小,等著你慢慢啟動就很浪費時間。

  而頻寬競爭,則是指當頻寬充足的時候,每條連線都會緩慢的增加傳送速度,而一旦頻寬不足時,這些連線傳輸資料的速度則會減慢,這樣就回帶來一個問題,就是優先順序的問題,重要資源隨著普通資源一起減慢了,真的是很苦惱。

  這兩個問題是TCP引起的,HTTP想改變也改變不了,只能接受,所以HTTP/3的時候乾脆不用TCP了。

  但是,隊頭阻塞的問題,則是HTTP可以進一步優化和解決的,想辦法在一定程度上規避TCP的這兩個問題,什麼意思呢?

  HTTP/2的思路是一個域名只採用一個TCP連線,這樣就能儘可能的減少TCP的慢啟動和頻寬競爭問題,就一個TCP連線,你也不用競爭了,就一個TCP連線,你就算啟動的很慢,平分到每一個連線好像也還可以。你看,好像所有的解決思路都類似,解決不了就平分。

  基於這樣的思路,HTTP/2針對隊頭阻塞的問題提出了多路複用的解決方案。什麼意思呢,HTTP/2實現了資源的並行請求,也就是你在任何時候都可以傳送請求,不用管前一個請求是否堵塞,伺服器會在處理好資料後就返回給你。

  那,核心的問題來了,多路複用是如何實現的呢?

多路複用的實現

  HTTP/2在HTTP和TCP的中間又加了一層,也就是二進位制分幀層:

 

   就像上圖這樣,其實二進位制分幀層屬於應用層,這個二進位制分幀層做了什麼呢?就是把傳送的HTTP封包拆成一個一個帶有id的幀,伺服器收到這些幀後,會把有同一個id的幀合併成一條完整的資訊,那麼同樣的,伺服器傳送給使用者端的資料也要這樣經過二進位制分幀層的分幀處理,瀏覽器會根據對應的id傳送給請求的資料來源頭。

  HTTP/2就是通過這樣的形式,引入了多路複用的機制,來解決隊頭阻塞的問題。

  看起來似乎很美好了是吧,HTTP/2可以說是HTTP目前為止最完美的解決方案了。但是故事並沒有就此停止。

五、TCP也有隊頭阻塞

   雖然,HTTP/2解決了HTTP的隊頭阻塞,但是TCP也有隊頭阻塞,雖然你把HTTP封包拆分成了一個又一個的幀,但是你還是傳輸在一條通道上,一旦某一個資料框丟失了,那TCP就得等丟失的封包重新傳過來才行,臥槽,問題又回到了原點。那咋整?我們改一改TCP協定?

  抱歉,你改不了,主要的原因在於僵化,一個是中間裝置的僵化,一個是作業系統的僵化。

  中間裝置其實就是指資料在網際網路中傳輸的過程中,所遇到的各種裝置,比如路由器,閘道器,代理伺服器,伺服器等等等等,很多很多,這些東西比較硬性,一旦安裝軟體後很少升級,所以你改了使用者端的TCP,這一連串的裝置,甚至說全球的裝置都要改,你想想,是不是很誇張。

  而作業系統僵化,則是因為TCP的核心實現是由作業系統底層來處理的,所以你看,要改TCP就要改作業系統,想想就頭大。

  所以,由於僵化的原因,TCP改不了。那咋整?嗯……那就不用他了唄,我們用UDP好了。

  HTTP/3選擇用UDP作為傳輸協定,並且在UDP和HTTP/3中又加了QUIC層。QUIC層則針對UDP區別於TCP的一些特性進行了處理,從而讓UDP的傳輸像TCP一樣完整和安全,並且像HTTP/2那樣採用多路複用機制,來解決TCP的隊頭阻塞。

  關於QUIC或者HTTP/3的更多內容,會在後面HTTP/3的部分詳細講解,本篇就不再過多的闡述了。

  嗯……本篇結束了~

六、總結

  這篇文章並不長,理論知識稍微多一點,而其中最核心的點就是隊頭阻塞和多路複用,大家一定要著重學習。那麼在本篇的最後,留給大家兩個小問題。

  1. 長連線和短連線是啥?長連線出現的原因是什麼?解決了什麼問題呢?
  2. 關於HTTP的隊頭阻塞,你都有哪些瞭解?HTTP解決了隊頭阻塞的問題麼?如果解決了,又是如何解決的?如果沒解決,為什麼沒解決呢?