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