淺談 HTTP/3 與 QUIC
前言
在 2019 八月中旬時,小弟很榮幸地,有機會在 COSCUP2019 的 SDN x Cloud Native x Golang 議軌中擔任講者。當時的題目為 HTTP/3 Leaks,主要是介紹下個世代的 HTTP 協定 — HTTP/3與其所使用的傳輸層協定(Transport Protocol) — QUIC。而似乎是場地限制的關係,本場演講並沒有影片釋出。因此,希望可以藉由這篇文章,簡單地介紹 HTTP/3 與 QUIC,讓更多人了解新協定的特徵功能與運作流程。
值得注意的是,在技術分享與撰寫本文的當下, HTTP/3 與 QUIC 還未確立最終的版本。因此本文的所描述的標準協定是基於時空背景下最新的草稿 — draft-ietf-quic-http-22 與 draft-ietf-quic-transport-22,其內容可能與最終版本有所出入。
簡介
本文章的目的是介紹 HTTP/3 與其底層的 QUIC,其涵蓋的議題包括:
- 為什麼需要新的 HTTP 協定?
- 為什麼需要新的傳輸協定 (transport protocol)?
- QUIC 的特徵
- HTTP/3 與 HTTP/2 的比較
- HTTP/3 常見的批評與顧慮
HTTP/2 的缺點
當我們談論到為什麼需要新的 HTTP 協定時,必須先了解目前的 HTTP 版本 (HTTP/2) 遇到了哪些問題。而大致上可以概括成兩點:
- 傳輸層的 Head-of-Line Blocking
- HTTP 連線建立過程冗長
傳輸層的 Head-of-Line Blocking
Head-of-line blocking (HOL blocking) 是一種效能受限的現象,指得是當前處理的請求受阻而導致整個序列中的請求都受阻的情況。就像排隊結帳一樣,如果當前結帳的人遲遲無法完成,整個排隊的人龍都會收到影響。
由於 HTTP/2 引進了 multiple streams over a single connection (multiplexing) ,因此成功解決了 HTTP 長久以來應用層的 HOL blocking 問題 (來自同一用戶端的 HTTP 請求,必須照接受的順序回應)。但可惜的是,由於 HTTP/2 採用單一 TCP 連線來做資料傳輸,反倒衍生出了傳輸層的 HOL blocking。
傳輸層的 HOL blocking 指的是,在同一個連線當中,即使封包在應用層中是屬於不同的 stream,TCP 依然會要求它們必須依序傳輸。原因在於TCP 是傳輸層協定,並不暸解上方應用層的邏輯,所以對它來說這些封包都屬於同一個連線。因此在網路環境不穩的情境下,不同 Stream 的封包有可能會在傳輸層中因為其他 stream 封包的遺失與重傳而受堵。
HTTP 連線建立過程冗長
HTTP 在建立連線時,必須先完成 TCP 的 3-way handshake,接著如果要採用 secure 連線,還要額外進行 TLS handshake,而這全部加起來需要 3 個 RTT (round trip time)才能完成。這對 HTTP 請求與回應的延遲時間,要求越來越高的現代網路應用而言,是個頭痛的問題。
成也 TCP,敗也 TCP
從上述 HTTP/2 的兩個主要痛點中可以發現,目前主要問題來自 HTTP 所採用傳輸層協定 TCP。TCP 提供了穩定且可靠的封包傳輸機制,也提供了重傳 (re-transmission) 與錯誤處理 (error handling) 等功能,讓基於 TCP 的應用層協定不用擔心資料遺失等問題, HTTP 也是受惠者。但當網路技術越來越發達,基礎網路建設也越來越強健的同時,TCP 繁瑣的溝通機制與缺乏彈性的設計,反倒成為了 HTTP 發展的阻礙。
有鑑於此,為了解決使用 TCP 所造成的問題,在新一代的 HTTP 中,將不再基於 TCP 訂製其傳輸層協定的規格,而是將其描述成兼具效能與可靠性的全新協定。
一個新的傳輸協定的誕生
承上, 由於 TCP 不再適用新一代的 HTTP ,因此勢必得找到其他替代方案,而基本上有兩種選擇 — 既有的傳輸協定或設計新的。在取捨上,除了符合需求外,最重要的議題在於該協定是否可以成功落地。
網路協定的僵化 (Protocol Ossification)
網際網路本身是由非常龐大的網路節點所構成,資料的傳輸必須仰賴兩端點間眾多中間節點的協助才能完成。但網際網路的實體其實並沒有大家所想的那麼進步與高科技,相反地,有大量的中間節點非常陳舊與過時。它們可能有數年至數十年未更新使用的軟體,也認不出許多近年才提出的網路協定與延伸功能。由於這些節點老舊且數量龐大,也造成了新的網路協定非常難以推廣與應用。
基於 UDP 的新傳輸協定
為了可以順利普及,一個全新的傳輸協定想必是不可行的。因此,大家開始把歪腦筋動到了另外一個普及的傳輸協定 — UDP 身上:
- 2012 時,Google 開始設計基於 UDP 的實驗性傳輸協定 — Quick UDP Internet Connections
- 2015 時,Google 正式向 IETF 提出協定草案 (簡稱 gQUIC),內容包含 Google 如何實作與整合 gQUIC 與 HTTP/2 。接著 IETF 於同年成立 QUIC workgroup。
- 2016 時, QUIC workgroup 正式提出協定草案(簡稱 ITEF-QUIC 或 QUIC),並著手擬訂細節。
ITEF-QUIC 與 gQUIC 的概念一致,但實作並不相同:
QUIC 的特徵
QUIC 主要的特徵包括:
- 透過 UDP 的封包傳輸 (transfer protocol over UDP)
- 支援 Connection 與 Stream
- 可靠的資料傳輸 (reliable data transfers)
- 流量控制 (flow control and congestion control)
- 基於 TLS 1.3 的傳輸層加密與 1-RTT 連線建立 ( secure and transport handshakes)
- 支援 0-RTT 快速交握 (fast handshakes and early data)
我們將依序介紹這些特徵。
透過 UDP 的封包傳輸
為了規避網路協定僵化的問題, QUIC 會把資料封裝成加密過的 UDP 封包來進行傳輸:
- 對於中間經手的網路節點而言,它們只會看到一個個加密過的 UDP 封包,因此除了導傳外不會多做處理,藉此提升傳輸效能。
- 對於請求與回應的兩端點而言,則需要在傳輸層負起重傳與錯誤處理等工作,藉此保證資料傳輸的可靠性。
支援 Connection 與 Stream
QUIC 提供了與 HTTP/2 一樣的 multiple streams over a single connection 的概念。差別在於:
- HTTP/2 是傳輸層的 TCP connection + 應用層的 stream。
- QUIC 是傳輸層同時提供了 connection 與 stream。
且由於 QUIC 是透過 UDP 來進行資料傳輸,因此它所提供的 connection 與 stream 皆是抽象邏輯,只存在於 client 與 sever 兩節點身上。
相較於 TCP 協定使用 TCP Socket 來建立連線,QUIC 則是採用 UDP Socket + QUIC connection。有別於 TCP socket 需要綁定用戶與伺服器兩端的 IP 與 port,QUIC connection 則是用特有的 connection ID 來辨認連線。
Connection ID
Connection ID 主要有 destination 與 source 兩種類別,都可以用來辨識接收到的 request 屬於哪一個連線。因此對於節點來說,它除了需要維護自身的 connection ID 與連線的對應外,也必須記錄所有連線對象的 connection ID 與連線的對應。
Connection ID Negotiation
本節將會解釋 client 與 server 如何在建立連線的過程中交換彼此的 connection ID,請搭配下圖服用:
- Client 初始建立連線
・Client 會將自己的 connection ID 填入 request 的 source ID 欄位中。
・將 destination ID 填入一個隨機數字。
・發送 ClientHello。 - Server 收到來自 client 的連線建立請求
・Server 將 request 中的 source ID 保存在自己的 destination ID 清單中。
・在 response 的 source ID 欄位中填入自己的 connection ID。
・在 response 的 destination ID 欄位中填入對方的 connection ID。
・發送 ServerHello ,以通知 client 連線建立成功。 - Client 收到 ServerHello
・Client 將 response 中的 source ID 保存在自己的 destination ID 清單中。
・至此,雙方完成 connection ID 的交換。 - 進行一般的資料傳輸 (request/response)
・在進行一般的資料傳輸時,請求方只會在 request 中填入 destination ID 欄位,並不會攜帶 source ID 欄位。
・QUIC 的連線雙方,會預設對方可以辨識出連線,並用對應的 connection ID 來回應。 - Connection ID 的新增
・一個連線所用的 Connection ID 可以透過傳送 NEW_CONNECTION_ID 這個 frame 來新增。
・交換的過程與一開始的 connection ID 交換是相同的。
Connection Migration
由於 QUIC connection 不再綁定 IP 與 port,所以一個連線的生命週期,與clinet 及 server 的實體網路狀態無關。因此 QUIC 可以實現 TCP 所做不到的 connection migration,即允許雙方節點在不重建連線的情況下更換底層的網路環境。
Streams
QUIC stream 除了實作於傳輸層外,大致上的特性與 HTTP/2 的版本一致:
- 對於同一個 Stream 內的資料,保證其請求與接受順序一致 (in-order delivery within streams)。
- 對於不同 Stream 內的資料,不保證之間的順序 (out-of-order delivery between streams)
- 提供雙向 (bidirectional) 與單向 (unidirectional) 兩種 stream 類別。
此外,也提供了 stream prioritization 與 stream state 等功能,用以控制資料傳輸的順序與流量。
可靠的資料傳輸
由於 QUIC 透過 UDP 來傳輸封包,因此資料的可靠性得仰賴 client 與 sever 兩節點。與 TCP 只提供 connection-level 的資料可靠性相比, QUIC 透過 stream state 的方式提供了 stream-level 的資料可靠性。因此不同stream 的資料重傳與錯誤處理將完全獨立。
流量控制
與資料可靠性相仿, QUIC 的流量控制也必須仰賴 client 與 sever 兩節點負責。QUIC 同時提供了 connection-level 與 stream-level 兩種層級的流量控制。
基於 TLS 1.3 的傳輸層加密與 1-RTT 交握
QUIC 並不提供明文(cleartext)的資料傳輸。且為了提升資料傳輸的安全性, QUIC 只支援 TLS 1.3 以上的傳輸層安全性協定。
除了保障資料傳輸的安全外,採用 TLS 1.3 的最大好處在於它只需要 1-RTT 便可以完成 TLS handshake。而 QUIC 也在進行 TLS handshake 的同時,順道完成整個 QUIC 連線的建立,整體包含 version negotiation、cryptographic 、transport handshake 與 TLS handshake。
相比於 HTTP + TLS 需要 3-RTT 的冗長連線建立過程,只需要 1-RTT 的 QUIC 大幅減少了連線建立的時間。
支援 0-RTT 快速交握
除了 1-RTT 交握模式外,QUIC 也支援了 0-RTT 的快速交握機制。 0-RTT 交握其實依舊需要進行1-RTT 的連線建立,但 client 可以在建立連線的同時,攜帶加密過的請求資料 (early data),server 則在回應連線建立成功的同時,回傳處理結果。
需要注意的是,0-RTT 交握並不是沒有使用上的限制。必須達成以下條件才能使用:
- Client 與 server 必須曾經建立過連線。
- 雙方必須還保有之前連線所使用的 PSK 或certificate。
- Early data 必須用之前連線所用的金鑰加密。
- Server 必須願意支援 0-RTT 快速交握 。由於快速交握必須在還未確認連線建立狀況的階段先行處理資料,除了額外的資源消耗,也有資安上的風險。
HTTP/2 vs HTTP/3
在介紹完 QUIC 的主要特徵後,接著來談談 HTTP/3 在行為與特徵上,與 HTTP/2 有什麼差別。大致上可以統整成下方的比較表:
HTTP/2 與 HTTP/3 的相同點
HTTP/2 與 HTTP/3 兩者皆支援:
- 相同的 HTTP syntax 與 HTTP frame
- Connection 與 stream 的概念
- Stream multiplexing 與 stream prioritization
- Stream-level 的 flow control 與 congestion control
- TLS handshake 與 fast handshake
- Server push
- Head compression
HTTP/2 與 HTTP/3 的相異點
- HTTP/2 的 stream 是實作在應用層,而 HTTP/3 則是實作於傳輸層。因此 HTTP/3 可以避免傳輸層的 HOL blocking 問題,並提供更細緻的 flow control 與 congestion control (應用層 vs 傳輸層)。
- 以傳輸效率來看,HTTP/2 仰賴 TCP socket 進行資料傳輸, HTTP/3 則是透過 UDP Socket + user space 上的 QUIC 實作來達成。 HTTP/3 可以提供更為有效的資料傳輸。
- 以系統資源消耗的角度而言,由於 TCP 有著十幾年的演算法與 Kernal 的優化,甚至有硬體的支援。在面對同樣量級的請求時,目前基於 QUIC 的 HTTP/3 會比基於 TCP 的HTTP/2 消耗更多的系統資源。
- HTTP/3 不再提供明文的資料傳輸。
- HTTP/3 的連線建立比 HTTP/2 更為快速。且相比於 HTTP/2 所用的 TCP fast open,HTTP/3 所用的 TLS fast handshake 更為容易落地普及。
- HTTP/2 使用 HPACK 來進行 header compression ,而 HTTP/3 則是採用QPACK。
- HTTP/3 支援 connection migration,對於行動網路環境的 client 更為友善。
常見的批評與顧慮
HTTP/3 與 QUIC 常見的批評包括:
- UDP will never work.
- UDP is slow in kernels.
- UDP is too easy to DDOS
- QUIC takes too much CPU.
- We are not Google.
- Too small of an improvement.
可以從上發現大家在意的點著重於資安隱憂與資源消耗。
關於安全的部分,其實 QUIC 的規格書中有詳細說明要如何避免 UDP 常見的資安議題,也可以在 QUIC workgroup 的 mailing list 中找到非常多相關的討論,安全的議題是 QUIC workgroup 非常重視的一環,也是決定普及成敗的重要因素。因此,筆者相信最終版本的 QUIC 可以設計出完善的方案,以解釋目前對於 UDP 及 QUIC 的安全疑慮。
至於的資源消耗的問題,主要癥結在於長久以來網路的世界都被 TCP 所統治,效能優化也都是基於 TCP/ IP 的需求而發展。因此基於 UDP 的應用技術勢必會相對弱勢 。但只要 HTTP/3 能被逐漸採納,那相關的優化研究自然地會發展前進,因此這是個在未來預期可被解決的問題。透過 HTTP 這塊大旗來推廣 QUIC ,是 QUIC workgroup 所擬定的發展策略中,非常聰明的一手。
總結
在本文章中,我們談論了目前 HTTP/2 的缺點,並解釋了 HTTP/3 如何藉由採用 QUIC 這個新的傳輸層協定解決這些問題。此外,我們也介紹了 QUIC 的特徵與運作原理,並比較了 HTTP/2 與 HTTP/3 兩者間的差異。
總得來說, 基於 QUIC 的 HTTP/3 是個非常令人期待的技術發展。儘管有許多人並不看好 HTTP/3 ,認為它改進的幅度有限,也會像 IPv6 那樣曲高和寡。但 HTTP/3 之所以令人感到期待的並不純粹是因為功能上的改進,而是其背後所藏的巨大野心 — 醞釀一場顛覆主流 TCP/IP 架構的大革命。
可以預期 HTTP/3 與 QUIC 的實踐與普及,並不是近年內會發生的事。但作為如筆者般的開發工程師或網路技術愛好者,我們能做的便是分享與關注,且期盼它對網路技術與網路應用服務帶來更多的激盪與火花。
感謝大家的閱讀。如果有任何問題歡迎在下方留言討論。如果喜歡本文內容,也歡迎大家轉載並給予本文一些掌聲 (clap),謝謝。
參考資料
- HTTP/3 explained, https://http3-explained.haxx.se/en/
- QUIC workgroup, https://datatracker.ietf.org/wg/quic/about/
- Draft-QUIC, https://tools.ietf.org/html/draft-ietf-quic-transport-22
- Draft-QUIC-TLS, https://tools.ietf.org/html/draft-ietf-quic-tls-22
- Draft-HTTP/3, https://tools.ietf.org/html/draft-ietf-quic-http-22
- RFC-HTTP/2 (RFC-7540), https://tools.ietf.org/html/rfc7540
- RFC-TLS 1.3 (RFC-8446), https://tools.ietf.org/html/rfc8446