隊頭阻塞與連線多工(head-of-line blocking / multiplexing)
/ head-of-line -> HED-of-line /
想像一條單線結帳通道,第一位顧客推著一車要結十分鐘的大採購。後面每個人都卡住,即使各自只拿一件商品——隊伍最前頭擋住了整條隊伍。隊頭阻塞(head-of-line,HOL)正是如此:當許多彼此獨立的請求共用一條有序通道時,最前頭一個緩慢或卡住的項目,會拖延排在它後面的一切,無論那些本來各自有多快。
它在網路裡咬人之處:為避免每個請求都開一條全新連線,現代協定把許多邏輯串流「多工」在一條連線上——HTTP/1.1 的管線化,尤其是 HTTP/2,把數個請求交錯在單一條 TCP 連線上。這有幫助,但 HTTP/2 跑在 TCP 上仍在傳輸層遭受隊頭阻塞:TCP 保證依序送達,所以只要「一個」封包遺失,TCP 就會扣住其後每一個位元組——包括屬於「其他」未受影響串流的位元組——直到那個遺失的封包被重傳。這些串流在邏輯上是獨立的,卻共乘一條有序的 TCP 管道,所以單一次遺失就讓它們全部停滯。這正是 HTTP/3 改用跑在 UDP 上的 QUIC 的原因,它讓每條串流有自己的排序,於是一條串流上的遺失不再凍結其他串流。
為何重要:隊頭阻塞形塑了現實世界的尾端延遲與協定設計。它解釋了為何天真的 HTTP/1.1 管線化被放棄、為何 HTTP/2 多工在易遺失的網路上仍然吃力、以及為何 HTTP/3/QUIC 存在。這個普遍教訓遠遠超出 HTTP:任何時候你把獨立的工作灌進一條嚴格有序的佇列——一條請求管線、一個共享事件處理常式、一條單一 I/O 執行緒——隊頭那個最慢或卡住的項目都可能擋住它後面的一切,所以需要隔離的設計,會給獨立的工作獨立的車道。
/* HTTP/2 跑在 TCP 上:串流 1、3、5 多工在一條連線上 */ /* 單一段 TCP 封包遺失,會在重傳前讓全部三條的送達停滯,即使只有串流 3 的封包遺失 */
多工共用一條有序的 TCP 管道,所以一個遺失的封包會隊頭阻塞每一條串流,直到它被重傳。
HTTP/2 多工治好了 HTTP/1.1 的應用層隊頭阻塞,卻治不了 TCP 內部的傳輸層隊頭阻塞——那需要 QUIC/HTTP/3,其中每條串流各自獨立排序。TCP/IP 協定本身的機制屬於網路領域;這裡的重點純粹是它對 I/O 與排程的後果。