Author:放翁(文初)
Date: 2010/6/28
Email:fangweng@taobao.com
圍脖: http://t.sina.com.cn/fangweng
前話
在前面的文章中,先給出了Web服務請求異步處理的壓力測試報告,從數據角度描述了支持Web請求異步化的容器在不同并發用戶下的處理能力及性能消耗。本文從概念的角度對于應用系統異步化,Web服務請求異步化和Web請求異步化規范及實現三方面做一個介紹,為系統異步化改造做好基礎準備。(同樣,文中大部分都是個人意見和想法,非完全正確,歡迎討論)
應用系統異步化
Web服務請求異步化也是應用系統異步化的一種,因此首先談一下對于應用系統異步化的一些看法和認識。
隨著系統不斷積累和發展,系統模塊化是必然趨勢,而模塊之間的耦合性和依賴會直接影響系統的穩定性和可用性,因此系統異步化概念就產生了。異步化和其他技術一樣,不是“萬能藥”,在適合的場景揚長避短才能夠體現它的優勢,提升系統的可用性和效率。
異步化要素:
1. 會話。
異步模式下,請求和結果不在一次交互中,因此需要通過會話的設計方式(增加會話碼)來保證請求和結果的一一對應。另一種方式可以不需要會話,但是要求請求和結果是保證順序的。(這種設計方式會使得系統的資源復用受到限制,同時容錯很難保證,容易“串”,早先用NIO去實現低版本的Memcached客戶端協議就存在沒有會話號的問題)
2. Callback or Stateful Result。結果產生如何通知請求方,有兩種手段:a. Callback,通過服務方主動推送的方式將結果推送給服務調用者。(如果是進程內,可以通過實現定義的回調接口方式,如果是進程外,可以通過注冊URL或者WebService等方式來實現)。b. Stateful Result,是通過調用方不斷輪詢執行結果,當服務提供者服務處理完畢后,修改Result的狀態。優劣不在此贅述。
異步化特點:
1. 系統間可用性和處理能力松耦合。
AàB(A系統依賴于B系統)
A的可用性最差情況是sum(A系統自身可用性,B系統可用性)。
A系統的處理能力是min(A系統處理能力,B系統處理能力)
作用:差別化系統設計,A系統可能是前端系統要求高處理能力,B系統是后段系統,要求高一致性,此時兩個系統如果異步方式依賴,A可以設計的較為輕量化,在高并發下有很好的吞吐量。B系統可以設計的有足夠的容錯和備份機制,在效率上適當放低要求。(我們時常在設計系統的時候談CAP原則,不同系統和流程不同階段對于這三個因素的要求都是不同的,因此通過異步的方式防止由于對于其他系統的依賴導致本系統的CAP無法有效的權衡)
2. 資源的有效利用。
異步模式天生就需要事件驅動模型支持,而事件驅動模型在高并發情況下對資源管理和使用十分有效。NIO設計就是典型的異步模式,基于對信道事件的監聽和分發,最大程度上復用信道,復用接收和發送緩存等相關資源,提高資源利用率,增強系統的服務能力。
3. 系統復雜度增加。
錯誤暴露及時性降低。當B系統出現問題時,A系統知曉情況被動,整個流程問題暴露及時性降低。因此要求B系統和A系統作好更多地容錯,異常檢查工作。(例如流程超時處理機制等)
4. 整體業務流程處理時間增大。
由于B反饋給A的結果是異步化,因此A就有了上述兩種方式去獲取結果:Pull和Push,Push就是B主動回調A的服務(及時性較強,不過仍有部分時間消耗),Pull的間隔時間決定了A獲得結果可能存在的延時性。
異步化場景:
1. 模塊間依賴,系統間依賴。
這里描述的是粒度,異步化最終是對一個流程中的部分環節的弱化:一種就是弱化非關鍵路徑來保證關鍵路徑的可用性和效率。另一種就是弱化整體流程的及時性和一致性,提高部分環節的處理能力和可用性。但不論哪一類弱化都是基于一定的業務粒度,模塊應該說是最小的業務粒度,在模塊內部設計就要求緊耦合。
2. 資源決定一切
異步的使用場景往往是為了節省資源,但是節省帶來的成本就是復雜度,當資源本身就不是問題(并發不高,資源足夠)的情況下,無需要選擇異步方式來增加復雜度,同時反而降低了可用性和穩定性。
3. 整體觀與局部觀
AàB,A依賴于B,此時采取異步化的方式,A的處理能力得到提高,大量的請求被提交到B上,B由于請求堆積,導致性能下降甚至崩潰,那么其實對于整體處理流程來說并沒有隨著A處理能力曲線上升而上升,因此此類優化沒有協調好局部和整體。
AàB AàC,A依賴于B,A依賴于C,異步改造A和B之間的關系,但是A還是受限于C,那么此類優化未必有效果,同時增加了復雜度。(這里就要關注整體流程的關鍵路徑,關鍵路徑的優化才是有效的優化)
Web服務請求異步化
隨著Servlet3.0的日趨成熟和各個Web容器廠商的支持,Web服務請求異步化在很多領域開始慢慢被使用和熟悉。
Web服務請求異步化與NIO的概念是不同的(Web服務請求異步化可以基于BIO的底層交互也可以基于NIO的底層交互),Web服務請求異步化是業務層的異步設計,與普通的應用系統異步化差別在于Web請求有固定的規范,請求流程和接口較為固定,同時請求底層的資源管理交由Web容器處理,因此在第一部分中所談到的異步的優勢劣勢,適用場景同樣適合Web服務請求的異步化。
這里也順帶談一下為什么NIO在Web領域里面被沒有像后臺系統一樣被得到廣泛使用:
a. 信道復用的投入產出比例。Http請求是無狀態的請求應答模式,信道復用概率不像內部后臺系統那么高,再加之業務時間占整體流程時間絕對大的比例,那么投入產出不成比例。
b. Web請求受限于Servlet規范的生命周期管理,導致前段無論如何異步,在服務處理過程中都是同步阻塞模式,因此異步不徹底,無法體現NIO的優勢。
三種Web請求模型的演進:
1. Thread Per Connection。在BIO的模型下,每次連接都會被分配一個線程,線程負責底層數據接收發送,業務處理。
2. Thread Per Request。在NIO的模型下,將不再為連接單獨分配線程,而為每一次請求事件發生創建線程,處理業務,對于底層的數據發送和接收資源做到了共享,同時數據通道得到了共享。
3. Thread Per Service Event。在Web請求異步處理模型下,底層數據處理可以依賴于第二個處理模型,上層業務處理將業務狀態對象獨立于業務處理流程,通過事件驅動模式來分階段觸發業務各階段處理,為每一次事件處理創建線程和資源(通常是資源池),最終提高資源利用率。
介紹一下四種場景下的Web請求處理:
角色介紹:(在下面的場景中會涉及到一些角色,有些隸屬于容器,有些是外部服務和資源)
:并發用戶。T代表有T個并發用戶。
Conn Thread Pool :連接線程池,屬于Web容器的一部分,作為響應和處理請求的線程資源獲取來源。
Conn Thread:線程連接,從線程連接池中獲取到的線程。
Service Provider:業務實現者。可以是本系統的業務實現,也可以是其他系統的業務實現,可以是異步方式的返回也可以是同步方式返回結果。(N)代表本身業務處理需要N個時間單位。
Worker Thread Pool:外部工作線程池,可以接收處理“耗時”的業務流程。
1. 非異步化Web請求處理

從這個場景可以看出,在非異步化Web請求的容器中,不論后端服務是否采取異步化,由于請求本身需要在一次阻塞式交互中返回,那么連接線程池中的線程在后端服務異步化的同時依然Hold沒有被釋放,當前端并發量增加的時候,容器的吞吐量就會成為瓶頸(就算后端服務能力還有很大的剩余)。
Resource表示消耗的資源:在T個并發用戶下,需要消耗T個連接線程資源。
Response表示響應處理時間:N個時間單位。
2. 異步化Web請求處理,后端服務提供者為阻塞模式。

這種模式下,增加了一個工作者線程池,在做后端服務處理的時候,工作線程池的線程取代了連接線程池的線程,在工作者線程獲得了掛起的異步上下文以后, 釋放了連接線程池的線程,當業務執行完畢以后,工作者線程通過異步上下文,提交返回結果,最后釋放自身資源。
Resource:少量ConnThread + T個WorkThread。
Response:N + workerThead消耗(創建,異步調用等)
可以看到,對于后端沒有支持異步化的情況下,僅僅前端容器異步化能夠起到效果的前提是:1. WorkThread很輕量化,消耗資源遠小于連接池線程資源。2.WorkerThread消耗占整體消耗的很小一部分,甚至可以忽略。此時通過用輕量級線程池替換容器連接線程池可以較好的提高效率和資源利用率。
3. 異步化Web請求處理,后端服務提供者為非阻塞模式。(Push & complete mode)

Push & complete mode指的是對于后端服務結果的反饋是Service Provider主動push給服務調用者,在Web請求異步化過程中,返回請求結果是直接通過調用異步上下文的complete事件來觸發commite的。(想對于resume的喚醒重入方式)
此場景服務提供者支持異步化處理業務請求,因此連接線程池的線程負責處理最輕量一些操作,然后將業務請求轉交給服務提供者,同時將異步上下文傳給服務提供者的處理線程,就此連接池連接資源釋放。當服務完成后,服務端線程通過異步上下文獲取到輸出對象,將處理結果直接返回給客戶。
Resource:少量Conn Thread。
Response:N個時間單位。
可以看到容器請求異步化結合后端服務體系異步化能夠起到最好的效果,但有一點是要注意的,前端線程池在沒有異步化以前吞吐量取決于它的線程池大小和后端服務處理速度,而當異步化后,吞吐量取決于它的線程池大小和異步化帶來的消耗,明顯并發服務能力得到了很大的提升(特別是后端服務耗時嚴重的時候),這樣就意味著會有更多的服務請求流向后端,當后端處理能力無法支撐的時候,那么N那個時間單位就會上升,同時穩定性也會產生問題,因此反而會起副作用,因此這種模式需要評估后端服務能力,保證異步化后服務質量依舊。
4. 異步化Web請求處理,后端服務提供者為非阻塞模式。(Pull & Complete mode)

Pull & complete mode指的是對于后端服務結果的反饋是Service Provider被動的等待其他監控線程定時pull結果對象并比對結果狀態確定是否完成,在Web請求異步化過程中,返回請求結果是直接通過調用異步上下文的complete事件來觸發commite的。(想對于resume的喚醒重入方式)
這個場景和前一個場景差別在于對于結果的獲取方式,同樣對于后臺服務來說,前端系統的業務處理不會侵入到它的業務代碼(Push方式會要求Service Provider回調或者直接提交結果到客戶端)。這種場景需要增加一個結果隊列,連接線程池中的線程責任就是調用后端服務,然后將Future結果和異步上下文作為服務結果放入隊列。由一個小的工作者線程池定時檢查任務執行情況,當執行通過時,直接取出結果集,將結果通過異步上下文輸出到客戶端。
Resource:少量的Conn Thread + 部分worker Thread
Response:N + 異步化消耗的時間(結果輪詢消耗的時間)
就系統本身來說,穩定性和可用性有所“折扣”,增加了對于隊列和工作者線程的依賴。
5. 異步化Web請求處理,后端服務提供者為非阻塞模式。(Push & resume mode)

Push & resume mode指的是對于后端服務結果的反饋是Service Provider主動push給服務調用者,在Web請求異步化過程中,在業務處理結束后,重入當前同樣的Servlet中(帶上結果),由Servlet在新的請求中返回結果給客戶端。(在Servlet3.0中是允許dispatch到不同的Servlet中,這樣帶來的靈活性就比較高了,在jetty的continuation中只允許重入當前請求的Servlet)
重入機制會給容器帶來一定的壓力,一次請求在容器這邊變成了兩次或者多次請求,同時對于Servlet中的業務代碼需要去關注是否是原始請求還是被模擬的重入的請求,區別化對待。
Resource:部分的Conn Thread。
Response:N + redispatch time。
連接線程消耗要比普通的complete來的多,同時消耗時間也比complete模式來的大。
Web請求異步化規范及實現
當前實現Web請求異步化的容器有Jetty6,Jetty7,Tomcat7.其中Jetty6支持他特有的Continuation機制,jetty7支持Continuation和Servlet3.0,Tomcat7支持Servlet3.0.后面就從Jetty的角度去介紹Continuation機制,再比較Continuation與Servlet3.0的差異。
Continuation
Jetty可以使用BIO或者NIO的底層來支持Continuation,不過就效果來說肯定是NIO的效果好,這里給出兩個圖(Jetty的NIO的類結構圖和Continuation的交互圖),從中可以看到這兩塊設計的實現。

Jetty NIO 類圖
Jetty作為外部容器或者嵌入式容器入口都是Server,Server中包含了Connector(這塊實現的不同就決定了是用BIO還是NIO的模式,這里描述的是NIO模式,因此Connector是SelectorChannelConnector),ThreadPool成為整個系統中的線程資源池,用來完成事件驅動模型的各種需求。為了提高性能,NIO模式下的Connector包含多個Selector(即SelectSet)和Acceptor,通過Acceptor循環檢測來觸發多個Selector檢查IO事件,當有請求產生的時候創建SelectChannelEndPoint來分配必要的資源處理請求(與前面描述的Thread Pre Request是一致的,確切的說是One Resource Pre Request)。
圖片過大了,請點擊看(http://www.flickr.com/photos/33194437@N03/4746678724/sizes/l/)
Continuation 交互圖
1. 用戶發起請求。
2. NIO的Selector接收到了IO事件創建了Endpoint分配了相應的資源。
3. 同時將Endpoint作為一個任務封裝后插入到線程池隊列中,等待工作線程執行請求處理。(IO事件處理到此結束)
4. 工作線程池中線程執行請求處理。
5. 先讀取請求數據。
同步模式:
6.1 -6.4調用內部的handler串行化處理請求,最后返回處理結果。
異步模式:
7.1 執行Handler的業務邏輯。
7.2 掛起請求,進入異步模式。
7.3 創建異步事件置入到Request中。(一來用于容器后續判斷當前請求是否處于同步模式,是否需要提交response,另一方面用于異步事件的超時檢測)
7.4 在Servlet的原生命周期的方法中(service,doget,dopost …)創建新的線程去執行業務操作,并且將Continuation傳遞給線程用于后續complete或者resume來提交業務處理結果或則重新分發進入Servlet。
7.5 結束常規的Servlet生命周期。
7.6 容器判斷,如果是異步模式,則將異步事件放入到timeoutTasks這個鏈狀超時事件隊列中,如果沒有啟動異步模式,則提交結果,回收請求處理資源。
7.7.1 工作線程執行業務處理。
7.7.2 執行完畢業務處理以后調用Continuation的complete或者resume方法來提交處理結果。
7.7.3 complete或者resume方法調用將產生事件被放入到了線程池隊列中。
7.7.4 執行complete或者resume事件,調用事件宿主Endpoint的分發請求的處理。(可以理解為重新模擬執行了一次服務端的dispatch,也就是重新執行一次handler鏈,不過中間結果數據已經完全不同)
7.7.5 如果沒有complete則重新回到7.7.1
7.7.6 如果complete 則返回結果,回收資源。
7.8.1 Endpoint會循環檢查異步事件是否已經超時(查看timeoutTasks)。
7.8.2 如果出現超時,則封裝超時事件放入線程池隊列等待執行。
7.8.3 線程池執行超時處理,返回結果,回收資源。
Servlet3.0 與 Continuation的差異
可以說Jetty團隊在Servlet3.0沒有成為正式規范之前就參考了它的設計理念,因此本質上來說兩者沒有太大的區別,唯一的幾個區別點在于:
1. Continuation和AsynContext分別是兩個體系的異步上下文載體。
2. Continuation resume機制沒有Servlet3靈活,Servlet3可以支持dispatch到內部任意的Service上。
異步化在客戶端
前面一致介紹異步化在服務端的應用,其實在客戶端的應用可以提高客戶端的連接能力及容錯能力(加長Timeout時間也不會導致連接耗盡),Jetty已經支持客戶端異步化,使用比較簡單。
后話
這些是剛開始,接下來對于應用實際的改造(TOP現有管道化流程的異步化嘗試)會找到異步化的優勢和軟肋。如何用好異步化對于高并發的多模塊或者多依賴系統來說是很關鍵的,是一把雙刃劍,需要有足夠能力的人去把控,這個人需要的不僅是教條,更多的是經驗。