本文由去哪兒網技術團隊田文琦分享,本文有修訂和改動。
1、引言
本文針對去哪兒網酒店業務網關的吞吐率下降、響應時間上升等問題,進行全流程異步化、服務編排方案等措施,進行了高性能網關的技術優化實踐。
技術交流:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
(本文已同步發布于:http://www.52im.net/thread-4618-1-1.html)
2、作者介紹
田文琦:2021年9月加入去哪兒網機票目的地事業群,擔任軟件研發工程師,現負責國內酒店主站技術團隊。主要關注高并發、高性能、高可用相關技術和系統架構。主導的酒店業務網關優化項目,榮獲22年去哪兒網技術中心TC項目三等獎。
3、專題目錄
本文是專題系列文章的第9篇,總目錄如下:
- 《長連接網關技術專題(一):京東京麥的生產級TCP網關技術實踐總結》
- 《長連接網關技術專題(二):知乎千萬級并發的高性能長連接網關技術實踐》
- 《長連接網關技術專題(三):手淘億級移動端接入層網關的技術演進之路》
- 《長連接網關技術專題(四):愛奇藝WebSocket實時推送網關技術實踐》
- 《長連接網關技術專題(五):喜馬拉雅自研億級API網關技術實踐》
- 《長連接網關技術專題(六):石墨文檔單機50萬WebSocket長連接架構實踐》
- 《長連接網關技術專題(七):小米小愛單機120萬長連接接入層的架構演進》
- 《長連接網關技術專題(八):B站基于微服務的API網關從0到1的演進之路》
- 《長連接網關技術專題(九):去哪兒網酒店高性能業務網關技術實踐》(* 本文)
4、技術背景
近來,Qunar 酒店的整體技術架構在基于 DDD 指導思想下,一直在進行調整。其中最主要的一個調整就是包含核心領域的團隊交出各自的“應用層”,統一交給下游網關團隊,組成統一的應用層。
這種由多個網關合并成大前臺(酒店業務網關)的融合,帶來的好處是核心系統邊界清晰了,但是對酒店業務網關來說,也帶來了不小的困擾。
系統面臨的壓力主要來自兩方面:
- 1)首先,一次性新增了幾十萬行大量硬編碼、臨時兼容、聚合業務規則的復雜代碼且代碼風格迥異,有些甚至是跨語言的代碼遷移;
- 2)其次,后續的復雜多變的應用層業務需求,之前分散在各個子網關中,現在在源源不斷地匯總疊加到酒店業務網關。
這就導致了一系列的問題:
- 1)業務網關吞吐性能變差:應對流量尖峰時期的單機最大吞吐量與合并之前相比,下降了20%
- 2)內部業務邏輯處理速度變差:主流程業務邏輯的處理時間與合并之前相比,上漲了10%。
- 3)代碼難以維護、開發效率低:主站內部各個模塊之間嚴重耦合,邊界不清,修改擴散問題非常明顯,給后續的迭代增加了維護成本,開發新需求的效率也不高。
酒店業務網關作為直接面對用戶的系統,出現任何問題都會被放大百倍,上述這些問題亟待解決。
5、吞吐量下降問題分析
現有系統雖然業務處理部分是異步化的,但是并不是全鏈路異步化(如下圖所示)。
同步 servlet 容器,servlet 線程與業務邏輯線程是同一個,高峰期流量上漲或者尤其是遇到流量尖峰的時候,servlet 容器線程被阻塞的時候,我們服務的吞吐量就會明顯下降。
業務處理雖然使用了線程池確實能實現異步調用的效果,也能壓縮同步等待的時間,但是也有一些缺陷。
比如:
- 1)CPU 資源大量浪費在阻塞等待上,導致 CPU 資源利用率低;
- 2)為了增加并發度,會引入更多額外的線程池,隨著 CPU 調度線程數的增加,會導致更嚴重的資源爭用,上下文切換占用 CPU 資源;
- 3)線程池中的線程都是阻塞的,硬件資源無法充分利用,系統吞吐量容易達到瓶頸。
6、響應時間上漲問題分析
前期為了快速落地酒店 DDD 架構,合并大前臺的重構中,并沒有做到一步到位的設計。
為了保證項目質量,將整個過程切分為了遷移+重構兩個步驟。遷移之后,整個酒店業務網關的內部代碼結構是割裂、混亂的。
總結如下:
我們最核心的一個接口會調用70多個上游接口,上述問題:邊界不清、不內聚、各種重復調用、依賴阻塞等問題導致了核心接口的響應時間有明顯上漲。
7、 解決方案Part1:全流程異步化提升吞吐量
全流程異步化方案,我們主要采用的是 Spring WebFlux。
7.1選擇的理由
1)響應式編程模型:Spring WebFlux 基于響應式編程模型,使用異步非阻塞式 I/O,可以更高效地處理并發請求,提高應用程序的吞吐量和響應速度。同時,響應式編程模型能夠更好地處理高負載情況下的請求,降低系統的資源消耗。
2)高性能:Spring WebFlux 使用 Reactor 庫實現響應式編程模型,可以處理大量的并發請求,具有出色的性能表現。與傳統的 Spring MVC 框架相比,Spring WebFlux 可以更好地利用多核 CPU 和內存資源,以實現更高的性能和吞吐量。
3)可擴展性:Spring WebFlux 不僅可以使用 Tomcat、Jetty 等常規 Web 服務器,還可以使用 Netty 或 Undertow 等基于 NIO 的 Web 服務器實現,與其它非阻塞式 I/O 的框架結合使用,可以更容易地構建可擴展的應用程序。
4)支持函數式編程:Spring WebFlux 支持函數式編程,使用函數式編程可以更好地處理復雜的業務邏輯,并提高代碼的可讀性和可維護性。
5)50與 Spring 生態系統無縫集成:Spring WebFlux 可以與 Spring Boot、Spring Security、Spring Data 等 Spring 生態系統的組件無縫集成,提供了完整的 Web 應用程序開發體驗。
7.2實現原理和異步化過程
上圖中從下到上每個組件的作用:
- 1)Web Server:適配各種 Web 服務, 監聽客戶端請求,并將其轉發到 HttpHandler 處理;
- 2)HttpHandler:以非阻塞的方式處理響應式 http 請求的最底層處理器,不同的處理器處理的請求都會歸一到 httpHandler 來處理,并返回響應;
- 3)DispatcherHandler:調度程序處理程序用于異步處理 HTTP 請求和響應,封裝了HandlerMapping、HandlerAdapter、HandlerResultHandler 的調用,實際實現了HttpHandler的處理邏輯;
- 4)HandlerMapping:根據路由處理函數 (RouterFunction) 將 http 請求路由到相應的handler。WebFlux 中可以有多個 handler,每個 handler 都有自己的路由;
- 5)HandlerAdapter:使用給定的 handler 處理 http 請求,必要時還包括使用異常處理handler 處理異常;
- 6)HandlerResultHandler:處理返回結果,將 response 寫到輸出流中;
- 7)Reactive Streams:Reactive Streams 是一個規范,用于處理異步數據流。Spring WebFlux 實現了 Reactor 庫,該庫基于響應式流規范,處理異步數據流。
在整個過程中 Spring WebFlux 實現了響應式編程模型,構建了高吞吐量、高并發的 Web 應用程序,同時也具有響應快速、可擴展性好、資源利用率高等優點。
下面我們來看下 webFlux 是如何將 Servlet 請求異步化的:
1)ServletHttpHandlerAdapter 展示了使用 Servlet 異步支持和 Servlet 3.1非阻塞I/O,將 HttpHandler 適配為 HttpServlet。
2)第10行:request.startAsync()開啟異步模式,然后將原始 request 和 response 封裝成 ServletServerHttpRequest 和 ServletServerHttpResponse。
3)第36行:httpHandler.handle(httpRequest, httpResponse) 返回一個 Mono 對象(即Publisher),對 Request 和 Response 的所有具體處理都在 Mono 對象中定義。
所有的操作只有在 subscribe 訂閱的那一刻才開始進行,HandlerResultSubscriber 是 Reactive Streams 規范中標準的 subscriber,在它的 onComplete 事件觸發時,會結束 servlet 的異步模式。
對 Servlet 返回結果的異步寫入,以 DispatcherHandler 為例說明:
1)第2行:exchange 是對 ServletServerHttpRequest 和 ServletServerHttpResponse 的封裝。
2)第10-15行:在系統預加載的 handlerMappings 中根據 exchange 找到對應的 handler,然后利用 handler 處理 exchange 執行相關業務邏輯,最終結果由 result 將 ServletServerHttpResponse 寫入到輸出流中。
最后:除了 Servlet 的異步化,作為業務網關,要實現全鏈路異步化還需要在遠程調用方面要支持異步化。在 RPC 調用方式下,我們采用的異步 Dubbo,在 HTTP 調用方式下,我們采用的是 WebClient。
WebClient 默認使用的是 Netty 的 IO 線程進行發送請求,調用線程通過訂閱一些事件例如:doOnRequest、doOnResponse 等進行回調處理。異步化的客戶端,避免了業務線程池的阻塞,提高了系統的吞吐量。
在使用 WebClient 這種異步 http 客戶端的時候,我們也遇到了一些問題:
1)首先:為了避免默認的 NettyIO 線程池可能會執行比較耗時的 IO 操作導致 Channel 阻塞,建議替換成其他線程池,替換方法是 Mono.publishOn(reactor.core.scheduler.Schedulers.newParallel("biz_scheduler", 300))。
2)其次:因為線程發生了切換,無法兼容 Qtracer (Qunar內部的分布式全鏈路跟蹤系統),所以在初始化 WebClient 客戶端的時候,需要在 filter 里插入對 Request 的修改,記錄前一個線程保存的 Qtracer 的上下文。WebClient.Builder wcb = WebClient.builder().filter(new QTraceRequestFilter())。
8、解決方案Part2:服務編排降低響應時間
Spring WebFlux 并不是銀彈,它并不能保證一定能降低接口響應時間,除了全流程異步化,我們還利用 Spring WebFlux 提供的響應式編程模型,對業務流程進行服務編排,降低依賴之間的阻塞。
8.1服務編排解決方案
在介紹服務編排之前,我們先來了解一下 Spring WebFlux 提供的響應式編程模型 Reactor。
它有最重要的兩個響應式類 Flux 和 Mono:
- 1)一個 Flux 對象表明一個包含0..N 個元素的響應式序列;
- 2)一個 Mono 對象表明一個包含零或者一個(0..1)元素的結果。
不管是 Flux 還是 Mono,它的處理過程分三步:
- 1)首先聲明整個執行過程(operator);
- 2)然后連通主過程,觸發執行;
- 3)最后執行主過程,觸發并執行子過程、生成結果。
每個執行過程連通輸入流和輸出流,子過程之間可以是并行的,也可以是串行的這個取決于實際的業務邏輯。我們的服務編排就是完成輸入和輸出流的編排,即在第一步聲明執行過程(包括子過程),第二步和第三步完全交給 Reactor。
下面是我們服務編排的總體設計:
如上圖所示:
1)service:是最小的業務編排單元,對 invoker 和 handler 進行了封裝,并將結果寫回到上下文中。主流程中,一般是由多個 service 進行并行/串行地編排。
2)Invoker:是對第三方的異步非阻塞調用,對返回結果作 format,不包含業務邏輯。相當于子過程,一個 service 內部根據實際業務場景可以編排0個或多個 Invoker。
3)handler:純內存計算,封裝共用和內聚的業務邏輯。在實際的業務開發過程中,對上下文中的任一變量,只有一個 handler 有寫權限,避免了修改擴散問題。也相當于子過程,根據實際需要編排進 service 中。
4)上下文:為每個接口都設計了獨立的請求/處理/響應上下文,方便監控定位每個模塊的處理正確性。
上下文設計舉例:
在復雜的 service 中我們會根據實際業務需求組裝 invoker 和 handler,例如:日歷房售賣信息展示 service 組裝了酒店報價、輔營權益等第三方調用 invoker,優惠明細計算、過濾報價規則等共用的邏輯處理 handler。
在實際優化過程中我們抽象了100多個 service,180多個 invoker,120多個 handler。他們都是小而獨立的類,一般都不會超過200行,減輕了開發同學尤其是新同學對代碼的認知負擔。邊界清晰,邏輯內聚,代碼的不可知問題也得到了解決。
每個 service 都是由一個或多個 Invoker、handler 組裝編排的業務單元,內部處理都是全異步并行處理的。
如下圖所示:ListPreAsyncReqService 中編排了多個 invoker,在基類 MonoGroupInvokeService 中,會通過 Mono.zip(list, s -> this.getClass() + " succ")將多個流合并成為一個流輸出。
在 controller 層就負責處理一件事,即對 service 進行編排(如下圖所示)。
我們利用 flatMap 方法可以方便地將多個 service 按照業務邏輯要求,進行多次地并行/串行編排。
1)并行編排示例:第12、14行是兩個并行處理的輸入流 afterAdapterValidMono、preRankSecMono ,二者并行執行各自 service 的處理。
2)并行處理后的流合并:第16行,搜索結果流 rankMono 和不依賴搜索的其他結果流preRankAsyncMono,使用 Mono.zip 操作將兩者合并為一個輸出流 afterRankMergeMono。
3)串行編排舉例:第16、20、22行,afterRankMergeMono 結果流作為輸入流執行 service14 后轉換成 resultAdaptMono,又串行執行 service15 后,輸出流 cacheResolveMono。
以上是酒店業務網關的整體服務編排設計。
8.2編排示例
下面來介紹一下,我們是如何進行流程編排,發揮網關優勢,在系統內和系統間達到響應時間全局最優的。
8.2.1)系統內:
上圖示例中的左側方案總耗時是300ms。
這300ms 來自最長路徑 Service1的200ms 加上 Service3 的100ms:
- 1)Service1 包含2個并行 invoker 分別耗時100ms、200ms,最長路徑200ms;
- 2)Service3 包含2個并行invoker 分別耗時50ms、100ms,最長路徑100ms。
而右圖是將 Service1 的200ms 的 invoker 遷移至與 Service1 并行的 Service0 里。
此時,整個處理的最長路徑就變成了200ms:
- 1)Service0 的最長路徑是200ms;
- 2)Service1+service3 的最長路徑是100ms+100ms=200ms。
通過系統內 invoker 的最優編排,整體接口的響應時間就會從300ms 降低到200ms。
8.2.2)系統間:
舉例來說:優化前業務網關會并行調用 UGC 點評(接口耗時100ms)和 HCS 住客秀(接口耗時50ms)兩個接口,在 UGC 點評系統內部還會串行重復調用 HCS 住客秀接口(接口耗時50ms)。
發揮業務網關優勢,UGC 無需再串行調用 HCS 接口,所需業務聚合處理(這里的業務聚合處理是純內存操作,耗時可以忽略)移至業務網關中操作,這樣 UGC 接口的耗時就會降下來。對全局來說,整體接口的耗時就會從原來的100ms 降為50ms。
還有一種情況:假設業務網關是串行調用 UGC 點評接口和 HCS 住客秀接口的話,那么也可以在業務網關調用 HCS 住客秀接口后,將結果通過入參在調用 UGC 點評接口的時候傳遞過去,也可以省去 UGC 點評調用 HCS 住客秀接口的耗時。
基于對整個酒店主流程業務調用鏈路充分且清晰的了解基礎之上,我們才能找到系統間的最優解決方案。
9、優化后的效果
9.1頁面打開速度明顯加快
優化后最直接的效果就是在用戶體感上,頁面的打開速度明顯加快了。
以詳情頁為例:
9.2接口響應時間下降50%
列表、詳情、訂單等主流程各個核心接口的P50響應時間都有明顯的降幅,平均下降了50%。
以詳情頁的 A、B 兩個接口為例,A接口在優化前的 P50 為366ms:
A 接口優化后的 P50 為36ms:
B 接口的 P50 響應時間,從660ms 降到了410ms:
9.3單機吞吐量性能上限提升100%,資源成本下降一半
單機可支持 QPS 上限從100提升至200,吞吐量性能上限提升100%,平穩應對七節兩月等常規流量高峰。
在考試、演出、臨時政策變化、競對故障等異常突發事件情況下,會產生瞬時的流量尖峰。在某次實戰的情況下,瞬時流量高峰達到過二十萬 QPS 以上,酒店業務網關系統經受住了考驗,能夠輕松應對。
單機性能的提升,我們的機器資源成本也下降了一半。
9.4圈復雜度降低38%,研發效率提升30%
具體就是:
- 1)優化后酒店業務網關的有效代碼行數減少了6萬行;
- 2)代碼圈復雜度從19518減少至12084,降低了38%;
- 3)網關優化后,業務模塊更加內聚、邊界清晰,日常需求的開發、聯調時間均有明顯減少,研發效率也提升了30%。
10、本文小結與下一步規劃
1)通過采用 Spring WebFlux 架構和系統內/系統間的服務編排,本次酒店業務網關的優化取得了不錯的效果,單機吞吐量提升了100%,整體接口的響應時間下降了50%,為同類型業務網關提供一套行之有效的優化方案。
2)在此基礎上,為了保持優化后的效果,我們除了建立監控日常做好預警外,還開發了接口響應時長變化的歸因工具,自動分析變化的原因,可以高效排查問題作好持續優化。
3)當前我們在服務編排的時候,只能根據上游接口在穩定期的響應時間,來做到最優編排。當某些上游接口響應時間存在波動較大的情況時,目前的編排功能還無法做到動態自動最優,這部分是我們未來需要優化的方向。
11、相關文章
[1] 從C10K到C10M高性能網絡應用的理論探索
[2] 一文讀懂高性能網絡編程中的I/O模型
[3] 一文讀懂高性能網絡編程中的線程模型
[4] 以網游服務端的網絡接入層設計為例,理解實時通信的技術挑戰
[5] 手淘億級移動端接入層網關的技術演進之路
[6] 喜馬拉雅自研億級API網關技術實踐
[7] B站基于微服務的API網關從0到1的演進之路
[8] 深入操作系統,徹底理解I/O多路復用
[9] 深入操作系統,徹底理解同步與異步
[10] 通俗易懂,高性能服務器到底是如何實現的
[11] 百度統一socket長連接組件從0到1的技術實踐
[12] 淘寶移動端統一網絡庫的架構演進和弱網優化技術實踐
[13] 百度基于金融場景構建高實時、高可用的分布式數據傳輸系統的技術實踐
(本文已同步發布于:http://www.52im.net/thread-4618-1-1.html)