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

一站式在線建站服務(wù)的平臺(tái)
有效解決您的所有問題
專屬客戶經(jīng)理提供技術(shù)支持
累計(jì)多年口碑和服務(wù)企業(yè)
首頁 | 關(guān)于酷優(yōu) | 短視頻運(yùn)營 | 架構(gòu)中心 | 會(huì)員模板 | 客戶案例 | 站長學(xué)院 | 付款方式 | 聯(lián)系我們 | 代理加盟 | 幫助中心 | 返回頂部
Copyright ? 2013-2025 徐州酷優(yōu)網(wǎng)絡(luò)科技有限公司 版權(quán)所有
關(guān)注
微信
關(guān)注

酷云平臺(tái)公眾號(hào)
定制
高端
定制
客服
聯(lián)系
客服
WAP
手機(jī)
訪問

酷云平臺(tái)手機(jī)端
反饋
用戶
反饋









