QUIC 協(xié)議在螞蟻集團(tuán)落地總結(jié)
自 2015 年以來(lái),QUIC 協(xié)議開始在 IETF 進(jìn)行標(biāo)準(zhǔn)化并被國(guó)內(nèi)外各大廠商相繼落地。鑒于 QUIC 具備“0RTT 建聯(lián)”、“支持連接遷移”等諸多優(yōu)勢(shì),并將成為下一代互聯(lián)網(wǎng)協(xié)議:HTTP3.0 的底層傳輸協(xié)議,螞蟻集團(tuán)支付寶客戶端團(tuán)隊(duì)與接入網(wǎng)關(guān)團(tuán)隊(duì)于 2018 年下半年開始在移動(dòng)支付、海外加速等場(chǎng)景落地 QUIC。
本文是綜述篇,介紹 QUIC 在螞蟻的整體落地情況。之所以是綜述,是因?yàn)?QUIC 協(xié)議過(guò)于復(fù)雜,如果對(duì)標(biāo)已有的協(xié)議,QUIC 近似等于 HTTP + TLS +TCP,無(wú)法詳細(xì)的畢其功于一役,因此我們通過(guò)綜述的方式將落地的重點(diǎn)呈現(xiàn)給讀者,主要介紹如下幾個(gè)部分:
QUIC背景:簡(jiǎn)單全面的介紹下 QUIC 相關(guān)的背景知識(shí)
方案選型設(shè)計(jì):詳細(xì)介紹螞蟻的落地方案如何另辟蹊徑、優(yōu)雅的支撐 QUIC 的諸多特性,包括連接遷移等
落地場(chǎng)景:介紹 QUIC 在螞蟻的兩個(gè)落地場(chǎng)景,包括:支付寶客戶端鏈路以及海外加速鏈路
幾項(xiàng)關(guān)鍵技術(shù):介紹落地 QUIC 過(guò)程中核心需要解決的問題,以及我們使用的方案,包括:“支持連接遷移”、“提升 0RTT 比例", "支持 UDP 無(wú)損升級(jí)”以及“客戶端智能選路” 等
幾項(xiàng)關(guān)鍵的技術(shù)專利
本文也是 QUIC 協(xié)議介紹的第一篇,后續(xù)我們會(huì)把更多的落地細(xì)節(jié)、體驗(yàn)優(yōu)化手段、性能優(yōu)化手段、安全與高可用、QUIC 新技術(shù)等呈現(xiàn)給大家。
QUIC 背景介紹
一、QUIC 是什么?

二、為什么是 QUIC ?
網(wǎng)絡(luò)設(shè)備支持 TCP 時(shí)的僵化,表現(xiàn)在:對(duì)于一些防火墻或者 NAT 等設(shè)備,如果 TCP 引入了新的特性,比如增加了某些 TCP OPTION 等,可能會(huì)被認(rèn)為是攻擊而丟包,導(dǎo)致新特性在老的網(wǎng)絡(luò)設(shè)備上無(wú)法工作。 網(wǎng)絡(luò)操作系統(tǒng)升級(jí)困難導(dǎo)致的 TCP 僵化,一些 TCP 的特性無(wú)法快速的被演進(jìn)。 除此之外,當(dāng)應(yīng)用層協(xié)議優(yōu)化到 TLS1.3、 HTTP2.0 后, 傳輸層的優(yōu)化也提上了議程,QUIC 在 TCP 基礎(chǔ)上,取其精華去其糟粕具有如下的硬核優(yōu)勢(shì):

三、QUIC 生態(tài)圈發(fā)展簡(jiǎn)史

介紹完 QUIC 相關(guān)背景,之后我們來(lái)介紹螞蟻的整個(gè)落地的內(nèi)容,這里為了便于闡述,我們用螞蟻 QUIC 的 一、二、三、四 來(lái)進(jìn)行概括總結(jié),即 “一套落地框架”、“兩個(gè)落地場(chǎng)景”、“三篇?jiǎng)?chuàng)新專利保護(hù)”、“四項(xiàng)關(guān)鍵技術(shù)”。
一套落地框架
螞蟻的接入網(wǎng)關(guān)是基于多進(jìn)程的 NGINX 開發(fā)的 (內(nèi)部稱為 Spanner,協(xié)議卸載的扳手),而 UDP 在多進(jìn)程編程模型上存在諸多挑戰(zhàn),典型的像無(wú)損升級(jí)等。為了設(shè)計(jì)一套完備的框架,我們?cè)诼涞厍俺浞挚紤]了服務(wù)端在云上部署上的方便性、擴(kuò)展性、以及性能問題,設(shè)計(jì)了如下的落地框架以支撐不同的落地場(chǎng)景:

在這套框架中,包括如下兩個(gè)組件:
QUIC LB 組件:基于 NGINX 4層 UDP Stream 模塊開發(fā),用來(lái)基于 QUIC DCID 中攜帶的服務(wù)端信息進(jìn)行路由,以支持連接遷移。 NGINX QUIC 服務(wù)器:開發(fā)了 NGINX_QUIC_MODULE,每個(gè) Worker 監(jiān)聽兩種類型的端口: (1)BASE PORT ,每個(gè) Worker 使用的相同的端口號(hào),以 Reuseport 的形式監(jiān)聽,并暴露給 QUIC LB,用以接收客戶端過(guò)來(lái)的第一個(gè) RTT 中的數(shù)據(jù)包,這類包的特點(diǎn)是 DCID 由客戶端生成,沒有路由信息。 (2)Working PORT,每個(gè) Worker 使用的不同的端口號(hào),是真正的工作端口,用以接收第一個(gè) RTT 之后的 QUIC 包,這類包的特定是 DCID 由服務(wù)端的進(jìn)程生成攜帶有服務(wù)端的信息。
在不用修改內(nèi)核的情況下,完全在用戶態(tài)支持 QUIC 的連接遷移,以及連接遷移時(shí) CID 的 Update 在不用修改內(nèi)核的情況下,完全在用戶態(tài)支持 QUIC 的無(wú)損升級(jí)以及其他運(yùn)維問題 支持真正意義上的 0RTT ,并可提升 0RTT 的比例
兩個(gè)落地場(chǎng)景
場(chǎng)景一、支付寶移動(dòng)端落地

具體的方案選型如下:
支持的 QUIC 版本是 gQUIC Q46。 NGINX QUIC MODULE 支持 QUIC 的接入和 PROXY 成 TCP 的能力。 支持包括移動(dòng)支付、基金、螞蟻森林在內(nèi)的所有的 RPC 請(qǐng)求。 當(dāng)前選擇 QUIC 鏈路的方式有兩種 :
Backup 模式,即在 TCP 鏈路無(wú)法使用的情況下,降級(jí)到 QUIC 鏈路。 Smart 模式,即 TCP和 QUIC 競(jìng)速,在 TCP 表現(xiàn)力弱于 QUIC 的情況下,下次請(qǐng)求主動(dòng)使用 QUIC 鏈路。
在此場(chǎng)景下,通過(guò)使用 QUIC 可以獲得的紅利包括:
在客戶端連接發(fā)生遷移的時(shí)候,可以不斷鏈繼續(xù)服務(wù) 客戶端在首次發(fā)起連接時(shí),可以節(jié)省 TCP 三次握手的時(shí)間 對(duì)于弱網(wǎng)情況,QUIC 的傳輸控制可以帶來(lái)傳輸性能提升

在海外接入點(diǎn)上(LP),每一個(gè) TCP 連接都被 Proxy 成 QUIC 上的一個(gè) Stream 進(jìn)行承載,在國(guó)內(nèi)接出點(diǎn)上(RP), 每一個(gè) QUIC Stream 又被 Proxy 成一個(gè) TCP 連接,LP 和 RP 之間使用 QUIC 長(zhǎng)連接。
通過(guò) QUIC 長(zhǎng)連接的上的 Stream 承載 TCP 請(qǐng)求,避免每次的跨海建聯(lián)。 對(duì)于跨海的網(wǎng)絡(luò),QUIC 的傳輸控制可以帶來(lái)傳輸性能提升。
三篇關(guān)鍵專利
專利一
專利二
專利三
四項(xiàng)關(guān)鍵技術(shù)
技術(shù)點(diǎn)1.優(yōu)雅的支持連接遷移能力
先說(shuō) 連接遷移面臨的問題 ,上文有提到,QUIC 有一項(xiàng)比較重要的功能是支持連接遷移。這里的連接遷移是指:如果客戶端在長(zhǎng)連接保持的情況下切換網(wǎng)絡(luò),比如從 4G 切換到 Wifi , 或者因?yàn)?NAT Rebinding 導(dǎo)致五元組發(fā)生變化,QUIC 依然可以在新的五元組上繼續(xù)進(jìn)行連接狀態(tài)。QUIC 之所以能支持連接遷移,一個(gè)原因是 QUIC 底層是基于無(wú)連接的 UDP,另一個(gè)重要原因是因?yàn)?QUIC 使用唯一的 CID 來(lái)標(biāo)識(shí)一個(gè)連接,而不是五元組。

然而,理論很豐滿,落地卻很艱難,在端到端的落地過(guò)程中,因?yàn)橐肓素?fù)載均衡設(shè)備,會(huì)導(dǎo)致在連接遷移時(shí),所有依賴五元組 Hash 做轉(zhuǎn)發(fā)或者關(guān)聯(lián) Session 的機(jī)制失效。以 LVS 為例,連接遷移后, LVS 依靠五元組尋址會(huì)導(dǎo)致尋址的服務(wù)器存在不一致。即便 LVS 尋址正確,當(dāng)報(bào)文到達(dá)服務(wù)器時(shí),內(nèi)核根據(jù)五元組關(guān)聯(lián)進(jìn)程,依然會(huì)尋址出錯(cuò)。同時(shí),IETF Draft 要求,連接遷移時(shí) CID 需要更新掉,這就為僅依靠 CID 來(lái)轉(zhuǎn)發(fā)的計(jì)劃同樣變的不可行。
再說(shuō) 我們的解決方法,為了解決此問題,我們?cè)O(shè)計(jì)了開篇介紹的落地框架,這里我們將方案做一些簡(jiǎn)化和抽象,整體思路如下圖所示:
在四層負(fù)載均衡上,我們?cè)O(shè)計(jì)了 QUIC LoadBalancer 的機(jī)制:
我們?cè)?QUIC 的 CID 中擴(kuò)展了一些字段(ServerInfo)用來(lái)關(guān)聯(lián) QUIC Server 的 IP 和 Working Port 信息。 在發(fā)生連接遷移的時(shí)候,QUIC LoadBalancer 可以依賴 CID 中的 ServerInfo 進(jìn)行路由,避免依賴五元組關(guān)聯(lián) Session 導(dǎo)致的問題。 在 CID 需要 Update 的時(shí)候,NewCID 中的 ServerInfo 保留不變,這樣就避免在 CID 發(fā)生 Update 時(shí),僅依賴 CID Hash 挑選后端導(dǎo)致的尋址不一致問題。
在 QUIC 服務(wù)器多進(jìn)程工作模式上,我們突破了 NGINX 固有的多 Worker 監(jiān)聽在相同端口上的桎梏,設(shè)計(jì)了多端口監(jiān)聽的機(jī)制,每個(gè) Worker 在工作端口上進(jìn)行隔離,并將端口的信息攜帶在對(duì) First Initial Packet 的回包的 CID 中,這樣代理的好處是:
無(wú)論是否連接遷移,QUIC LB 都可以根據(jù) ServerInfo,將報(bào)文轉(zhuǎn)發(fā)到正確的進(jìn)程。 而業(yè)界普遍的方案是修改內(nèi)核,將 Reuse port 機(jī)制改為 Reuse CID 機(jī)制,即內(nèi)核根據(jù) CID 挑選進(jìn)程。即便后面可以通過(guò) ebpf 等手段支持,但我們認(rèn)為這種修改內(nèi)核的機(jī)制對(duì)底層過(guò)于依賴,不利于方案的大規(guī)模部署和運(yùn)維,尤其在公有云上。 使用獨(dú)立端口,也有利于多進(jìn)程模式下,UDP 無(wú)損升級(jí)問題的解決,這個(gè)我們?cè)诩夹g(shù)點(diǎn) 3 中介紹。

技術(shù)點(diǎn)2.提升 0RTT 握手比例
這里先 介紹 QUIC 0RTT 原理。前文我們介紹過(guò), QUIC 支持傳輸層握手和安全加密層握手都在一個(gè) 0RTT 內(nèi)完成。TLS1.3 本身就支持加密層握手的 0RTT,所以不足為奇。而 QUIC 如何實(shí)現(xiàn)傳輸層握手支持 0RTT 呢?我們先看下傳輸層握手的目的,即:服務(wù)端校驗(yàn)客戶端是真正想握手的客戶端,地址不存在欺騙,從而避免偽造源地址攻擊。在 TCP 中,服務(wù)端依賴三次握手的最后一個(gè) ACK 來(lái)校驗(yàn)客戶端是真正的客戶端,即只有真正的客戶端才會(huì)收到 Sever 的 syn_ack 并回復(fù)。

QUIC 同樣需要對(duì)握手的源地址做校驗(yàn),否則便會(huì)存在 UDP 本身的 DDOS 問題,那 QUIC 是如何實(shí)現(xiàn)的?依賴 STK(Source Address Token) 機(jī)制。這里我們先聲明下,跟 TLS 類似,QUIC 的 0RTT 握手,是建立在已經(jīng)同一個(gè)服務(wù)器建立過(guò)連接的基礎(chǔ)上,所以如果是純的第一次連接,仍然需要一個(gè) RTT 來(lái)獲取這個(gè) STK。如下圖所示,我們介紹下這個(gè)原理:
類似于 Session Ticket 原理,Server 會(huì)將客戶端的地址和當(dāng)前的 Timestamp 通過(guò)自己的 KEY 加密生成 STK。 Client 下次握手的時(shí)候,將 STK 攜帶過(guò)來(lái),由于 STK 無(wú)法篡改,所以 Server 通過(guò)自己的 KEY 解密,如果解出來(lái)的地址和客戶端此次握手的地址一致,且時(shí)間在有效期內(nèi),則表示客戶端可信,便可以建立連接。 由于客戶端第一次握手的時(shí)候,沒有這個(gè) STK,所以服務(wù)度會(huì)回復(fù) REJ 這次握手的信息,并攜帶 STK。

理論上說(shuō),只要客戶端緩存了這個(gè) STK,下次握手的時(shí)候帶過(guò)來(lái),服務(wù)端便可以直接校驗(yàn)通過(guò),即實(shí)現(xiàn)傳輸層的 0RTT。但是真實(shí)的場(chǎng)景卻存在如下兩個(gè)問題:
因?yàn)?STK 是服務(wù)端加密的,所以如果下次這個(gè)客戶端路由到別的服務(wù)器上了,則這個(gè)服務(wù)器也需要可以識(shí)別出來(lái)。
STK 中 encode 的是上一次客戶端的地址,如果下一次客戶端攜帶的地址發(fā)生了變化,則同樣會(huì)導(dǎo)致校驗(yàn)失敗。此現(xiàn)象在移動(dòng)端發(fā)生的概率非常大,尤其是 IPV6 場(chǎng)景下,客戶端的出口地址會(huì)經(jīng)常發(fā)生變化。
再介紹下我們的解決方法。第一個(gè)問題比較好解,我們只要保證集群內(nèi)的機(jī)器生成 STK 的秘鑰一致即可。第二個(gè)問題,我們的解題思路是:
我們?cè)?STK 中擴(kuò)展了一個(gè) Client ID, 這個(gè) Clinet ID 是客戶端通過(guò)無(wú)線保鏢黑盒生成并全局唯一不變的,類似于一個(gè)設(shè)備的 SIMID,客戶端通過(guò)加密的 Trasnport Parameter 傳遞給服務(wù)端,服務(wù)端在 STK 中包含這個(gè) ID。 如果因?yàn)?Client IP 發(fā)生變化導(dǎo)致校驗(yàn) STK 校驗(yàn)失敗,便會(huì)去校驗(yàn) Client ID,因?yàn)?ID 對(duì)于一個(gè) Client 是永遠(yuǎn)不變的,所以可以校驗(yàn)成功,當(dāng)然前提是,這個(gè)客戶端是真實(shí)的。為了防止 Client ID 的泄露等,我們會(huì)選擇性對(duì) Client ID 校驗(yàn)?zāi)芰ψ鱿蘖鞅Wo(hù)。

技術(shù)點(diǎn)3. 支持 QUIC 無(wú)損升級(jí)
我們知道 UDP 無(wú)損升級(jí)是業(yè)界難題。無(wú)損升級(jí)是指在 reload 或者更新二進(jìn)制時(shí),老的進(jìn)程可以處理完存量連接上的數(shù)據(jù)后優(yōu)雅退出。以 NGINX 為例,這里先介紹下 TCP 是如何處理無(wú)損升級(jí)的,主要是如下的兩個(gè)步驟:
老進(jìn)程先關(guān)閉 listening socket,待存量連接請(qǐng)求都結(jié)束后,再關(guān)閉連接套接字 新進(jìn)程從老進(jìn)程繼承 listening socket , 開始 accept 新的請(qǐng)求
在熱升級(jí)的時(shí)候,old process fork 出 new process 后,new process 會(huì)繼承 listening socket 并開始 recv msg。 而 old process 此時(shí)如果關(guān)閉 listenging socket, 則在途的數(shù)據(jù)包便無(wú)法接收,達(dá)不到優(yōu)雅退出的目的。 而如果繼續(xù)監(jiān)聽,則新老進(jìn)程都會(huì)同時(shí)收取新連接上的報(bào)文,導(dǎo)致老進(jìn)程無(wú)法退出。

這里介紹下相關(guān)的解決方法。針對(duì)此問題,業(yè)界有一些方法,比如:在數(shù)據(jù)包中攜帶進(jìn)程號(hào),當(dāng)數(shù)據(jù)包收發(fā)錯(cuò)亂后,在新老進(jìn)程之間做一次轉(zhuǎn)發(fā)。考慮到接入層上的性能等原因,我們不希望數(shù)據(jù)再做一次跳轉(zhuǎn)。結(jié)合我們的落地架構(gòu),我們?cè)O(shè)計(jì)了如下的 基于多端口輪轉(zhuǎn)的無(wú)損升級(jí)方案,簡(jiǎn)單來(lái)說(shuō),我們讓新老進(jìn)程監(jiān)聽在不同的端口組并攜帶在 CID 中,這樣 QUIC LB 就可以根據(jù)端口轉(zhuǎn)發(fā)到新老進(jìn)程。為了便于運(yùn)維,我們采用端口輪轉(zhuǎn)的方式,新老進(jìn)程會(huì)在 reload N 次之后,重新開始之前選中的端口。如下圖所示:
無(wú)損升級(jí)期間,老進(jìn)程的 Baseport 端口關(guān)閉,這樣不會(huì)再接受 first intial packet, 類似于關(guān)閉了 tcp 的 listening socket。 老進(jìn)程的工作端口,繼續(xù)工作,用來(lái)接收當(dāng)前進(jìn)程上殘余的流量。 新進(jìn)程的 Baseport 開始工作,用來(lái)接收 first initial packet, 開啟新的連接,類似于開啟了 tcp 的 listening socket。 新進(jìn)程的 working port = (I + 1) mod N, N 是指同時(shí)支持新老進(jìn)程的狀態(tài)的次數(shù),例如 N = 4, 表示可以同時(shí) reload 四次,四種 Old, New1, New2, New3 四種狀態(tài)同時(shí)并存,I 是上一個(gè)進(jìn)程工作的端口號(hào),這里 + 1 是因?yàn)橹挥幸粋€(gè) worker, 如果 worker 數(shù)有 M 個(gè),則加 M。
建好的連接便被 Load Balancer 轉(zhuǎn)移到新進(jìn)程的監(jiān)聽端口的 Working Port 上。

技術(shù)點(diǎn)4.客戶端智能選路
在帶寬緊張的時(shí)候,UDP 會(huì)經(jīng)常被限流。 一些防火墻對(duì)于 UDP 包會(huì)直接 Drop。 NAT 網(wǎng)關(guān)針對(duì) UDP 的 Session 存活時(shí)間也較短。
同時(shí),根據(jù)觀察發(fā)現(xiàn),不同的手機(jī)廠商對(duì)于 UDP 的支持能力也不同,所以在落地過(guò)程中,如果盲目的將所有流量完全切為 QUIC 可能會(huì)導(dǎo)致一些難以預(yù)料的結(jié)果。為此,我們?cè)诳蛻舳松希O(shè)計(jì)了開篇介紹的 TCP 和 QUIC 相互 Backup 的鏈路,如下圖所示,我們實(shí)時(shí)探測(cè) TCP 鏈路和 QUIC 鏈路的 RTT、丟包率、請(qǐng)求完成時(shí)間、錯(cuò)誤率等指標(biāo)情況,并根據(jù)一定的量化方法對(duì)兩種鏈路進(jìn)行打分,根據(jù)評(píng)分高低,決定選擇走哪種鏈路,從而避免尋址只走一條鏈路導(dǎo)致的問題。

做個(gè)總結(jié)
未來(lái)規(guī)劃
我們將利用 QUIC 在應(yīng)用層實(shí)現(xiàn)的優(yōu)勢(shì),設(shè)計(jì)一套統(tǒng)一的具備自適應(yīng)業(yè)務(wù)類型和網(wǎng)絡(luò)類型的 QUIC 傳輸控制框架,對(duì)不同類型的業(yè)務(wù)和網(wǎng)絡(luò)類型,做傳輸上的調(diào)優(yōu),以優(yōu)化業(yè)務(wù)的網(wǎng)絡(luò)傳輸體驗(yàn)。 將 gQUIC 切換成 IETF QUIC,推進(jìn)標(biāo)準(zhǔn)的 HTTP3.0 在螞蟻的進(jìn)一步落地。 將螞蟻的 QUIC LB 技術(shù)點(diǎn)向 IETF QUIC LB 進(jìn)行推進(jìn),并最終演變?yōu)闃?biāo)準(zhǔn)的 QUIC LB。 探索并落地 MPQUIC(多路徑 QUIC) 技術(shù),最大化在移動(dòng)端的收益。 繼續(xù) QUIC 的性能優(yōu)化工作,使用 UDP GSO, eBPF,io_uring 等內(nèi)核技術(shù)。 探索 QUIC 在內(nèi)網(wǎng)承載東西向流量的機(jī)會(huì)。
- END -
「技術(shù)分享」某種程度上,是讓作者和讀者,不那么孤獨(dú)的東西。歡迎關(guān)注我的微信公眾號(hào):「Kirito的技術(shù)分享」
