Author:放翁(文初)
Date: 2010/11/23
Email:fangweng@taobao.com
mblog: http://t.sina.com.cn/fangweng
blog: http://blog.csdn.net/cenwenchu79/
這篇文章將會(huì)從問(wèn)題,技術(shù)背景,設(shè)計(jì)實(shí)現(xiàn),代碼范例這些角度去談基于管道化和事件驅(qū)動(dòng)模型的Web請(qǐng)求處理,其中的一些描述和例子也許不是很恰當(dāng),也希望得到更多的反饋。
業(yè)務(wù)架構(gòu)設(shè)計(jì):
基于上述問(wèn)題,通過(guò)兩步走來(lái)解決。首先采用支持打破傳統(tǒng)http request生命周期管理的Web容器(很多人說(shuō)可以自己寫(xiě),其實(shí)Web容器寫(xiě)起來(lái)并不是最麻煩的,如何做好兼容和照顧好每一個(gè)細(xì)節(jié)才是漫長(zhǎng)發(fā)展的道路)。其次在容器新的線程生命周期管理基礎(chǔ)上封裝業(yè)務(wù)框架,為開(kāi)發(fā)者屏蔽底層異步化和事件驅(qū)動(dòng)模式帶來(lái)的復(fù)雜流程管理內(nèi)容。

Pipe Service Framework:
基礎(chǔ)管道體系:
很多時(shí)候設(shè)計(jì)和實(shí)現(xiàn)都會(huì)有很多細(xì)節(jié)上的差異,而這些差異往往是在事實(shí)驗(yàn)證后對(duì)體系的一種修訂,也許修訂后的結(jié)構(gòu)不如修訂前的清晰和優(yōu)雅,但是確實(shí)在性能和結(jié)構(gòu)上找到了平衡點(diǎn),下面就看看兩個(gè)基礎(chǔ)管道體系的設(shè)計(jì),后一個(gè)是前一個(gè)的演進(jìn)。

流程與角色說(shuō)明:
角色分成:Container(傳統(tǒng)的容器),dispatcher(任務(wù)派發(fā)線程數(shù)量根據(jù)性能要求可以是1-m個(gè)),job pool(存儲(chǔ)任務(wù)數(shù)據(jù)的本地緩存),event queue(任務(wù)狀態(tài)發(fā)生變化的事件存儲(chǔ)隊(duì)列),pipe register center(管道鏈注冊(cè)中心,根據(jù)job的自描述信息給出相關(guān)處理的單個(gè)管道或者管道鏈),thread pool(用于處理業(yè)務(wù)請(qǐng)求的線程池)
流程描述如下:
1. 容器解析請(qǐng)求數(shù)據(jù)。
2. 創(chuàng)建任務(wù)并存儲(chǔ)到job pool。
3. 發(fā)送job執(zhí)行消息到消息隊(duì)列。
4. 釋放容器線程,掛起請(qǐng)求資源。
5. Dispatcher阻塞方式的從event queue獲取事件消息。
6. 如果是刪除任務(wù)事件消息,則將剩余未發(fā)送數(shù)據(jù)flush到客戶端,結(jié)束本次Http會(huì)話。(刪除任務(wù)消息是在任務(wù)走完所有管道或者任務(wù)執(zhí)行超時(shí)或者任務(wù)執(zhí)行失敗產(chǎn)生)
7. 如果是執(zhí)行任務(wù)消息事件,則從job pool獲取任務(wù)數(shù)據(jù)。
8. 根據(jù)任務(wù)信息去pipe register center獲取pipe或者pipe chain。
9. 將任務(wù)數(shù)據(jù)和管道信息發(fā)送給線程池。
10. 線程池分配線程執(zhí)行任務(wù),如果當(dāng)前pipe chain執(zhí)行后并沒(méi)有完成job,則將job信息存儲(chǔ)到job pool。(這塊后面可以參看一下job 執(zhí)行邏輯圖)
11. 如果沒(méi)有執(zhí)行完畢,則可以創(chuàng)建一個(gè)或者多個(gè)執(zhí)行事件激發(fā)下一次的處理,如果執(zhí)行完畢,則創(chuàng)建一個(gè)刪除任務(wù)消息激發(fā)任務(wù)結(jié)束處理。
問(wèn)題:
1. 規(guī)范化帶來(lái)的消息事件過(guò)多,線程切換消耗的問(wèn)題。
2. Dispatcher自身任務(wù)是否繁重導(dǎo)致處理速度變慢。同時(shí)兩套線程池管理麻煩(如果Dispatcher的個(gè)數(shù)為M也就可以看作另一個(gè)線程池)。
細(xì)節(jié):
1. 利用容器本身支持請(qǐng)求掛起的方式,將容器線程池和業(yè)務(wù)線程池分割開(kāi)來(lái)。
2. 如果所有子任務(wù)都是串行化且沒(méi)有一個(gè)子任務(wù)是由外部系統(tǒng)來(lái)實(shí)施狀態(tài)遷移,則可以在一個(gè)線程中完成所有子任務(wù),減少線程切換和事件分發(fā)帶來(lái)的消耗。最極端是退化到任務(wù)交由容器線程一并完成。
3. 當(dāng)允許并行多個(gè)子任務(wù)執(zhí)行時(shí),只需要在并行子任務(wù)執(zhí)行前的那個(gè)任務(wù)完成后,分發(fā)多個(gè)任務(wù)執(zhí)行事件,并且任務(wù)執(zhí)行事件指定要求處理的Pipe,就可以讓分發(fā)器將當(dāng)前任務(wù)分發(fā)給多個(gè)線程并行執(zhí)行子任務(wù),后續(xù)詳細(xì)介紹子任務(wù)并行處理的過(guò)程。
4. Job會(huì)被多線程訪問(wèn),因此必要的屬性需要做成線程安全的。另一種模式就是抓取job的數(shù)據(jù)是個(gè)快照(clone),在結(jié)果產(chǎn)生后再鎖住合并。

角色和流程說(shuō)明:
上圖角色將線程池和消息隊(duì)列做了合并,去掉了dispatcher,event queue合并到了 Thread Pool中。
1. 容器解析請(qǐng)求參數(shù)。
2. 創(chuàng)建任務(wù)并放置到任務(wù)緩存中。
3. 發(fā)送執(zhí)行任務(wù)事件到線程池。
4. 釋放容器線程資源。
5. 線程池從自身事件隊(duì)列中獲取事件。
6. 如果是刪除事件,則直接刪除任務(wù),并發(fā)送數(shù)據(jù)到客戶端,結(jié)束本地會(huì)話。
7. 如果是執(zhí)行事件,則從pipe register center獲取pipe或者pipe chain。
8. 本地執(zhí)行pipe或者pipe chain。
9. 更新job 數(shù)據(jù)到緩存。
10. 創(chuàng)建執(zhí)行或者刪除消息事件到本地線程池隊(duì)列或者直接連續(xù)執(zhí)行。
差異:
1. 將分發(fā)器的功能散落到各個(gè)實(shí)際業(yè)務(wù)操作線程上,提升處理效率。(增加了對(duì)于消息隊(duì)列的競(jìng)爭(zhēng),不過(guò)這個(gè)代價(jià)不是很大)
2. 線程可以連續(xù)執(zhí)行子任務(wù),減少任務(wù)事件數(shù)量,減少線程切換代價(jià)。(類似于自旋鎖的方式,自己可以盡量的完成可以完成的任務(wù),帶來(lái)的問(wèn)題就是對(duì)于不同任務(wù)多階段并行執(zhí)行的策略有所減弱)
細(xì)節(jié):
和第一種模式一樣,可以退化這個(gè)模型到傳統(tǒng)的一個(gè)web容器線程處理所有的子任務(wù),減少線程切換代價(jià)。
四種方式的子任務(wù)執(zhí)行說(shuō)明:

傳統(tǒng)的串行化任務(wù)執(zhí)行模式,這種模式下可以交由單個(gè)線程全部執(zhí)行,減少線程切換代價(jià),另一方面假如3這個(gè)環(huán)節(jié)將會(huì)等待外部系統(tǒng)來(lái)更新?tīng)顟B(tài)并繼續(xù)執(zhí)行,那么到2執(zhí)行完畢可以將job放入緩沖區(qū),不產(chǎn)生事件消息,等外部操作完成后,創(chuàng)建執(zhí)行事件消息,激發(fā)后續(xù)管道執(zhí)行任務(wù)。(這種方式可以直接利用容器的掛起,來(lái)釋放容器線程,而后續(xù)操作交由后臺(tái)業(yè)務(wù)線程池執(zhí)行)
這里有點(diǎn)說(shuō)明一下,也是很多朋友問(wèn)起的,關(guān)于上下文,原來(lái)的模式中上下文一種方式是通過(guò)方法參數(shù)不斷傳遞,另一種方式保存在ThreadLocal中,而現(xiàn)在因?yàn)橐袚Q線程可能就需要做拷貝或者線程之間傳遞。在后面幾種模式中都建議直接將狀態(tài)存儲(chǔ)在本地緩存中共享,帶來(lái)的問(wèn)題就是多線程安全,一種方式是都獲取此對(duì)象,然后操作時(shí)候做鎖,一種是獲得對(duì)象快照,然后合并結(jié)果時(shí)鎖定。(這還是取決于多個(gè)線程之間處理是否需要看到對(duì)方的數(shù)據(jù)變化)

3,4兩個(gè)任務(wù)可以并行完成,同時(shí)任何一個(gè)完成即可進(jìn)入5,此時(shí)在2完成后,將會(huì)產(chǎn)生兩個(gè)執(zhí)行任務(wù)消息,并且自描述后續(xù)的Pipe,此時(shí)兩個(gè)線程可以分別執(zhí)行3,4,任何一個(gè)完畢后創(chuàng)建執(zhí)行消息,激發(fā)任務(wù)處理進(jìn)入到5流程中。(當(dāng)發(fā)現(xiàn)已經(jīng)進(jìn)入5狀態(tài)時(shí),則忽略某個(gè)過(guò)期任務(wù)消息)

與上一個(gè)圖的區(qū)別就是,3,4將不再是二選一,而是必須全執(zhí)行完畢后才可以進(jìn)入下一個(gè)階段,因此job在執(zhí)行后會(huì)先判斷是否被并行的另一個(gè)任務(wù)執(zhí)行過(guò),確定全部都Ready,則發(fā)起創(chuàng)建執(zhí)行消息。(在完成3或者4后都會(huì)判斷當(dāng)前合并結(jié)果是否符合進(jìn)入下一環(huán)節(jié)的要求,符合再發(fā)起新的執(zhí)行任務(wù)消息)

此圖是2,3兩種方案的結(jié)合,因此參照3的做法完成。
支持異步化請(qǐng)求處理模型:
上面的管道模型是較為通用的模型,但考慮到TOP現(xiàn)有業(yè)務(wù)狀況和資源消耗在上述框架下定制了簡(jiǎn)單的異步支持模型:

角色及流程說(shuō)明:
App第三方ISV軟件,Container Web容器,PipeManager管道注冊(cè)管理者(區(qū)別于通用的管道注冊(cè)中心在于他對(duì)于所有請(qǐng)求都只管理一套Pipe Chain,由他將請(qǐng)求數(shù)據(jù)傳入,并管理整個(gè)子任務(wù)的執(zhí)行和分發(fā)),AsynTaskChecker是異步執(zhí)行任務(wù)狀態(tài)變更事件的檢查者(類似于前面的事件分發(fā)器角色),ResultQueue保存事件及事件所帶的上下文,workerThead是工作線程池。
1. 應(yīng)用發(fā)起服務(wù)請(qǐng)求。
2. 容器調(diào)用管道管理器去執(zhí)行任務(wù)管道鏈。(解析參數(shù)通過(guò)Lazy方式解析字節(jié)流被離散放到了各個(gè)管道環(huán)節(jié)中)
3. 檢查容器是否對(duì)異步支持。(便于多容器兼容)
4. 創(chuàng)建上下文和輸入輸出對(duì)象(輸入輸出是管道基本傳遞參數(shù),后面給出類圖結(jié)構(gòu)可知,上下文則是放置在ThreadLocal的數(shù)據(jù),在多個(gè)管道邏輯中共享)。
5. 設(shè)置管道鏈執(zhí)行的起始點(diǎn)(為了異步化后再次進(jìn)入管道鏈無(wú)需重新執(zhí)行前面執(zhí)行過(guò)的管道作處理)。
6. 循環(huán)執(zhí)行管道鏈。
如有異步管道在管道鏈中:
a) 復(fù)制管道上下文,保存當(dāng)前執(zhí)行的管道位置。
b) 掛起請(qǐng)求,釋放容器線程資源。
c) 創(chuàng)建線程執(zhí)行異步化管道。
d) 保存任務(wù)到隊(duì)列,等待外部處理結(jié)束改變?nèi)蝿?wù)狀態(tài)。
e) 推出循環(huán)執(zhí)行后續(xù)管道
7. 判斷是否是異步執(zhí)行后的重入,如果是則提交異步結(jié)束事件,讓容器在這次管道鏈執(zhí)行后自動(dòng)提交數(shù)據(jù)到客戶端,結(jié)束本地Http請(qǐng)求會(huì)話。
8. 釋放上下文等線程本地資源。
9. 返回容器,容器判斷是否有掛起請(qǐng)求,如果請(qǐng)求結(jié)束則返回結(jié)果到客戶端。
10. 容器自檢查從掛起到當(dāng)前是否處于執(zhí)行超時(shí)(每次掛起請(qǐng)求就會(huì)產(chǎn)生一個(gè)超時(shí)事件,容器循環(huán)的校驗(yàn)這些事件)
11. AsynTaskChecker循環(huán)的檢查隊(duì)列中的任務(wù)是否已經(jīng)完成,如果狀態(tài)變更為完成,則提交到給線程池繼續(xù)執(zhí)行后續(xù)的管道鏈。(處于性能考慮,可以將未完成的對(duì)象先不放入隊(duì)列,等到后端服務(wù)處理完畢再放入,這樣AsynTaskChecker消耗會(huì)大大降低,任務(wù)超時(shí)完全交給容器來(lái)處理,不由業(yè)務(wù)方來(lái)處理)
細(xì)節(jié):
主要目的是將容器和業(yè)務(wù)線程池分開(kāi),這樣業(yè)務(wù)線程池可以采用后面提到的權(quán)重線程池,通過(guò)對(duì)權(quán)重線程池的權(quán)重模型設(shè)置來(lái)滿足根據(jù)業(yè)務(wù)或者根據(jù)服務(wù)健康狀況來(lái)不均衡的分配線程執(zhí)行不同的業(yè)務(wù)請(qǐng)求。
后端系統(tǒng)的NIO異步方式能夠利用操作系統(tǒng)的中斷來(lái)激發(fā)改變對(duì)象狀態(tài),節(jié)省前端業(yè)務(wù)線程等待消耗。(如果后端是非異步化的操作,那么執(zhí)行線程只是從容器線程變?yōu)榱藰I(yè)務(wù)線程,當(dāng)然可以讓業(yè)務(wù)線程更加輕量)
系統(tǒng)中盡量減少線程切換(能夠一個(gè)線程干完的,盡量一個(gè)線程執(zhí)行多個(gè)子任務(wù)),盡量減少內(nèi)存拷貝復(fù)用對(duì)象(當(dāng)然復(fù)用的代價(jià)就是同步問(wèn)題,因此取決于數(shù)據(jù)操作沖突的概率選擇使用快照還是引用)。

上圖的設(shè)計(jì)省略了隊(duì)列和檢查者,直接交由業(yè)務(wù)線程阻塞方式等待返回,并直接執(zhí)行后續(xù)的管道,其實(shí)也就是對(duì)第一種場(chǎng)景的簡(jiǎn)化,在后端服務(wù)非異步方式的情況下,推薦這種方式。
總的來(lái)說(shuō),任務(wù)切割執(zhí)行在設(shè)計(jì)上會(huì)覺(jué)得很清晰,但是還是要看整體處理時(shí)間的分布,如果整個(gè)事務(wù)處理消耗的時(shí)間很短,那么切割帶來(lái)的復(fù)雜度和內(nèi)部消耗就會(huì)得不償失,采用簡(jiǎn)單的方式來(lái)實(shí)現(xiàn)可以滿足業(yè)務(wù)上的需求(分離容器和業(yè)務(wù)線程,根據(jù)業(yè)務(wù)需求和系統(tǒng)動(dòng)態(tài)性能決定線程資源分配),也能保證性能。
權(quán)重線程池:
將請(qǐng)求全程處理從容器線程池分離到業(yè)務(wù)線程池后,可以使用帶權(quán)重的線程池來(lái)動(dòng)態(tài)調(diào)整請(qǐng)求線程資源分配,下面是一個(gè)簡(jiǎn)單的權(quán)重線程池的實(shí)現(xiàn)。
目標(biāo):執(zhí)行的任務(wù)實(shí)現(xiàn)接口getkey來(lái)用于判斷是否有空余線程可以執(zhí)行請(qǐng)求處理任務(wù)。資源被分成兩種:默認(rèn)全局可使用資源,給特定請(qǐng)求預(yù)留資源。配置分成兩種,限制最大使用線程數(shù),預(yù)留特定請(qǐng)求的線程數(shù)。

上圖是簡(jiǎn)單的請(qǐng)求任務(wù)執(zhí)行流程圖,不多解釋了。下圖是狀態(tài)轉(zhuǎn)換圖:

Wait到doing的轉(zhuǎn)換和init到doing的轉(zhuǎn)換一樣,就沒(méi)有重復(fù)畫(huà)了。內(nèi)部的一些標(biāo)識(shí)解釋(totalCounter全局的計(jì)數(shù)器,maxThreadPoolSize線程池最大線程數(shù),defaultCounter是沒(méi)有設(shè)置預(yù)留或者限制的請(qǐng)求的計(jì)數(shù)器,defaultThreshold是maxThreadPoolSize – sum(預(yù)留線程),keyCounter表示設(shè)置了預(yù)留或者限制的請(qǐng)求自身標(biāo)識(shí)(自身標(biāo)識(shí)通過(guò)getkey接口獲得)計(jì)數(shù)器,leave表示某一類請(qǐng)求設(shè)置的預(yù)留的數(shù)值,limit表示某一類請(qǐng)求設(shè)置的限制的數(shù)值)
上圖中大括號(hào)中的是場(chǎng)景描述,例如:{Limit Mode}keyCounter <= limit && defaultCounter <= defaultThreshold表示在設(shè)置了限制模式的場(chǎng)景下符合當(dāng)前請(qǐng)求類型計(jì)數(shù)器(當(dāng)前請(qǐng)求類型通過(guò)請(qǐng)求實(shí)現(xiàn)getkey接口返回?cái)?shù)據(jù)來(lái)區(qū)別)小于限制且默認(rèn)計(jì)數(shù)器小于默認(rèn)閥值時(shí)狀態(tài)轉(zhuǎn)變。
一點(diǎn)小技巧:在存儲(chǔ)預(yù)留和限制的閥值時(shí),因?yàn)榇鎯?chǔ)在一個(gè)map中,通過(guò)將閥值設(shè)置為負(fù)數(shù)來(lái)區(qū)分開(kāi),這樣節(jié)省了區(qū)分閥值類型的工作。(這點(diǎn)可以在很多場(chǎng)景中考慮,比如說(shuō)有多個(gè)類型的數(shù)據(jù)配置需要存儲(chǔ),可以通過(guò)數(shù)據(jù)區(qū)間的劃分來(lái)判斷是什么類型的,提高判斷效率)
Comet Push Framework:
服務(wù)端實(shí)現(xiàn):這期做了很簡(jiǎn)單的服務(wù)端實(shí)現(xiàn),也是為了驗(yàn)證原型,標(biāo)準(zhǔn)的REST實(shí)現(xiàn)。

POST操作,用于新增資源,操作后得到資源返回,會(huì)話非長(zhǎng)連接。

GET操作,獲得當(dāng)前請(qǐng)求的資源,會(huì)被加入到資源關(guān)注者列表中,保持長(zhǎng)連接,用于資源變更后推送變更后的資源對(duì)象。

PUT或者Delete操作,短鏈接,同時(shí)產(chǎn)生變化事件,交由后臺(tái)線程執(zhí)行通知?jiǎng)幼鳌?br />

批量執(zhí)行通知消息。
1. ResourceBoard阻塞式的從隊(duì)列中獲取事件通知。
2. 創(chuàng)建臨時(shí)事件存儲(chǔ)Map。
3. 如果存在通知事件,判斷是否屬于刪除事件(此類事件發(fā)生在異常發(fā)生或者正常結(jié)束),如果是刪除事件,立刻提交給后臺(tái)線程池執(zhí)行刪除動(dòng)作。(刪除動(dòng)作就是獲取刪除資源的follow列表,然后關(guān)閉所有follow的長(zhǎng)連接)
4. 如果屬于修改事件,判斷當(dāng)前資源的刪除事件是否已經(jīng)保存在臨時(shí)存儲(chǔ)Map中,如果有就不再加入修改事件直接忽略,否則就放入Map。
5. 判斷當(dāng)前循環(huán)累積事件是否超過(guò)一定時(shí)間或者存儲(chǔ)的消息量已經(jīng)超過(guò)一定值,如果是就跳出循環(huán),如果否,則繼續(xù)從隊(duì)列中獲取數(shù)據(jù)循環(huán)判斷,直到隊(duì)列為空。
6. 批量執(zhí)行臨時(shí)存儲(chǔ)中的事件消息,如果是修改,則獲取資源的follows來(lái)推送變更后的數(shù)據(jù)。
細(xì)節(jié):
內(nèi)部對(duì)于follow的有效性管理是在發(fā)送數(shù)據(jù)時(shí)判斷的,如果出錯(cuò)就會(huì)產(chǎn)生刪除事件。
對(duì)于消息批量處理主要是針對(duì)數(shù)據(jù)不斷被修改,合并這些無(wú)用消息而作,但是某些場(chǎng)景也許就需要所有的修改痕跡,那就不能簡(jiǎn)單合并,因此資源需要提供類似合并的接口實(shí)現(xiàn)來(lái)保證獲取的正確性。
問(wèn)題:
海量長(zhǎng)連接的支持。
采用簡(jiǎn)單的Http InnerFrame + js實(shí)現(xiàn)客戶端增量展現(xiàn)會(huì)使得頁(yè)面數(shù)據(jù)越來(lái)越多,到一定程度需要放棄連接重新建立follow,減輕客戶端和服務(wù)端雙重壓力。XHR的方式在各種瀏覽器中支持的不一致。
代碼實(shí)現(xiàn),Demo及測(cè)試效果
待續(xù)….