Trip.com QUIC 高可用及性能提升
首先介紹了QUIC多進程部署架構,隨后分析了QUIC網絡架構在生產應用中遇到的問題及其優化方案。在性能提升方面,分享了QUIC全鏈路埋點監控的實現思路及其收獲,QUIC擁塞控制算法開發與調優思路等等。希望這些內容能夠幫助大家了解QUIC協議及其在實際應用中的優化思路,并從中獲得啟發。
一、前言
1.1 QUIC在Trip.com APP的落地簡介
QUIC(Quick UDP Internet Connections)是由Google提出的基于UDP的傳輸層協議,為HTTP3的標準傳輸層協議。相較于TCP協議,QUIC主要具備以下優勢:
1)多路復用:QUIC允許在單個連接上并行傳輸多個數據流,解決了TCP的隊頭阻塞問題,從而提高了傳輸效率;
2)快速建連:新建連時,QUIC握手與TLS握手并行,避免了TLS單獨握手所消耗的1個RTT時延。當用戶連接過期失效且在PSK(pre-shared key)有效期內再次建連時,QUIC通過驗證PSK復用TLS會話,實現0-RTT建聯,從而可以更快的建立連接,此特性在短連接,單用戶低頻請求的場景中收益尤為明顯;
3)連接遷移:TCP通過四元組標識一個連接,當四元組中的任一部分發生變化時,連接都會失效。而QUIC通過連接CID唯一標識一個連接,當用戶網絡發生切換(例如WIFI切換到4G)時,QUIC依然可以通過CID找到連接,完成新路徑驗證繼續保持連接通信,避免重新建連帶來的耗時;
4)擁塞控制:TCP的擁塞控制需要操作系統的支持,部署成本高,升級周期長,而QUIC在應用層實現擁塞控制算法,升級變更加靈活;
上述優質特性推動了QUIC協議在IETF的標準化發展,2021年5月,IETF推出了QUIC的標準化版本RFC9000,我們于2022年完成了QUIC多進程部署方案在Trip.com APP的落地,支持了多進程下的連接遷移和0-RTT特性,最終取得了Trip.com App鏈路耗時縮短20%的收益,大大的提升了海外用戶的體驗。我們的初期網絡架構如下:
其中有兩個重要組成部分,QUIC SLB和QUIC Server:
- QUIC SLB工作在傳輸層,具有負載均衡能力,負責接收并正確轉發UDP數據包至Server。當用戶進行網絡切換,導致連接四元組發生變化時,SLB通過從連接CID中提取Server端的ip+port來實現數據包的準確轉發,從而支持連接遷移功能;
- QUIC Server工作在應用層,是QUIC協議的主要實現所在,負責轉發及響應客戶端請求,通過Server集群共享ticket_key方案實現0-RTT功能;
1.2 QUIC高可用及性能提升
隨著全球旅游業的復蘇,攜程在國內外的業務迎來了成倍的增長,業務體量越來越龐大,QUIC作為Trip.com APP的主要網絡通道,其重要性不言而喻。為了更好地應對日益增長的流量,更穩定地支持每一次用戶請求的送達,我們建立了QUIC高可用及性能提升的目標,最終完成了以下優化內容:
QUIC集群及鏈路高可用優化:
- 完成QUIC Server容器化改造,具備了HPA能力,并定制化開發了適用于QUIC場景的HPA指標
- 優化QUIC網絡架構,具備了QUIC Server的主動健康監測及動態上下線能力
- 通過推拉結合的策略,有效提升了客戶端App容災能力,實現網絡通道和入口ip秒級切換
- 搭建了穩定可靠的監控告警體系
QUIC成功率及鏈路性能提升:
- 支持QUIC全鏈路埋點,使QUIC運行時數據更加透明化
- 通過優化擁塞控制算法實現了鏈路性能的進一步提升
- 通過多Region部署縮短了歐洲用戶20%的鏈路耗時
- 客戶端Cronet升級,網絡請求速度提升明顯
QUIC應用場景拓展:
- 支持攜程旅行App和商旅海外App接入QUIC,國內用戶和商旅用戶在海外場景下網絡成功率和性能大幅提升
下文將詳細的介紹這些優化內容。
二、QUIC網絡架構升級
2.1 容器化改造
改造前我們統一使用VM部署QUIC SLB和QUIC Server,具體部署流程如下:
QUIC實踐早期我們經常需要在機器上執行自定義操作,這種部署方案的優點是比較靈活。但隨著QUIC服務端功能趨于穩定,以及業務流量的日益增長,此方案部署時間長、不支持動態擴縮容的弊端日益顯現。為了解決以上問題,我們對QUIC SLB以及QUIC Server進行了容器化改造:
- QUIC Server承載了QUIC協議處理以及用戶http請求轉發這兩項核心功能,我們將其改造成了容器鏡像,并接入到內部Captain發布系統中,支持了灰度發布,版本回退等功能,降低了發布帶來的風險,同時具備了HPA能力,擴縮容時間由分鐘級縮短到秒級
- QUIC SLB作為外網入口,主要負責用戶UDP包的轉發,負載較小,對流量變化不敏感。并且由于需要Akamai加速,QUIC SLB需要同時支持UDP以及TCP協議,當前容器是無法支持雙協議的外網入口的。因此我們將QUIC SLB改造成了虛擬機鏡像,支持在PaaS上一鍵擴容,大大降低了部署成本
2.2 服務發現與主動健康監測
在QUIC SLB中,我們使用Nginx作為4層代理,實現QUIC UDP數據包的轉發以及連接遷移能力。
容器化后的前期我們使用Consul作為QUIC Server的注冊中心,Server會在k8s提供的生命周期函數postStart和preStop中分別調用Consul的注冊和刪除API,將自身ip在Consul中注冊或摘除。QUIC SLB會監聽Consul中ip的變化,從而及時感知到每個Quic Server的狀態,并實時更新到Nginx的配置文件中,這樣就實現了QUIC Server的自動注冊與發現。
但在實際演練場景中,我們發現當直接對QUIC Server注入故障時,由于Server所在pod并沒有被銷毀,因此不會觸發preStop API的調用,故障Server無法在Consul中摘除自身ip,導致QUIC SLB無法感知到Server的下線,因此QUIC SLB的nginx.conf中依舊會保留故障的Server ip,這種情況在Nginx做TCP代理和UDP代理時所產生的影響不同:
- 當Nginx做TCP代理時,Nginx與故障Server之間建立的是TCP連接,Server故障時TCP連接會斷開,Nginx會與故障Server重新建聯但最終失敗,此時會自動將其拉出一段時間,并每隔一段時間進行探測,直至其恢復,從而避免了TCP數據包轉發至錯誤的Server,不會導致服務成功率下降;
- 當Nginx做UDP代理時,由于UDP是無連接的,QUIC SLB依舊會轉發數據包至故障Server,但SLB不會收到任何響應數據包,由于UDP協議特性,此時QUIC SLB不會判定Server為異常,從而持續大量的UDP包被轉發到故障Server實例上,導致QUIC通道的成功率大幅下降;
經過上述分析,我們知道了使用UDP進行健康監測存在一定弊端,期望使用TCP協議對QUIC Server進行主動的健康檢測。所以開啟了新方案的探索,其需要同時支持UDP數據轉發,基于TCP協議的主動健康監測,支持服務發現與注冊,并且能夠較好的適配QUIC SLB層。
調研了很多方案,其中較適配的是開源的Nginx UDP Health Check項目,其同時支持UDP數據包轉發和TCP的主動健康檢測,但是其不支持nginx.conf中下游ip的動態變更,也就是不支持QUIC Server的動態上下線,這直接影響了Server集群的HPA能力,因此舍棄了這個方案。
最終通過調研發現公司內部的L4LB組件,既能同時支持TCP的主動健康檢測和UDP數據包轉發,還支持實例的動態上下線,完美適配我們的場景,因此最終采用了L4LB作為QUIC SLB和QUIC Server之間的轉發樞紐。
具體實現是為QUIC Server的每個group申請一個UDP的內網L4LB入口ip,這些ip是固定不變的,那么對于QUIC SLB來說只需要將UDP數據包轉發至固定的虛擬ip即可。L4LB開啟TCP的健康檢測功能,這樣當group中的QUIC Server實例故障時,健康檢測失敗,L4LB就會將此實例拉出,后續UDP包就不會再轉發到此實例上,直至實例再次恢復到健康狀態。這樣就完美解決了QUIC Server的自動注冊與主動健康監測功能。
2.3 推拉結合方式提升客戶端用戶側網絡容災能力
Trip.com App的網絡請求框架同時支持QUIC/TCP/HTTP 三通道能力,其中80%以上的用戶請求都是通過QUIC通道訪問服務器的,日均流量達到數億,在現有的多通道/多IP切換能力的基礎上,進一步提升容災能力顯得尤為重要。于是我們設計了一套推拉結合的策略方案,結合公司配置系統實現了秒級通道/IP切換。下面是簡化過程:
在客戶端App啟動和前后臺切換等場景,根據變動情況獲取最新的配置,網絡框架基于最新的配置進行無損通道或IP的切換。
同時當用戶APP處于前臺活躍狀態時,通過對用戶進行主動的配置更新推送,讓在線用戶可以立即感知到變化并切換至最新的網絡配置上面,且此切換過程對用戶是無感的。
這樣一來,我們的QUIC客戶端網絡框架進一步提升了容災能力,當某個IP發生故障時,可以在秒級通知所有用戶切離故障IP,當某通道發生異常時,用戶亦可以無感的切換至優質通道,而不會受到任何影響。
2.4 監控告警穩定性保證及彈性擴縮容指標建設
QUIC數據監控系統的穩定性,對于故障預警、故障響應起到至關重要的作用。通過將埋點數據寫至access log和error log來完成QUIC運行時埋點數據的輸出,再通過logagent將服務器本地的日志數據發送至Kafka,隨后Hangout消費并解析,將數據落入Clickhouse做數據存儲及查詢,這給我們做運行時數據觀察及數據分析提供了很大的便利性。
在完成QUIC網絡架構升級之后,只依靠上述日志體系遇到了下面兩類問題:
第一,在應用場景下,單純依靠這部分數據做監控告警,偶發由于某些中間環節出現波動導致監控告警不準確, 例如Hangout消費者故障導致流量驟降或突增的假象,這可能會影響監控告警的及時性和準確性;
第二,容器化之后具備了彈性擴縮容能力,而HPA依賴于擴縮容數據指標,僅僅使用CPU、內存利用率等資源指標,無法充足的反映QUIC服務器狀態。根據QUIC服務特性,仍需要自定義一些HPA數據指標,例如空閑連接占比,空閑端口號占比等,以建立更合理、更穩定的擴縮容依賴;
基于上述兩方面的考量,在經過調研之后,我們將Nginx與Prometheus進行整合,支持了關鍵數據指標、擴縮容數據指標通過Prometheus上報的方案。在Nginx中預先對成功率,耗時,可用連接數等等重要指標進行了聚合,只上報聚合數據指標,從而大大縮小了數據體量,使Nginx整合Prometheus的影響可以忽略不計。另外我們支持了空閑連接占比,空閑端口號占比等等HPA指標,使QUIC集群在流量高峰期,能夠非常準確迅速的完成系統擴容,在低流量時間段,也能夠縮容至適配狀態,以最大程度的節約機器資源。
這樣一來,QUIC系統的監控告警數據來源同時支持Prometheus和Clickhouse,Prometheus側重關鍵指標及聚合數據的上報,Clickhouse側重運行時明細數據的上報,兩者相互配合互為補充。
在支持Prometheus過程中我們遇到了較多依賴項的版本搭配導致的編譯問題,以nginx/1.25.3版本為例,給出版本匹配結果:
組件名 | 描述 | 版本 | 下載鏈接 |
nginx-quic | nginx官方庫 | 1.25.3 | |
nginx-lua-prometheus | lua-prometheus語法庫 | 0.20230607 | https://github.com/knyar/nginx-lua-prometheus/releases/tag/0.20230607 |
luajit | lua即時編譯 | v2.1-20231117 | https://github.com/openresty/luajit2/releases/tag/v2.1-20231117 |
lua-nginx-module | lua-nginx框架 | v0.10.25 | https://github.com/openresty/lua-nginx-module/releases/tag/v0.10.25 |
ngx_devel_kit | lua-nginx依賴的開發包 | v0.3.3 | https://github.com/vision5/ngx_devel_kit/releases/tag/v0.3.3 |
lua-resty-core | lua-resty核心模塊 | v0.1.27 | https://github.com/openresty/lua-resty-core/releases/tag/v0.1.27 |
lua-resty-lrucache | lua-resty lru緩存模塊 | v0.13 | https://github.com/openresty/lua-resty-lrucache/releases/tag/v0.13 |
三、全鏈路埋點
3.1 落地實踐
我們基于優化用戶鏈路耗時,尋找并優化耗時短板的目標出發,開始抽樣分析耗時較久的請求,并根據所需,在服務端access.log中逐漸添加了較多的數據埋點,nginx官方對單個http請求維度的數據埋點支持性較好,但僅僅分析單個請求維度的信息,難以看清請求所屬連接的各類數據,仍需要觀察用戶連接所處網絡環境,握手細節,數據傳輸細節,擁塞情況等等數據,來協助對問題進行定位。
QUIC客戶端之前僅有端到端的整體埋點數據,并且存在QUIC埋點體系和現有體系mapping的問題,我們收集過濾Cronet metrics的信息,整合進現有埋點體系內。
3.1.1 收集過濾QUIC客戶端Cronet Metrics埋點數據
端到端流程支持了DNS、TLS握手、請求發送、響應返回等環節細粒度的埋點,QUIC端到端各環節數據一目了然。
3.1.2 改造服務端nginx源碼
我們在連接創建到連接銷毀的全生命周期內進行了詳細的數據埋點,另外通過連接CID實現了連接級別埋點和請求級別埋點數據的串聯,這對進行問題定位,性能優化等提供了可靠的數據支持。下面分類列舉了部分服務端全鏈路埋點,并簡要概述了其用途:
1)連接生命周期時間線
連接類型(1-rtt/0-rtt),連接創建時間(Server收到Client第一個數據包的時間),連接發送第一個數據包的時間,連接收到第一個ack幀的時間,連接握手耗時,連接收到及發送cc幀(Connection Close Frame)的時間,連接無響應超時時間,連接銷毀時間等等;這一類埋點主要幫助我們理清用戶連接生命周期中的各個關鍵時間點,以及握手相關的耗時細節。
2)數據傳輸細節
(以下皆為連接生命周期內)發送和接收字節、數據包、數據幀總數,包重傳率,幀重傳率等等。這類數據幫助分析我們的數據傳輸特性,對鏈路傳輸優化,擁塞控制算法調整提供數據參考。
3)RTT(Round-trip time)和擁塞控制數據
平滑RTT,最小RTT,首次和最后一次RTT,擁塞窗口大小,最大in_flight,慢開始閾值,擁塞recovery_start時間等等。這些數據可用來分析用戶網絡狀況,觀察擁塞比例,評估擁塞控制算法合理性等等。
4)用戶信息
客戶端ip,國家及地區等。這幫助我們對用戶數據進行區域級別的聚合分析,找到網絡傳輸的區域性差異,以進行一些針對性優化。
3.2 分析挖掘
通過合并聚合各類數據埋點,實現了QUIC運行時數據可視化透明化,這也幫助我們發現了諸多問題及優化項,下面列舉幾項進行詳述:
3.2.1 0-rtt連接存活時間異常,導致重復請求問題
通過篩選0-rtt類型的連接,我們觀察到此類連接的存活總時間,恰好等于QUIC客戶端和服務端協商之后的max_idel_timeout,而max_idel_timeout的準確定義為”連接無響應(客戶端服務端無任何數據交互)超時關閉時間“,也就是說正常情況下,當一個連接上最后一次http請求交互完畢之后,若經過max_idel_timeout時間仍未發生其他交互時,連接會進入關閉流程;當連接上不斷的有請求交互時,連接的存活時間必定大于max_idel_timeout(實際連接存活時間 = 最后一次請求數據傳輸完成時間 - 連接創建時間 + max_idel_timeout)。
為了論證上述現象,我們通過連接的dcid,關聯篩選出0-rtt連接生命周期中所有http請求列表,發現即使連接上的請求在不斷的進行,0-rtt連接仍然會在存活了max_idel_timeout時無條件關閉,所以斷定0-rtt連接的續命邏輯存在問題。我們對nginx-quic源碼進行閱讀分析,最終定位并及時修復了問題代碼:
在源碼ngx_quic_run()函數中,存在兩個連接相關的定時器:
兩者都會影響連接的關閉,其中c->read定時器存在續命邏輯,會隨著連接生命周期內,請求的不斷發生而刷新定時器。qc->close在源碼中不存在續命邏輯,有且僅有一處刪除邏輯,即在執行ngx_quic_handle_datagram()函數過程中,若完成了ssl初始化,則調用ngx_quic_do_init_streams()進行qc->close定時器的刪除操作;
- 若為1-rtt建聯,第一次執行ngx_quic_handle_datagram()函數不會完成ssl初始化,所以qc->close的創建發生在ssl初始化完畢之前,在后續數據包交互過程中能夠正常完成刪除邏輯;
- 若為0-rtt建聯,第一次執行ngx_quic_handle_datagram()函數會完成ssl初始化邏輯,所以僅一次的刪除邏輯,發生在了qc->close定時器設置之前,所以導致qc->close不能被正常移除,從而導致max_idel_timeout時間一到,連接立即關閉的現象;
這個bug除了導致較多無效0-rtt新建連之外,在我們的應用場景下,經過對全鏈路埋點數據聚合分析發現,還會導致重復請求問題,下面介紹發生重復請求的原因:
quic client發起某個request的第一次請求,quic server端收到并轉發給后端應用,在server收到后端應用response之前,恰好qc->close定時器到期,導致server立即向client發送cc幀,client在收到cc幀后,按照quic協議應無條件立即關閉當前連接,所以client認為第一次請求失敗,從而發起新建連,開始第二次請求,從而導致重復請求問題。而這個過程從客戶端業務角度來說只請求了一次,但后端應用卻連續收到兩次一模一樣的請求,這對于冪等性要求較高的接口影響較大。
我們在2024年2月發現并修復了上述定時器bug,修復之后連接復用比例提升0.5%,不必要的0-rtt建連比例降低7%。目前nginx-quic官方分支于2024/04/10也提交了對此問題的修復,Commit鏈接:https://hg.nginx.org/nginx-quic/rev/155c9093de9d
3.2.2 客戶端App Cronet升級給95線用戶帶來體驗提升
經過數據分析發現,長尾用戶在0-RTT上占比不高,大多為1-RTT新建連接,經過分析判斷可能和QUIC客戶端Cronet裁剪有關。Trip.com App上Cronet庫在優化前使用的是2020年的舊版本,并且由于包大小問題對其進行了裁剪(比如:重構了PSK相關邏輯,轉為session級別),在保留關鍵功能的前提下盡可能剔除了無用代碼十幾萬行。同時經過與其他Cronet使用方溝通,得到Cronet升級后有不錯的性能提升表現,于是時隔近四年,客戶端對Cronet庫進行了一次大升級。
另外,由于Chromium官方在2023年11月份官宣不再提供iOS的Cli工具,所以此次升級目標就定位選一個盡可能靠近官方刪除iOS構建工具之前的版本,最終我們選定了 120.0.6099.301。
經過Cronet升級及相關適配性改造,對線上升級前后版本進行對比,升級后用戶側95線請求耗時降低了18%。
3.2.3 Nginx-quic分支中擁塞控制算法實現具有較大的優化空間
通過對nginx-quic源碼的研究以及鏈路埋點的數據分析,我們發現源碼中的擁塞控制算法為Reno算法的簡化版,初始傳輸窗口設置較大為131054字節(若mtu為1200,初始就有109個包可以同時傳輸)。若發生擁塞事件,降窗的最小值仍為131054字節,在網絡較好時,網絡公平性不友好,在網絡較差時,這會加劇網絡擁堵。在發現此問題后,我們開始著手改造源碼中的擁塞控制算法邏輯,這一優化內容將在第四部分詳述。
四、擁塞控制算法探索
nginx-quic官方分支中,對于擁塞控制算法的實現目前仍處于demo級別,我們結合QUIC在應用層實現擁塞控制算法而不依賴于操作系統的特性,對Nginx官方代碼中擁塞控制相關邏輯進行了抽象重構,以方便拓展各種算法,并支持了可配置式的擁塞控制算法切換,可以根據不同的網絡狀況,不同的server業務場景配置不同的擁塞控制算法。
4.1 擁塞控制算法簡介
目前主流的擁塞控制算法大抵可分為兩類,一類是根據丟包做響應,如Reno、Cubic,一類是根據帶寬和延遲反饋做響應,如BBR系列。這里簡要介紹下Reno,Cubic和BBR的工作原理,適用場景及優缺點:
1)Reno算法是TCP最早的擁塞控制算法之一,基于丟包的擁塞控制機制。它使用兩個閾值(慢啟動閾值和擁塞避免閾值)來控制發送速率。在慢啟動階段,發送方每經過一個往返時間(RTT),就將擁塞窗口大小加倍。一旦出現擁塞,會觸發擁塞避免階段,發送速率會緩慢增長。當發生丟包時,發送方會認為發生了擁塞,將擁塞窗口大小減半;適用于低延時、低帶寬的場景,對于早期互聯網環境比較適用。其優點是簡單直觀,易于理解和實現。缺點是對網絡變化反應較慢,可能導致網絡利用率不高,且在丟包率較高時性能不佳。
2)Cubic算法在Reno算法的基礎上進行改進,利用網絡往返時間(RTT)和擁塞窗口的變化率來計算擁塞窗口的大小,使用擬立方函數來模擬網絡的擁塞狀態,并根據擁塞窗口的大小和時間來調整擁塞窗口的增長速率。適用于中度丟包率的網絡環境,對于互聯網主流環境有較好的性能表現。優點是相對于Reno算法,能更好地適應網絡變化,提高網絡利用率。缺點是在高丟包率或長肥管道環境下,發送窗口可能會迅速收斂到很小,導致性能下降。
3)BBR(Bottleneck Bandwidth and RTT)算法是Google開發的一種擁塞控制算法,通過測量網絡的帶寬和往返時間來估計網絡的擁塞程度,并根據擁塞程度調整發送速率。BBR算法的特點是能夠更精確地估計網絡的擁塞程度,避免了過度擁塞和欠擁塞的情況,提高了網絡的傳輸速度和穩定性。適用于高帶寬、高延時或中度丟包率的網絡環境。優點是能夠更精確地控制發送速率,提高網絡利用率和穩定性。缺點是可能占用額外的CPU資源,影響性能,且在某些情況下可能存在公平性問題。
4.2 優化實現及收益
源碼中有關擁塞控制邏輯的代碼分散在各處功能代碼中,未進行抽象統一管理,我們通過梳理擁塞控制算法響應事件,將各個事件函數抽象如下:
我們基于上述抽象結構,先后實現了目前主流的擁塞控制算法Reno,Cubic及BBR,并且我們根據埋點數據對擁塞控制算法進行了參數和邏輯調優,包括設置合理的初始窗口和最小窗口,設置最優的擁塞降窗邏輯等等,這些調整會引起包重發率和連接擁塞率的數據變化,而這些數據變化皆會影響到鏈路傳輸性能。
我們通過對QUIC擁塞控制算法的優化,SHA環境連接擁塞比例降低了15個點,實現了SHA端到端耗時降低4%的收益。后續將繼續基于Trip.com的數據傳輸特性,根據每個IDC的不同網絡狀況進行適應性定制化開發,并通過長期的AB實驗,探索每個IDC下最優的擁塞控制邏輯。
五、成果與展望
我們不斷的優化QUIC鏈路性能,不斷的提升QUIC通道穩定性,旨在為Trip.com日益增長的業務提供優質的網絡服務,同時我們也在不斷的探索支持更多的QUIC應用場景。
1)通過容器化改造,將擴縮容由手動改為自動,擴縮容時間縮短30倍,在20s內就能拉起并上線大批服務器;
2)通過開發全鏈路埋點,聚合分析出了較多優化項,修復0-rtt連接問題,連接復用比例提升0.5%,優化擁塞控制算法,端到端耗時縮短4%;
3)通過在FRA(法蘭克福)部署QUIC集群,降低了歐洲用戶耗時20%以上,提高了網絡成功率0.5%以上;
4)支持了攜程旅行App和商旅海外App接入QUIC,國內用戶和商旅用戶在海外場景下網絡成功率和性能也大幅提升;
5)客戶端升級Cronet之后,綜合上述優化項,用戶側端到端整體耗時95線降低18%;
Trip.com在QUIC上的探索將持續的進行,我們將密切關注社區動態,探索支持更多的QUIC應用場景,挖掘更多的優化項目,為攜程國際化戰略貢獻力量。