你是否也有這樣的困擾:打開 APP 巨耗時、刷劇一直在緩沖、追熱搜打不開頁面、信號稍微差點就直接加載失敗……
如果有一個協議能讓你的上網速度,在不需要任何修改的情況下就能提升 20%,特別是網絡差的環境下能夠提升 30% 以上;如果有一個協議可以讓你在 WiFi 和蜂窩數據切換時,網絡完全不斷開、直播不卡頓、視頻不緩沖;你愿意去了解一下它嗎?它就是 QUIC 協議。本文將從 QUIC 的背景、原理、實踐部署等方面來詳細介紹。
網絡協議棧 1.1 什么叫網絡協議?
類似于我們生活中簽署的合同一樣,比如買賣合同是為了約束買賣雙方的行為按照合同的要求履行,網絡協議是為了約束網絡通信過程中各方(客戶端、服務端及中間設備)必須按照協議的規定進行通信,它制定了數據包的格式、數據交互的過程等等,網絡中的所有設備都必須嚴格遵守才可以全網互聯。
在網絡協議棧中,是有分層的,每一層負責不同的事務。我們討論最多的有三個:應用層、傳輸層、網絡層。應用層主要是針對應用軟件進行約束,比如你訪問網站需要按照 HTTP 協議格式和要求進行,你發送電子郵件需要遵守 SMTP 等郵件協議的格式和要求;傳輸層主要負責數據包在網絡中的傳輸問題,比如如何保證數據傳輸的時候的安全性和可靠性、數據包丟了怎么處理;網絡層,也叫路由轉發層,主要負責數據包從出發地到目的地,應該怎樣選擇路徑才能更快的到達。合理的網絡協議能夠讓用戶上網更快!
1.2 HTTP/3 協議
HTTP/3 是第三個主要版本的 HTTP 協議。與其前任 HTTP/1.1 和 HTTP/2 不同,在 HTTP/3 中,棄用 TCP 協議,改為使用基于 UDP 協議的 QUIC 協議實現。所以,HTTP/3 的核心在于 QUIC 協議。顯然,HTTP/3 屬于應用層協議,而它使用的 QUIC 協議屬于傳輸層協議。
1.3 我們需要 HTTP/3 協議嗎
很多人可能都會有這樣一個疑問,為什么在 2015 年才標準化了 HTTP/2 ,這么快就需要 HTTP/3?
我們知道,HTTP/2 通過引入“流”的概念,實現了多路復用。簡單來說,假設你訪問某個網站需要請求 10 個資源,你使用 HTTP1.1 協議只能串行地發請求,資源 1 請求成功之后才能發送資源 2 的請求,以此類推,這個過程是非常耗時的。如果想 10 個請求并發,不需要串行等待的話,在 HTTP1.1 中,應用就需要為一個域名同時建立 10 個 TCP 連接才行(一般瀏覽器不允許建立這么多),這無疑是對資源的極大的浪費。HTTP/2 的多路復用解決了這一問題,能使多條請求并發。
但現實很殘酷,為什么很多業務用了 HTTP/2,反倒不如 HTTP1.1 呢?
第一:多流并發帶來了請求優先級的問題,因為有的請求客戶端(比如瀏覽器)希望它能盡快返回,有的請求可以晚點返回;又或者有的請求需要依賴別的請求的資源來展示。流的優先級表示了這個請求被處理的優先級,比如客戶端請求的關鍵的 CSS 和 JS 資源是必須高優先級返回的,圖片視頻等資源可以晚一點響應。流的優先級的設置是一個難以平衡或者難以做到公平合理的事情,如果設置稍微不恰當,就會導致有些請求很慢,這在用戶看來,就是用了 HTTP/2 之后,怎么有的請求變慢了。
第二:HTTP/2 解決了 HTTP 協議層面的隊頭阻塞,但是 TCP 的隊頭阻塞仍然沒有解決,所有的流都在一條 TCP 連接上,如果萬一序號小的某個包丟了,那么 TCP 為了保證到達的有序性,必須等這個包到達后才能滑動窗口,即使后面的序號大的包已經到達了也不能被應用程序讀取。這就導致了在多條流并發的時候,某條流的某個包丟了,序號在該包后面的其他流的數據都不能被應用程序讀取。這種情況下如果換做 HTTP1.1,由于 HTTP1.1 是多條連接,某個連接上的請求丟包了,并不影響其他連接。所以在丟包比較嚴重的情況下,HTTP/2 整體效果大概率不如 HTTP1.1
事實上,我們并不是真的需要新的 HTTP 版本,而是需要對底層傳輸控制協議 (TCP) 進行升級。
1.4 QUIC 協議棧
圖 0-QUIC 協議棧
QUIC 協議實現在用戶態,建立在內核態的 UDP 的基礎之上,集成了 TCP 的可靠傳輸特性,集成了 TLS1.3 協議,保證了用戶數據傳輸的安全。
QUIC 協議的優秀特性 2.1 建連快
數據的發送和接收,要想保證安全和可靠,一定是需要連接的。TCP 需要,QUIC 也同樣需要。連接到底是什么?連接是一個通道,是在一個客戶端和一個服務端之間的唯一一條可信的通道,主要是為了安全考慮,建立了連接,也就是建立了可信通道,服務器對這個客戶端“很放心”,對于服務器來說:你想跟我進行通信,得先讓我認識一下你,我得先確認一下你是好人,是有資格跟我通信的。那么這個確認對方身份的過程,就是建立連接的過程。
傳統基于 TCP 的 HTTPS 的建連過程為什么如此慢?它需要 TCP 和 TLS 兩個建連過程。如圖 1 所示(傳統 HTTPS 請求流程圖):
圖 1- 傳統 HTTPS 請求流程圖
對于一個小請求(用戶數據量較小)而言,傳輸數據只需要 1 個 RTT,但是光建連就花掉了 3 個 RTT,這是非常不劃算的,這里建連包括兩個過程:TCP 建連需要 1 個 RTT,TLS 建連需要 2 個 RTT。RTT:Round Trip Time,數據包在網絡上一個來回的時間。
為什么需要兩個過程?可惡就可惡在這個地方,TCP 和 TLS 沒辦法合并,因為 TCP 是在內核里完成的,TLS 是在用戶態。也許有人會說把干掉內核里的 TCP,把 TCP 挪出來放到用戶態,然后就可以和 TLS 一起處理了。首先,你干不掉內核里的 TCP,TCP 太古老了,全世界的服務器的 TCP 都固化在內核里了。所以,既然干不掉 TCP,那我不用它了,我再自創一個傳輸層協議,放到用戶態,然后再結合 TLS,這樣不就可以把兩個建連過程合二為一了嗎?是的,這就是 QUIC。
2.1.1 QUIC 的 1-RTT 建連
如圖 2 所示,是 QUIC 的連接建立過程:初次建連只需要 1 個 RTT 即可完成建連。后續再次建連就可以使用 0-RTT 特性
圖 2-QUIC 建連過程圖
QUIC 的 1-RTT 建連:客戶端與服務端初次建連(之前從未進行通信過),或者長時間沒有通信過(0-RTT 過期了),只能進行 1-RTT 建連。只有先進行一次完整的 1-RTT 建連,后續一段時間內的通信才可以進行 0-RTT 建連。
如圖 3 所示:QUIC 的 1-RTT 建連可以分成兩個部分。QUIC 連接信息部分和 TLS1.3 握手部分。
圖 3-QUIC 建連抓包
QUIC 連接:協商 QUIC 版本號、協商 quic 傳輸參數、生成連接 ID、確定 Packet Number 等信息,類似于 TCP 的 SYN 報文;保證通信的兩端確認過彼此,是對的人。
TLS1.3 握手:標準協議,非對稱加密,目的是為了協商出 對稱密鑰,然后后續傳輸的數據使用這個對稱密鑰進行加密和解密,保護數據不被竊取。
我們重點看 QUIC 的 TLS1.3 握手過程。
圖 4-QUIC 的 1-RTT 握手流程
我們通過圖 4 可以看到,整個握手過程需要 2 次握手(第三次握手是帶了數據的),所以整個握手過程只需要 1-RTT(RTT 是指數據包在網絡上的一個來回)的時間。
1-RTT 的握手主要包含兩個過程:
客戶端發送 Client Hello 給服務端;
服務端回復 Server Hello 給客戶端;
我們通過下圖中圖 5 和圖 6 來看 Client Hello 和 Server Hello 具體都做了啥:
第一次握手(Client Hello 報文)
圖 5-Client Hello 報文
首先,Client Hello 在擴展字段里標明了支持的 TLS 版本(Supported Version:TLS1.3)。值得注意的是 Version 字段必須要是 TLS1.2,這是因為 TLS1.2 已經在互聯網上存在了 10 年。網絡中大量的網絡中間設備都十分老舊,這些網絡設備會識別中間的 TLS 握手頭部,所以 TLS1.3 的出現如果引入了未知的 TLS Version 必然會存在大量的握手失敗。
圖 6-Client Hello 報文
其次,ClientHello 中包含了非常重要的 key_share 擴展:客戶端在發送之前,會自己根據 DHE 算法生成一個公私鑰對。發送 Client Hello 報文的時候會把這個公鑰發過去,那么這個公鑰就存在于 key_share 中,key_share 還包含了客戶端所選擇的曲線 X25519。總之,key_share 是客戶端提前生成好的公鑰信息。
最后,Client Hello 里還包括了:客戶端支持的算法套、客戶端所支持的橢圓曲線以及簽名算法、psk 的模式等等,一起發給服務端。
圖 7-Client Hello 報文
第二次握手:(Server Hello 報文)
圖 8-Server Hello 報文
服務端自己根據 DHE 算法也生成了一個公私鑰對,同樣的,Key_share 擴展信息中也包含了 服務端的公鑰信息。服務端通過 ServerHello 報文將這些信息發送給客戶端。
至此為止,雙方(客戶端服務端)都拿到了對方的公鑰信息,然后結合自己的私鑰信息,生成 pre-master key,在這里官方的叫法是(client_handshake_traffic_secret 和server_handshake_traffic_secret),然后根據以下算法進行算出 key 和 iv,使用 key 和 iv 對 Server Hello 之后所有的握手消息進行加密。
注意:在握手完成之后,服務端會發送一個 New Session Ticket 報文給客戶端,這個包非常重要,這是 0-RTT 實現的基礎。
圖 9-New Session Ticket 報文
2.1.2 QUIC 的 0-RTT 握手
這個功能類似于 TLS1.2 的會話復用,或者說 0-RTT 是基于會話復用功能的。
圖 10- QUIC 的 0-RTT 流程圖
通過上面圖 10 我們可以看到,client 和 server 在建連時,仍然需要兩次握手,仍然需要 1 個 rtt,但是為什么我們說這是 0-rtt 呢,是因為 client 在發送第一個包 client hello 時,就帶上了數據(HTTP 請求),從什么時候開始發送數據這個角度上來看,的確是 0-RTT。
我們通過抓包來看 0-RTT 的過程:
圖 11- QUIC 的 0-RTT 抓包
所以真正在實現 0-RTT 的時候,請求的數據并不會跟 Initial 報文(內含 Client Hello)一起發送,而是單獨一個數據包(0-RTT 包)進行發送,只不過是跟 Initial 包同時進行發送而已。
圖 12- QUIC 的 0-RTT 包
我們單獨看 Initial 報文發現,除了 pre_share_key、early-data 標識等信息與 1-RTT 時不同,其他并無區別。
2.1.3 QUIC 建連需要注意的問題
第一,QUIC 實現的時候,必須緩存收到的亂序加密幀,這個緩存至少要大于 4096 字節。當然可以選擇緩存更多的數據,更大的緩存上限意味著可以交換更大的密鑰或證書。終端的緩存區大小不必在整個連接生命周期內保持不變。這里記住:亂序幀一定要緩存下來。如果不緩存,會導致連接失敗。如果終端的緩存區不夠用了,則其可以通過暫時擴大緩存空間確保握手完成。如果終端不擴大其緩存,則其必須以錯誤碼 CRYPTO_BUFFER_EXCEEDED 關閉連接。
第二,0-RTT 存在前向安全問題,請慎用!
2.2 連接遷移
QUIC 通過連接 ID 實現了連接遷移。
我們經常需要在 WiFi 和 4G 之間進行切換,比如我們在家里時使用 WiFi,出門在路上,切換到 4G 或 5G,到了商場,又連上了商場的 WiFi,到了餐廳,又切換到了餐廳的 WiFi,所以我們的日常生活中需要經常性的切換網絡,那每一次的切換網絡,都將導致我們的 IP 地址發生變化。
傳統的 TCP 協議是以四元組(源 IP 地址、源端口號、目的 ID 地址、目的端口號)來標識一條連接,那么一旦四元組的任何一個元素發生了改變,這條連接就會斷掉,那么這條連接中正在傳輸的數據就會斷掉,切換到新的網絡后可能需要重新去建立連接,然后重新發送數據。這將會導致用戶的網絡會“卡”一下。
但是,QUIC 不再以四元組作為唯一標識,QUIC 使用連接 ID 來標識一條連接,無論你的網絡如何切換,只要連接 ID 不變,那么這條連接就不會斷,這就叫連接遷移!
圖 13-QUIC 連接遷移介紹
2.2.1 連接 ID
每條連接擁有一組連接標識符,也就是連接 ID,每個連接 ID 都能標識這條連接。連接 ID 是由一端獨立選擇的,每個端(客戶端和服務端統稱為端)選擇連接 ID 供對端使用。也就是說,客戶端生成的連接 ID 供服務端使用(服務端發送數據時使用客戶端生成的連接 ID 作為目的連接 ID),反過來一樣的。
連接 ID 的主要功能是確保底層協議(UDP、IP 及更底層的協議棧)發生地址變更(比如 IP 地址變了,或者端口號變了)時不會導致一個 QUIC 連接的數據包被傳輸到錯誤的 QUIC 終端(客戶端和服務端統稱為終端)上。
2.2.2 QUIC 的連接遷移過程
QUIC 限制連接遷移為僅客戶端可以發起,客戶端負責發起所有遷移。如果客戶端接收到了一個未知的服務器發來的數據包,那么客戶端必須丟棄這些數據包。
如圖 14 所示,連接遷移過程總共需要四個步驟。
連接遷移之前,客戶端使用 IP1 和服務端進行通信;
客戶端 IP 變成 IP2,并且使用 IP2 發送非探測幀給服務端;
啟動路徑驗證(雙方都需要互相驗證),通過 PATH_CHANLLENGE 幀和 PATH_RESPONSE 幀進行驗證。
驗證通過后,使用 IP2 進行通信。
圖 14- 連接遷移流程圖
2.3 解決 TCP 隊頭阻塞問題
在 HTTP/2 中引入了流的概念。目的是實現 多個請求在同一個連接上并發,從而提升網頁加載的效率。
圖 15-QUIC 解決 TCP 隊頭阻塞問題
由圖 15 來看,假設有兩個請求同時發送,紅色的是請求 1,藍色的是請求 2,這兩個請求在兩條不同的流中進行傳輸。假設在傳輸過程中,請求 1 的某個數據包丟了,如果是 TCP,即使請求 2 的所有數據包都收到了,但是也只能阻塞在內核緩沖區中,無法交給應用層。但是 QUIC 就不一樣了,請求 1 的數據包丟了只會阻塞請求 1,請求 2 不會受到阻塞。
有些人不禁發問,不是說 HTTP2 也有流的概念嗎,為什么只有 QUIC 才能解決呢,這個根本原因就在于,HTTP2 的傳輸層用的 TCP,TCP 的實現是在內核態的,而流是實現在用戶態度,TCP 是看不到“流”的,所以在 TCP 中,它不知道這個數據包是請求 1 還是請求 2 的,只會根據 seq number 來判斷包的先后順序。
2.4 更優的擁塞控制算法
擁塞控制算法中最重要的一個參數是 RTT,RTT 的準確性決定了擁塞控制算法的準確性;然而,TCP 的 RTT 測量往往不準確,QUIC 的 RTT 測量是準確的。
圖 16-TCP 計算 RTT
如圖 16 所示:由于網絡中經常出現丟包,需要重傳,在 TCP 協議中,初始包和重傳包的序號是一樣的,擁塞控制算法進行計算 RTT 的時候,無法區別是初始包還是重傳包,這將導致 RTT 的計算值要么偏大,要么偏小。
圖 17-QUIC 計算 RTT
如圖 17 所示:QUIC 通過 Packet Number 來標識包的序號,而且規定 Packet Number 只能單調遞增,這也就解決了初始包和重傳包的二義性。從而保證 RTT 的值是準確的。
另外,不同于 TCP,QUIC 的擁塞控制算法是可插拔的,由于其實現在用戶態,服務可以根據不同的業務,甚至不同的連接靈活選擇使用不同的擁塞控制算法。(Reno、New Reno、Cubic、BBR 等算法都有自己適合的場景)
2.5 QUIC 的兩級流量控制
很多人搞不清楚流量控制與擁塞控制的區別。二者有本質上的區別。
流量控制要解決的問題是:接收方控制發送方的數據發送的速度,就是我的接收能力就那么大點,你別發太快了,你發太快了我承受不住,會給你丟掉 你還得重新發。
擁塞控制要解決的問題是:數據在網絡的傳輸過程中,是否網絡有擁塞,是否有丟包,是否有亂序等問題。如果中間傳輸的時候網絡特別卡,數據包丟在中間了,發送方就需要重傳,那么怎么判斷是否擁塞了,重傳要怎么重傳法,按照什么算法進行發送數據才能盡可能避免數據包在中間路徑丟掉,這是擁塞控制的核心。
所以,流量控制解決的是接收方的接收能力問題,一般采用滑動窗口算法;擁塞控制要解決的是中間傳輸的時候網絡是否擁堵的問題,一般采用慢啟動、擁塞避免、擁塞恢復、快速重傳等算法。
圖 18-QUIC 流量控制
QUIC 是雙級流控,不僅有連接這一個級別的流控,還有流這個級別的流控。如下圖所示,每個流都有自己的可用窗口,可用窗口的大小取決于最大窗口數減去發送出去的最大偏移數,跟中間已經發送出去的數據包,是否按順序收到了對端的 ACK 無關。
QUIC 協議如何優化
QUIC 協議定義了很多優秀的功能,但是在實現的過程中,我們會遇到很多問題導致無法達到預期的性能,比如 0-RTT 率很低,連接遷移失敗率很高等等。
3.1 QUIC 的 0-RTT 成功率不高
導致 0-RTT 成功率不高的原因一般有如下幾個:
1. 服務端一般都是集群,對于客戶端來說,處理請求的服務端是不固定的,新的請求到來時,如果當前 client 沒有請求過該服務器,則服務器上沒有相關會話信息,會把該請求當做一個新的連接來處理,重新走 1-RTT。
針對此種情況,我們可以考慮集群中所有的服務器使用相同的 ticket 文件。
2. 客戶端 IP 不是固定的,在發生連接遷移時,服務端下發的 token 融合了客戶端的 IP,這個 IP 變化了的話,攜帶 token 服務端校驗不過,0-RTT 會失敗。
針對這個問題,我們可以考慮采用如圖 19 所示的方法,使用設備信息或者 APP 信息來生成 token,唯一標識一個客戶端。
圖 19- 使用設備信息提高 0-RTT 的成功率
3. Session Ticket 過期時間默認是 2 天,超過 2 天后就會導致 0-RTT 失敗,然后降級走 1-RTT。可以考慮增長過期時間。
3.2 實現連接遷移并不容易。
連接遷移的實現,不可避開的兩個問題:一個是四層負載均衡器對連接遷移的影響,一個是七層負載均衡器對連接遷移的影響。
四層負載均衡器的影響:LVS、DPVS 等四層負載均衡工具基于四元組進行轉發,當連接遷移發生時,四元組會發生變化,該組件就會把同一個請求的數據包發送到不同的后端服務器上,導致連接遷移失敗;
七層負載均衡器的影響(QUIC 服務器多核的影響):由于多核的影響,一般服務器會有多個 QUIC 服務端進程,每個進程負載處理不同的連接。內核收到數據包后,會根據二元組(源 IP、源 port)選擇已經存在的連接,并把數據包交給對應的 socket。在連接遷移發生時,源地址發生改變,可能會讓接下來的數據包去到不同的進程,影響 socket 數據的接收。
如何解決以上兩個問題?DPVS 要想支持 QUIC 的連接遷移,就不能再以四元組進行轉發,需要以連接 ID 進行轉發,需要建立 連接 ID 與對應的后端服務器的對應關系;
QUIC 服務器也是一樣的,內核就不能用四元組來進行查找 socket,四元組查找不到時,就必須使用連接 ID 進行查找 socket。但是內核代碼又不能去修改(不可能去更新所有服務器的內核版本),那么我們可以使用 eBPF 的方法進行解決。如下圖 20 所示:
圖 20- 多核 QUIC 服務器解決連接遷移問題
3.3 UDP 被限速或禁閉
業內統計數據全球有 7% 地區的運營商對 UDP 有限速或者禁閉,除了運營商還有很多企業、公共場合也會限制 UDP 流量甚至禁用 UDP。這對使用 UDP 來承載 QUIC 協議的場景會帶來致命的傷害。對此,我們可以采用多路競速的方式使用 TCP 和 QUIC 同時建連。除了在建連進行競速以外,還可以對網絡 QUIC 和 TCP 的傳輸延時進行實時監控和對比,如果有鏈路對 UDP 進行了限速,可以動態從 QUIC 切換到 TCP。
圖 21-QUIC 和 TCP 協議競速
3.4 QUIC 對 CPU 消耗大
相對于 TCP,為什么 QUIC 更消耗資源?
QUIC 在用戶態實現,需要更多的內核空間與用戶空間的數據拷貝和上下文切換;
QUIC 的 ACK 報文也是加密的,TCP 是明文的。
內核知道 TCP 連接的狀態,不用為每一個數據包去做諸如查找目的路由、防火墻規則等操作,只需要在 tcp 連接建立的時候做一次即可,然而 QUIC 不行;
總的來說,QUIC 服務端消耗 CPU 的地方主要有三個:密碼算法的開銷;udp 收發包的開銷;協議棧的開銷;
針對這些,我們可以適當采取優化措施來:
開啟 GSO 功能。
數據在傳輸過程中,可以將一輪中所有的 ACK 解析后再同時進行處理,避免處理大量的 ACK。
適當將 QUIC 的包長限制調高(比如從默認的 1200 調到 1400 個字節)
減少協議棧的內存拷貝
QUIC 的性能
從公開的數據來看,國內各個廠(騰訊、阿里、字節、華為、OPPO、網易等等)使用了 QUIC 協議后,都有很大的提升,比如網易上了 QUIC 后,響應速度提升 45%,請求錯誤率降低 50%;比如字節火山引擎使用 QUIC 后,建連耗時降低 20%~30%;比如騰訊使用 QUIC 后,在騰訊會議、直播、游戲等場景耗時也降低 30%;
圖 22- 字節火山引擎 QUIC 業務收益
總 結
QUIC 協議的出現,為 HTTP/3 奠定了基礎。這是近些年在 web 協議上最大的變革,也是最優秀的一次實踐。面對新的協議,我們總是有著各種各樣的擔憂,誠然,QUIC 協議在穩定性上在成熟度上,的確還不如 TCP 協議,但是經過近幾年的發展,成熟度已經相當不錯了,Nginx 近期也發布了 1.25.0 版本,支持了 QUIC 協議。所以面對這樣優秀的協議,我們希望更多的公司,更多的業務參與進來使用 QUIC,推動 QUIC 更好的發展,推動用戶上網速度更快!
-
網絡協議
+關注
關注
3文章
265瀏覽量
21515 -
HTTP
+關注
關注
0文章
501瀏覽量
31065 -
網絡通信
+關注
關注
4文章
792瀏覽量
29759 -
Quic
+關注
關注
0文章
25瀏覽量
7289
原文標題:一文讀懂 QUIC 協議:更快、更穩、更高效的網絡通信
文章出處:【微信號:AI前線,微信公眾號:AI前線】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論