說一下請求是異步的為什么會造成阻塞?


HTTP 協議的隊首阻塞

隊首阻塞:隊首的事情沒有處理完的時候,后面的都要等着。

HTTP1.1 的隊首阻塞

HTTP1.1 版本上使用了一種 Pipelining 管道技術來並行發送和處理多個請求。讓客戶端能夠並行發送多個請求,服務器端也可以並行處理多個來自客戶端的請求。在一個 TCP 連接中,發送多個 HTTP 請求,不需要等待服務器端對前一個請求的響應之后,再發送下一個請求。但是使用了管道技術的 HTTP/1.1,根據 HTTP/1.1 的規則,服務器端在響應時,要嚴格按照接收請求的順序發送,即先接收到的請求,需要先發送其響應,客戶端瀏覽器也是如此,接收響應的順序要按照自己發送請求的順序來。這樣造成的問題是,如果最先收到的請求的處理時間長的話,響應生成也慢,就會阻塞已經生成了的響應的發送,也會造成隊首阻塞。

總的來說,管道技術允許客戶端和服務器端並行發送多個請求和響應,但是客戶端接收響應的順序要和自己發送請求的順序對應,服務器端發送響應的順序要和自己接收到的請求的順序對應,這樣做似乎沒什么問題,看起來是不是“FIFO"先來先服務的方式,如果前面收到的一個請求,在服務器端處理的時間很長,生成響應需要很多時間,那么對於后面的已經處理完生成響應的請求來說,它們只能阻塞等待,等待前面的響應發送完后,自己才能被發送出去(即使該請求的響應已經生成)造成了“隊首阻塞”問題。可見隊首阻塞發生在服務器端,雖然服務器端並行接收了多個請求,也並行處理生成多個響應,但由於要遵守 HTTP/1.1 的規則,先接收到的請求需要先發送響應,造成了阻塞問題。

另外需要注意的是,雖然 HTTP/1.1 規范中規定了 Pipelining 管道技術來並行發送和處理多個請求,但是這個功能在瀏覽器中默認是關閉的。

看一下 Pipelining 是什么,RFC 2616 中的規定:一個支持持久連接的客戶端可以在一個連接中發送多個請求(不需要等待任意請求的響應)。收到請求的服務器必須按照請求收到的順序發送響應。 至於標准為什么這么設定,我們可以大概推測一個原因:由於 HTTP/1.1 是個文本協議,同時返回的內容也並不能區分對應於哪個發送的請求,所以順序必須維持一致。比如你向服務器發送了兩個請求GET/query?q=A和GET /query?q=B,服務器返回了兩個結果,瀏覽器是沒有辦法根據響應結果來判斷響應是對應於哪一個請求的。

Pipelining 這種設想看起來比較美好,但是在實踐中會出現許多問題:

  • 一些代理服務器不能正確的處理 HTTP Pipelining。

  • 正確的流水線實現是復雜的

  • Head-of-line Blocking 連接頭阻塞:在建立起一個 TCP 連接之后,假設客戶端在這個連接連續向服務器發送了幾個請求。按照標准,服務器應該按照收到請求的順序返回結果,假設服務器在處理首個請求時花費了大量時間,那么后面所有的請求都需要等着首個請求結束才能響應。

所以現代瀏覽器默認是不開啟 HTTP Pipelining 的。

HTTP2 隊首阻塞

對於 HTTP1.1 中管道化導致的請求/響應級別的隊頭阻塞,可以使用 HTTP2 解決。HTTP2 不使用管道化的方式,而是引入了幀、消息和數據流等概念,每個請求/響應被稱為消息,每個消息都被拆分成若干個幀進行傳輸,每個幀都分配一個序號。每個幀在傳輸是屬於一個數據流,而一個連接上可以存在多個流,各個幀在流和連接上獨立傳輸,到達之后在組裝成消息,這樣就避免了請求/響應阻塞。

當然,即使使用 HTTP2,如果 HTTP2 底層使用的是 TCP 協議,仍可能出現 TCP 隊頭阻塞。因為 HTTP/2 並沒有解決 TCP 的隊首阻塞問題,它僅僅是通過多路復用解決了以前 HTTP1.1 管線化請求時的隊首阻塞。比如 HTTP/1.1 時代建立一個 TCP 連接,三個請求組成一個隊列發出去,服務器接收到這個隊列之后會依次響應,一旦前面的請求阻塞,后面的請求就會無法響應。HTTP/2 是通過分幀並且給每個幀打上流的 ID 去避免依次響應的問題,對方接收到幀之后根據 ID 拼接出流,

這樣就可以做到亂序響應從而避免請求時的隊首阻塞問題。但是 TCP 層面的隊首阻塞是 HTTP/2 無法解決的(HTTP 只是應用層協議,TCP 是傳輸層協議),TCP 的阻塞問題是因為傳輸階段可能會丟包,一旦包就會等待重新發包,阻塞后續傳輸,這個問題雖然有滑動窗口(Sliding Window)這個方案,但是只能增強抗干擾,並沒有徹底解決。

TCP 隊首阻塞

隊首阻塞(head-of-line blocking)發生在一個 TCP 分節丟失,導致其后續分節不按序到達接收端的時候。該后續分節將被接收端一直保持直到丟失的第一個分節被發送端重傳並到達接收端為止。該后續分節的延遲遞送確保接收應用進程能夠按照發送端的發送順序接收數據。這種為了達到完全有序而引入的延遲機制非常有用,但也有不利之處。

假設在單個 TCP 連接上發送語義獨立的消息,比如說服務器可能發送 3 幅不同的圖像供 Web 瀏覽器顯示。為了營造這幾幅圖像在用戶屏幕上並行顯示的效果,服務器先發送第一幅圖像的一個斷片,再發送第二幅圖像的一個斷片,然后再發送第三幅圖像的一個斷片;服務器重復這個過程,直到這 3 幅圖像全部成功地發送到瀏覽器為止。要是第一幅圖像的某個斷片內容的 TCP 分節丟失了,客戶端將保持已到達的不按序的所有數據,直到丟失的分節重傳成功。這樣不僅延緩了第一幅圖像數據的遞送,也延緩了第二幅和第三幅圖像數據的遞送。

如何解決 TCP 隊頭阻塞

TCP 中的隊頭阻塞的產生是由 TCP 自身的實現機制決定的,無法避免。想要在應用程序當中避免 TCP 隊頭阻塞帶來的影響,只有舍棄 TCP 協議。比如 google 推出的 quic 協議,在某種程度上可以說避免了 TCP 中的隊頭阻塞,因為它根本不使用 TCP 協議,而是在 UDP 協議的基礎上實現了可靠傳輸。而 UDP 是面向數據報的協議,數據報之間不會有阻塞約束。

此外還有一個 SCTP(流控制傳輸協議),它是和 TCP、UDP 在同一層次的傳輸協議。SCTP 的多流特性也可以盡可能的避免隊頭阻塞的情況。

總結

從 TCP 隊頭阻塞和 HTTP 隊頭阻塞的原因我們可以看到,出現隊頭阻塞的原因有兩個:

  • 獨立的消息數據都在一個鏈路上傳輸,也就是有一個“隊列”。比如 TCP 只有一個流,多個 HTTP 請求共用一個 TCP 連接

  • 隊列上傳輸的數據有嚴格的順序約束。比如 TCP 要求數據嚴格按照序號順序,HTTP 管道化要求響應嚴格按照請求順序返回

所以要避免隊頭阻塞,就需要從以上兩個方面出發,比如 quic 協議不使用 TCP 協議而是使用 UDP 協議,SCTP 協議支持一個連接上存在多個數據流等等。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM