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

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

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

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

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

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

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

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

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

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

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

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

POST操作,用于新增資源,操作后得到資源返回,會話非長連接。

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

PUT或者Delete操作,短鏈接,同時產(chǎn)生變化事件,交由后臺線程執(zhí)行通知動作。

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