海量企業網站模板 · 任您選擇
美出特色,精出品質,一切為了企業更好的營銷
美出特色,精出品質,一切為了企業更好的營銷
背景
我們在入口層有一個提供HTTP服務的應用。隨著業務的復雜,一個用戶請求的處理過程,涉及多個對后端遠程服務的調用。為了實現的簡單,目前都是使用同步方式完成的,也就是在一個請求的處理過程中,會占用一個容器線程進行邏輯運算和同步遠程調用。這種開發方式的好處是直觀,開發成本低,但也帶來了一些穩定性和資源浪費的問題。對于我們的HTTP服務來說,同步化的實現帶來下面這3個問題。
下游服務超時帶來的服務可用性問題。一部分的請求超時會導致HTTP服務線程池被占滿,從而導致其它的請求無法獲取到線程資源而失敗。
性能問題,多個對遠程服務的調用串行執行,導致服務響應時間長。
容量問題,服務吞吐量受限。每個請求長時間占用線程,導致線程得不到充分利用。
為了解決這些問題,結合目前使用的技術棧以及適應成本,我們對HTTP服務進行了一次異步化改造。
解決方案
異步化編程中聞名的Callback Hell,讓不少同學望而止步。當業務復雜的時候,各種call back互相嵌套,使代碼變得更加容易出錯和不易理解。業內也有有不少框架提供了異步化編程支持,有以下三個思路:
纖程
纖程可以認為是輕量級的用戶線程,脫離了OS的調度機制,在應用級別進行調度管理。由于它只維護了基本的執行棧信息,并不立即分配執行資源,因此,它可以輕松創建成千上萬的纖程(受內存大小的限制),通過極少的線程完成對纖程的調度執行。這個方向的代表有微信團隊開源的libco,以及在語言層面上支持的Go語言等。libco hook了底層IO相關的系統函數,通過底層IO事件驅動纖程的調度執行。當遇到同步調用網絡請求時,libco自動注冊回調監聽器,并讓出CPU。而在IO事件完成或者超時候,自動恢復纖程,然后調度執行。它的實現機制決定了它非常適合依賴耗時IO服務的實現。承載了微信千萬級調用的一個基石。不過遺憾的是,libco是一個高效的c/c++協程庫,并沒有在JVM上實現。
Quasar是在JVM之上實現了纖程機制,基本可以在Quasar的類庫基礎上,以同步的模式來編寫異步的代碼。在真正執行代碼前,通過編譯或者Instrument Agent的形式織入相關的字節碼。從頭起步引入纖程還是一個不錯的選擇。對現有項目的改造,需要對現有的線程類修改成纖程類,這需要改動我們底層非常多的中間件。另外業內公布的使用經驗較少,后續可以持續關注它的發展。
Actor模型
Actor模型其實不是什么新概念了。近些年有逐漸流行的趨勢。Actor模型中一個核心概念就是Actor實體。每個Actor實體負責一個邏輯計算。傳統并發編程都是基于共享內存的方式來達到多線程之間的通訊的目的。Actor之間不共享數據,也不直接通訊,而是發送或者接受mailbox/queque中的消息來達到通訊的目的。Actor之間通過消息來驅動。正式由于發送者與接受者的分離,是的Actor具有內在的并發特性,它可以不用考慮actor之間的同步問題,不受限制的調度執行收到消息的Actor,從而優化了IO等待的問題。Scala,Golang等在語言層面支持Actor模型。Scala的新版中,推出Akka來完成Actor模型,并有了Java版本。但是需要引入新的API,對現有業務代碼塊改造成Actor模型,對現有代碼改動較大。
RX
Rx也是一種編程模型,它嘗試提供統一的異步編程接口封裝來操作一個可觀察的數據流。其吸收了函數式編程的優秀思想,并將觀察者,迭代器模式實現的淋漓精致。當下流行的語言,基本都有相應的實現。 如RxJava類庫,即提供了java版本的實現,RxJava在Netflix的Zuul項目中得到成功的應用。Rx看起來更像是一種編程思想的突破。它提供了統一的函數式的風格編程接口來簡化異步程序的編寫,同時內部也通過callback機制,比Actor能獲得更好的響應速度。在調研過程中,我們發現它同樣要求對現有代碼做較大改動,并將之前的同步模式轉換成函數式編程風格。
綜合來看,以上一些優秀的框架并不能立即利用到我們的項目中,引入成本還是很高的。結合現有技術架構上,以及產品正在快速迭代的環境下,我們對HTTP服務進行了一次輕量級的異步化改造。這次改造,引入Graph-Based Execution Engine來解決服務之間復雜的依賴關系,集中管理異步狀態。結合Servlet 3.0提供了請求及釋放tomcat容器線程的接口,充分利用Servlet容器線程資源。最后,通過spring mvc的異步模塊銜接這兩種異步機制,達到了全棧異步化的目的。
原理分析
Servlet從3.0開始,增加了異步規范。spring mvc從3.2開始也支持異步Servlet 3.0。針對現有技術棧,實現全棧異步化可以通過下面的一段代碼來說明:
可以看到,orderService.createOrderAsync(request) 這個調用在請求發出后,不等待返回結果,而是立即返回。在返回的future對象上注冊了一個監聽器。最后返回DeferredResult。spring mvc在收到返回結果為DeferredResult(當然也可以是WebAsyncTask和Callable)時,將調用
AsyncContext context = HttpServletRequest.startAsync(req, response);
來獲取上下文,然后退出容器線程。當createOrderAsync完成得到結果后,注冊在future上的監聽器被喚起開始執行,此處忽略中間的一些處理,直接將RPC結果設置在DeferredResult上。spring mvc在獲得執行結果后,通過調用Servet的上下文
context.dispatch();
來通知容器繼續執行后續操作,例如重新進入spring mvc 攔截器的complete流程,最終輸出結果到客戶端。整個流程可以用下圖表示:
圖中3個框表示整個請求被打散在3個階段執行。第一框到第二個框之間表示RPC服務正在執行。此時處理請求的線程已經釋放。它可以繼續接受處理其它請求。RPC服務有返回值或者超時的時候,會在單獨的一個線程池中喚起注冊的監聽器。最終通知Servlet容器來繼續執行第三個框中的interceptor.complete。通過回調通知的機制,將使CPU得到充分的利用。避免了啟動一個寶貴的線程來等待IO的完成。
Graph-Based Execution Engine
真實的業務場景要比上面的代碼復雜的多。例如下單業務,一般都會依賴用戶,報價,支付,優惠等服務。服務之間存在依賴關系,如黑名單服務校驗通過才能提交訂單。還有一些服務之間處于對等關系,互相之間沒有依賴,可以并行調用,以降低服務的整體響應時間。如下圖所示,這是一個常見的服務依賴關系:
圖中A、B、C沒有依賴關系,實際上可以并行執行。C服務不關心返回結果,因此將調用通知發出后及可結束。D服務需要等待A的結果,E需要等待B、D的執行結果。使用傳統的異步編程的話,大概是這個樣子:
可以看到服務的依賴關系隱藏在代碼行間,業務邏輯穿插在各個callback中,中間引入了ListeableFuturefutureBT 管理異步狀態。不太易于閱讀及維護。為此,我們提供了一個Graph-Based Execution Engine(GBEE)。GBEE的主要目標在于解決以下:
(1)管理服務之間的依賴關系
將服務之間的依賴關系從業務代碼中分離出來,通過一個有向無環圖的數據結構來描述服務之間的依賴關系。圖中每個節點保存了其前驅(后驅)節點。每個節點可以執行的前提條件是其所有前驅節點都完成。
(2)統一注冊callback
每個節點可以覆寫callback,用來注冊自身的監聽器。一般用來轉換結果,記錄監控。callback統一由執行器管理注冊。避免在代碼嵌套中注冊監聽器。
(3)使用異步事件驅動執行
在GBEE中統一注冊異步事件監聽器,在事件發生時驅動執行callback,或者在條件成熟時,喚起下一個節點的執行。
具體做法:
(1)將業務邏輯分離成多個節點,每個節點負責具體的業務邏輯執行,但沒有任何狀態,例如發起異步RPC調用,并返回ListenableFuture。
(2)通過配置文件來定義依賴管理
每個Node定義了自己的parents,即表示依賴關系。spring本身提供了服務的依賴管理能力。因此其依賴關系定義如下:
(3)提供了一個執行器Graph-Based Executor 來負責統一注冊監聽器以及管理異步狀態。
每個請求到達后,通過上面的依賴配置,可以構造出一個Graph-Based執行器:
Graph會找到根節點,多個根節點可以同時并行。
apply(node, context) 是一個遞歸調用,每次執行完當前node,主動探測下是否可以執行父節點為自己的節點:
Graph-Based Executor 將業務代碼與底層的異步機制解耦,使得各個節點更加關注自身業務。
后記
在遷移具體業務時,也遇到一些比較常見的問題,供后續的實施者參考。
(1)公司RPC服務主要送是dubbo,利用公司的基礎組件,可以方便使用異步調用。
(2)線上還有很多應用使用tomcat 6,Servlet 3 從tomcat 7開始支持,應該將相關應用升級到tomcat 7.
(3)web.xml 配置有幾個比較重要的配置。
為了讓spring mvc真正啟用異步支持,除了需要將org.springframework.web.servlet.DispatcherServlet的異步選項激活,即:true
還需要將此servlet之前的所有filter的async-supported設置成true。只要中間有一個filter沒有設置,后面的設置都是無效的。并且在后續開發中,如果增加了filter,也一定要配置上。
(4)ThreadLocal 問題。
現有系統的一些通用的上下文參數通過ThreadLocal傳遞。異步化改造后,代碼并不是始終在請求線程中執行。這就使得通過ThreadLocal傳遞的變量失效。我們采用了兩種方法來解決,一是一些業務代碼的改造,通過參數的形式來傳遞。另一種是將一些通用變量存入HttpServletRequest的Attribute里。異步上下文中保持了對HttpServletRequest的引用。然后通過工具類直接從HttpServletRequest提取公共變量。
(5)異常處理
在同步代碼中,一般我們會自定義一些業務異常,這些業務異常被捕獲后,根據異常理性及狀態碼,做一些業務邏輯。ListeableFuture繼承的Future接口規定了,在異步計算過程中拋出的所有異常封裝在ExecutionException中。此時,同步代碼中的catch,就不能捕獲ExecutionException了。此時業務代碼就需要修改捕獲的具體類型,然后通過Exception.getCause()來獲取原始異常。這塊可以通過Graph-Based Execution Engine統一處理。將原始異常轉換后,調用節點的onException.
--結束END--
本文鏈接: http://www.u0rvp.cn/station/experience/1997.html (轉載時請注明來源鏈接)
下班PC閱讀不方便?
手機也可以隨時學習開發
一站式在線建站服務的平臺
有效解決您的所有問題
專屬客戶經理提供技術支持
累計多年口碑和服務企業