本文由去哪兒網技術團隊田文琦分享,本文有修訂和改動。
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)