Author:放翁(文初)
Date: 2010/4/14
Email:fangweng@taobao.com
緣起
早在兩年前做開放平臺的時候,由于平臺的特質(zhì),就開始尋求對于Web請求異步的解決方案,當(dāng)時Jetty和Tomcat都在最新的版本中集成類似于Comet和Asyn Process的功能,但經(jīng)過測試,效果不佳,因此也沒有再深入去了解其中的一些設(shè)計理念。時隔兩年,依然在做開放平臺,但當(dāng)研究twitter和facebook api的時候,發(fā)現(xiàn)已經(jīng)有了Streaming 模式的Web請求處理模式,由此又再次的去了解今天,在Servlet3規(guī)范已經(jīng)逐漸成熟的情況下,容器,開源項目對于Web請求的異步化是否已經(jīng)有了很大的提高。在我看來,傳統(tǒng)的Web Container要改造成為異步模式要解決內(nèi)在和外在兩方面問題,內(nèi)在需要將那么多年的成熟體系打破,以分階段,異步化的方式來利用現(xiàn)有的功能(異步化很大問題就是復(fù)雜了流程,包括對多任務(wù)多線程的協(xié)調(diào),對資源的分配和回收等等),外在就是要讓用戶如何能夠在最小的代價下移植到異步模式的場景(如果代價很大,那么就很難在已有系統(tǒng)上推廣,同時使用的學(xué)習(xí)曲線也是推動新技術(shù)的一項很重要的指標(biāo))。閑話不多說,后續(xù)會從概念,到具體的概念實現(xiàn),最后到實際測試,給一個完整的評估。目標(biāo)很明確,最基本要學(xué)會設(shè)計中的一些好的思想,如果能夠找到使用場景就嘗試使用,最終在具體場景下改造現(xiàn)有系統(tǒng)比對效果。
Comet & Async Request Process
概念
Comet和Async Request Process的技術(shù)已經(jīng)不算是什么新鮮事物了,但是真正在后臺業(yè)務(wù)體系中大量使用其實還不多,主要原因還是現(xiàn)在成熟的Web容器都是基于servlet 2.5實現(xiàn),遵循的也是標(biāo)準(zhǔn)的Http無狀態(tài)應(yīng)答式請求處理模式。但隨著互聯(lián)網(wǎng)應(yīng)用不斷發(fā)展,時刻都以人為本的情況下,如何讓用戶體驗不斷提升,就要求能夠在很多場景盡量減少交互延時,盡量多的有人性化的展現(xiàn)(更多的數(shù)據(jù)交互)。因此,Comet和Async Request Process的應(yīng)用場景就誕生了,這里我只是說應(yīng)用場景誕生了,也表明其實傳統(tǒng)的應(yīng)答式請求處理模式在很多場景還是有他的優(yōu)勢。
Comet中文如果直譯叫做彗星,其實是比較形象的一種說法。Comet在某些場景下也被叫做Http Streaming。Comet作為一種技術(shù)手段,其實是指在客戶端和服務(wù)端建立連接后,可以由服務(wù)端主動發(fā)起請求將數(shù)據(jù)推送給客戶端,而客戶端根據(jù)推送的數(shù)據(jù)增量迭代的更新展示。因此服務(wù)端的數(shù)據(jù)推送就好比彗星一樣不定時的傳送到了客戶端。
Async Request Process也可以被叫做long polling,表示異步請求處理。Async Request Process和傳統(tǒng)的Http請求的差別就和BIO與NIO差別類似。Web容器或者傳統(tǒng)的Socket應(yīng)用在處理請求的時候,通常都是One Connection One Thread來處理,申請資源的回收速度根據(jù)業(yè)務(wù)處理來決定,如果后端處理的慢,那么在連接輸入和輸出部分的資源就會閑置而導(dǎo)致浪費(input buffer 和 output buffer),通過Selector和事件模型可以將流程切割成為更細(xì)的任務(wù)階段,提高資源利用率。
優(yōu)缺點及應(yīng)用場景
在Servlet3規(guī)范中已經(jīng)將Comet和ARP(Async Request Process)都作為基本內(nèi)容涵蓋在內(nèi),很多支持servlet3的容器也都已經(jīng)支持了這些特性,后面會具體的談到。但是這些特性其實也是在一些特定場景下才會體現(xiàn)出其價值,同時也存在著自身的不足之處。
Comet與傳統(tǒng)的處理模式最大的特點就在于http通道的長連接和服務(wù)端主動推送,基于事件模型。對于客戶端頻繁要去獲取狀態(tài)數(shù)據(jù)或者消息數(shù)據(jù)的場景下,通過傳統(tǒng)的請求方式來輪詢會極大消耗服務(wù)端的資源(帶寬和業(yè)務(wù)處理能力),同時反復(fù)建立數(shù)據(jù)連接也會造成服務(wù)端和客戶端性能的消耗。但另一方面,其實這兩個特點也會成為缺點,保持長連接會導(dǎo)致大量的連接資源消耗(如果沒有數(shù)據(jù)傳輸?shù)脑挘硪环矫娣?wù)端如果數(shù)據(jù)推送過于頻繁,會導(dǎo)致客戶端崩潰。
對于Comet和傳統(tǒng)Http請求處理的取舍,需要考慮這些因素:
1. 是否是單個客戶端反復(fù)需要請求服務(wù)端獲取數(shù)據(jù)或者狀態(tài)的場景。是則繼續(xù)2的判斷,否則考慮使用傳統(tǒng)的方式。
2. 客戶端數(shù)目多少。如果客戶端數(shù)目不多,則直接采用Comet,否則考慮第三點。
3. 單個客戶端對于服務(wù)端請求的頻度。(主要是由服務(wù)端數(shù)據(jù)狀態(tài)變化的頻度來決定),頻度高,則考慮才用Comet,因為如果頻度高,那么長連接的利用率就會高,則長連接帶來的消耗就可以忽略。
對于服務(wù)端數(shù)據(jù)推送過多導(dǎo)致客戶端崩潰,可以通過在服務(wù)端做數(shù)據(jù)合并或者在客戶端丟棄數(shù)據(jù)的方式來提升性能。(建議在服務(wù)端處理,減少帶寬和兩方的性能消耗)
最后就是Comet的編程模型基于事件模式和Http長連接,那么首先需要選擇新版本的容器,例如Tomcat7或者jetty6以上或者glassfish的新版本,其次服務(wù)端開發(fā)需要符合事件模型驅(qū)動的設(shè)計,客戶端也需要支持長連接的數(shù)據(jù)增量推送處理和展示。另一方面確保你的網(wǎng)絡(luò)方面在做LB的時候不會由于Http長連接過多導(dǎo)致LB性能大幅度下降,同時客戶端網(wǎng)絡(luò)如果質(zhì)量不好也會間接導(dǎo)致服務(wù)端Load上升。
Async Request Process的特點在于長連接和類似于NIO的設(shè)計理念,將服務(wù)請求處理過程更加細(xì)化,基于事件模型驅(qū)動和Selector的方式,提高了高并發(fā)下的服務(wù)處理能力,同時也在資源管理方面提供了更多的優(yōu)化空間。ARP在不同的容器中或者開源項目中實現(xiàn)的細(xì)節(jié)都有可能不同,特別是對于請求的掛起,喚醒,終止,對于資源的分配,回收,都有自己的優(yōu)化和設(shè)計思路,后續(xù)再介紹Jetty的Continuation會談到它的一些設(shè)計理念。在一定程度上Comet設(shè)計是包含了ARP的,ARP只負(fù)責(zé)一次請求的交互。ARP的優(yōu)勢就在于能夠最大限度優(yōu)化容器或者Socket連接處理能力,優(yōu)化資源分配,提高并發(fā)處理能力。劣勢在于編程習(xí)慣不符合常規(guī)的模式,對框架的要求高(異步協(xié)同,線程和資源管理等,由此帶來的復(fù)雜度會導(dǎo)致可用性會受到影響)。
對于ARP和傳統(tǒng)Http請求處理的取舍,需要考慮這些因素:
1. 是否是高并發(fā)應(yīng)用。不是則選擇傳統(tǒng)方式,是則考慮第二點。
2. 服務(wù)處理時間較長或者不確定。如果處理時間很短,業(yè)務(wù)邏輯極為簡單,則選擇傳統(tǒng)方式,否則考慮第三點。
3. 瓶頸是否在后端,優(yōu)化前端處理能力是否會產(chǎn)生反效果。如果后端處理能力強(qiáng),前端是瓶頸,則選擇ARP,如果后端已經(jīng)到了無可優(yōu)化的地步,則考慮采用傳統(tǒng)方式。
今天很多人沒有去使用ARP方式,一方面是容器的不成熟,其次也是因為當(dāng)前前端不是瓶頸,而且靠堆機(jī)器很容易解決問題,而后端例如數(shù)據(jù)庫,存儲成為了瓶頸,因此前端優(yōu)化反而會將水流放的更多,導(dǎo)致后端在沒有優(yōu)化或者無法優(yōu)化的情況下瓶頸劣勢顯示的更加突出。因此優(yōu)化系統(tǒng)不是對局部的優(yōu)化,而是對整個系統(tǒng)自下而上的優(yōu)化,任何一個關(guān)鍵路徑成為瓶頸,那么其他的優(yōu)化都失去意義。
這里也實際的舉兩個例子來說明TOP在那些實現(xiàn)中需要用到Comet和ARP。
ARP的兩個應(yīng)用場景:
TOP最大的一個難點就是服務(wù)的隔離問題,所有的淘寶服務(wù)無差別的被集成在TOP的服務(wù)集群中,TOP的一個基本功能就是Proxy,由于不同服務(wù)的處理能力不同,響應(yīng)時間也是不同,對于圖片上傳類服務(wù)處理速度可能平均要到300ms,而對于普通的請求可能就只需要幾ms,而對于容器連接資源的申請卻都是一樣的,一個Request對應(yīng)一個Thread去處理,當(dāng)后端某個服務(wù)出現(xiàn)問題或者響應(yīng)較慢的時候,那么會導(dǎo)致容器的連接資源被大量hold,最后影響到其他服務(wù)的正常中轉(zhuǎn)。這里回顧我剛才談到使用ARP的幾個判斷點,首先需要處理大并發(fā)的情況(當(dāng)前每天3億多次api的服務(wù)call,到年底估計會有10億左右的call),其次后端服務(wù)處理時間是不定的,有些長有些短,有些根據(jù)服務(wù)當(dāng)前所處壓力而不同,最后平臺的服務(wù)由于單個問題而影響全部,其實表明后端服務(wù)能力其實不是瓶頸(當(dāng)然對于出現(xiàn)問題的服務(wù)會采取降級和保護(hù)的措施,在沒有實施保護(hù)的時候需要能夠繼續(xù)提供對于其他正常服務(wù)的中轉(zhuǎn))。
TOP的請求處理流程其實可以分成很多個Pipe,有安全的Pipe,有業(yè)務(wù)預(yù)處理Pipe,有業(yè)務(wù)轉(zhuǎn)發(fā)Pipe,有業(yè)務(wù)后處理Pipe等等,其中參數(shù)的解析和處理及業(yè)務(wù)轉(zhuǎn)發(fā)等待回應(yīng)是最消耗的兩個Pipe,參數(shù)解析當(dāng)前通過lazy讀取stream來減少錯誤請求對于內(nèi)存的消耗(這同jetty的一個設(shè)計類似),對于業(yè)務(wù)中轉(zhuǎn)及等待回應(yīng)的過程其實可以作為一個異步的事件交由服務(wù)框架處理,而將請求接收處理線程掛起,釋放掉必要的資源(輸入輸出緩沖),提高整體處理能力。
Comet的應(yīng)用場景:
對于很多大商家需要能夠比較及時的了解當(dāng)前的交易處理狀況和訂單情況,因此需要有API能夠支持這樣的場景。最初采用類似于notify的方式去實現(xiàn),但是在實施的過程中發(fā)現(xiàn),外部服務(wù)notify的成本過高(服務(wù)回調(diào)地址的接收能力及可用性較差,服務(wù)端的資源大量消耗),最后修改成為客戶端有限制的定期輪詢獲取增量數(shù)據(jù)。其實,現(xiàn)在外部宣稱很多對外的http方式的notify都不是很靠譜,也有類似于pubsubhubbub的方式,但其實這也增加了一層訂閱關(guān)系的中轉(zhuǎn)和維護(hù)的成本。如果采用Comet的方式,一來可以節(jié)省大量輪詢帶來的開銷,同時復(fù)用長連接可以減少外部連接產(chǎn)生的通信消耗,在加上對數(shù)據(jù)推送的優(yōu)化合并,在一定程度上可以實現(xiàn)外部數(shù)據(jù)推送的場景。
同樣還有在產(chǎn)品推廣的業(yè)務(wù)上,當(dāng)很多外部店鋪推廣一款商品時候,如果商品發(fā)生了變化需要能夠告知推廣的應(yīng)用插件,如果靠輪詢會給TOP帶來不小的壓力,因此通過修改事件觸發(fā)Comet事件來更新商品推廣信息。
外部開源實現(xiàn)
外部有很多項目實現(xiàn)了Comet和ARP,這里就舉出其中一部分:
Web容器:
Jetty 6以上的版本(Continuation),Tomcat 6以上,JBoss配置resteasy,glassfish(glassfish的grizzly容器內(nèi)核天然支持)
開源項目:
asyncWeb(基于mina)和xsocket
具體的使用可以參看這些項目的技術(shù)文檔,我這里只是給出一些設(shè)計的特點說明,在異步模式下的Web請求(實踐篇)中結(jié)合詳細(xì)改造和測試來說明具體的實施效果及在改造過程中需要注意的內(nèi)容。
Jetty
我個人比較喜歡的一個內(nèi)嵌式容器,現(xiàn)在像GAE和Hadoop都在用它,它也是最早支持Comet和ARP的容器。在我過去的基于SCA規(guī)范的服務(wù)框架中,發(fā)布REST的內(nèi)嵌容器就采用的是Jetty,處理能力還是比較強(qiáng),在業(yè)務(wù)協(xié)議方面也支持比較廣(同時支持ajp協(xié)議,ssl等)。^_^有點打廣告了…
Jetty中的異步處理叫做Continuation,這里不是基于事件驅(qū)動模型的,而是通過Continuation這個對象來suspend/resume請求處理線程,同時它的suspend/resume也不是和傳統(tǒng)的wait/notify一樣,在阻塞的地方直接掛起或者被喚醒。它的resume其實又再次模擬了請求,會二次進(jìn)入服務(wù)處理流程,同時第一次和第二次請求數(shù)據(jù)是不共享的(可以通過attachment來傳遞數(shù)據(jù))。就這么看來,其實在suspend的時候所有的資源都被釋放了,僅僅只是保存了請求來源信息在隊列中,在后續(xù)被喚醒的時候再次模擬請求,由業(yè)務(wù)代碼在實現(xiàn)中判斷是否是第一次進(jìn)入,并且在不同進(jìn)入時請求處理過程做差異化實現(xiàn),最終將不同的邏輯通過不同階段的重入判斷來分階段處理。
Jetty有兩個技術(shù)優(yōu)化點:
1.split buffers。jetty6 采用split buffer架構(gòu)和動態(tài)buffer分配架構(gòu)。一個idle的connection沒有buffers分配給他,一旦請求收到,則會有小的請求頭buffer會被分配。大部分都是小請求,則只需要分配消息頭buffer,當(dāng)發(fā)現(xiàn)有大數(shù)據(jù)內(nèi)容的時候則分配大buffer.當(dāng)在等待回寫數(shù)據(jù)到response的時候,輸出緩存不會被分配。只有當(dāng)servlet開始寫入數(shù)據(jù)到response的時候,輸出緩存才被分配,只有當(dāng)response被提交的時候,response的消息頭緩存才被分配,并且寫出到輸出緩存,有效的執(zhí)行寫操作。總的來說分配緩存只有在他們需要的時候,而且是根據(jù)需求分配
2.延時分發(fā)。支持用異步io讀取內(nèi)容,延時分發(fā)請求到處理器,減少在處理器等待的時間。(簡單來說就是等到read了才去調(diào)用服務(wù)處理,避免無謂的資源等待浪費)
TOMCAT
Tomcat采用的與Jetty不同的設(shè)計方式,它的Comet是事件驅(qū)動的。每一個傳統(tǒng)的Servlet需要實現(xiàn)CometProcessor接口,這個接口就需要實現(xiàn)類似于原來servlet的service的event方法,event方法會在各種事件發(fā)生的時候被激發(fā),event當(dāng)前主要包含了整個處理的生命周期(begin,close,error,read)。
需要注意的是在Tomcat配置connector的時候必須選擇apr或者nio的connector,否則是不生效的。可以看到,就和NIO一樣,對于連接建立,數(shù)據(jù)可讀,都是基于事件觸發(fā),將業(yè)務(wù)和具體的連接分開,提高在業(yè)務(wù)處理較慢的情況下,服務(wù)器的吞吐能力。這種設(shè)計是比較貼近于NIO的設(shè)計思想的。
上面介紹的都是服務(wù)端的異步處理,在客戶端其實也需要實現(xiàn)異步化的處理模式,通常情況況下都是基于事件模型實現(xiàn)的。服務(wù)端和客戶端如何在異步的情況下理解消息的來源,一種是通過默認(rèn)消息發(fā)送和接收保持順序一致的方式,另一種就是通過頒發(fā)消息會話號來實現(xiàn)。
到此為止都是對于技術(shù)的介紹,沒有實質(zhì)性的使用,后續(xù)會根據(jù)TOP的實際應(yīng)用場景去嘗試改造(主要采用jetty或者Tomcat來實施),并作壓力測試,最終得出實際的使用結(jié)果,來判斷技術(shù)的成熟度。