備注:翻譯自theserverside.com的一篇文章,原文地址請見http://www.theserverside.com/tt/articles/article.tss?l=IOandSEDAModel。英文能力一般,翻譯質量不是特別理想,大家將就點看吧。如有錯誤請幫忙指正。
正文如下:
討論
這篇文章展示一個解決方案,用來解決企業應用中的可伸縮性問題,這些應用必須支持即要求快速響應而又長時間運行的業務程序,吞吐量或大或小。
讓我們定義一個簡單的示例場景來模擬這種情況。我們有一個前端web應用程序,通過http接收請求,然后把請求發送給不同的web service后端。web service請求的后端平臺中有一個響應很慢。結果導致我們將獲得一個很低的吞吐量.因為這個響應很慢的服務使得web服務器的線程池中的一個工作線程始終保持繁忙,其他請求無法得到處理。
這里有一個解決這種情況的方案,雖然現在還沒有標準化,但已經被幾乎所有servlet容器以這樣或者那樣的方法實現:Jetty, Apache Jakarta Tomcat, and Weblogic. 這就是異步IO(asynchronous IO,or AIO).
上面提到的解決方案中使用到的關鍵架構組件如下:
1. 在servlet容器中使用異步IO
2. 階段化事件驅動架構模型(SEDA)
在servlet容器中使用異步IO
servlet容器正成為在java nio庫之上實現高可伸縮性應用的良好機會——nio類庫給予從每連接一線程轉變為每請求一線程的能力。
當然這些還不夠,在實現Reverse Ajax-based的應用時會發生問題。目前沒有機制提供servlet API來容許異步發送數據給客戶端。目前Reverse Ajax有三種實現方式:
* polling
* piggy back
* Comet
當前基于Commet的實現是保持和客戶端的一個打開的通道,基于某些事件發回數據。但是這打破了每請求一線程模型,在服務器端至少需要分派一個工作線程。
在servlet容器中目前有兩種實現方式:
1.異步IO(Apache Tomcat, Bea Weblogic)——容許servlet異步處理數據
2.continuations (延續?)(Jetty)——在Jetty6介紹的非常有趣的特性,容許掛起當前請求并釋放當前線程。
所有這些實現都有優點和缺點,而最好的實現將是所有這些實現的組合。
我的例子基于Apache Jakarta Tomcat的實現,稱為CometProcessor。這種實現將請求和應答從工作線程中解耦,從而容許工作線程稍后再完成應答。
Staged event-driven architecture (SEDA) model
SEDA模型是伯克利大學的Matt Welsh, David Culler和Eric Brewer推薦的一個架構設計。SEDA將應用分解為由動態資源控制器分離的不同階段,從而容許應用程序動態調整來改變負載。
下面你將看到基于SEDA的HTTP服務器:
圖片2: SEDA HTTP服務器: 基于SEDA的HTTP服務器的架構表述。應用由被隊列分離的多個階段的集合組成。箭頭表述了階段之間的事件流程。每個階段可以被獨立管理,并且階段可以按順序依次運行或并發運行,或者是兩者的組合。時間隊列的使用容許每個階段分別load-conditioned(負載調節?).例如,設置事件隊列的閥值。
有關這個架構的更多內容可以在這個頁面找到:SEDA: An Architecture for Well-Conditioned, Scalable Internet Services.
讓我們一起來看,我們的簡化場景是如何映射到這個SEDA架構的。
基于SEDA的應用將由七個階段組成。當一個特定類型的請求到達時,它將被路由到正確的隊列中。對應的階段將處理這個消息,然后將應答放到應答隊列中。最后數據將被發送給客戶端。通過這種方法我們可以解決當請求被路由到應答緩慢的服務時阻塞其他請求處理而帶來的擴展性問題。
讓我們一起來看看怎么用Mule來實現這種架構。
Mule是一種開源Enterprise Message Bus (ESB),它的模型概念是基于SEDA模型。Mule也支持其他信息模型,但默認是SEDA模型。在這種模式下,Mule將每個組件當成一個階段,使用自己的線程和工作隊列。
在SEDA模型中的關鍵組件——Incoming Event Queue(輸入事件隊列), Admission Controller(許可控制器), Dynamically sized Thread Pool(動態線程池), Event Handler(事件處理器)和Resource Controller(資源控制器)——被映射到Mule的服務組件。
在Mule中,Incoming Event Queue(輸入事件隊列)是作為一個inbound(內部?)的路由器或者終端提供,而Event Handler(事件處理器)自身就是作為一個組件。Thus we're short of an Admission Controller, Resource Controller and a Dynamically sized Thread Pool. (be short of ?怎么翻譯,sorry)
Admission Controller(許可控制器)作為SEDA階段和Incoming Event Queue(輸入事件隊列)連接,用Mule的術語說是組件。實現這種方式的最直接的方法是作為一個Inbound路由器,用于控制被注冊到通道上的組件接受的事件,哪些該被處理和該如何處理。
我們場景的邏輯流程,將在下面的圖中展示如何被映射到Mule模型。圖中列舉的步驟如下:
1. 客戶端通過http請求下一個訂單
2. 請求被http服務器處理,在我們的案例中是Apache Jakarta Tomcat。基于http請求提供的參數,前端應用程序組合一個請求對象。在我們的場景中,我們有兩個對象類型,PriceOrderRequest和StockOrderRequest。每個請求會自動生成一個關聯id,并被映射到關聯這個請求的應答對象中。我們將在稍后看到這個關聯id將被如何用于匹配從Mule容器到原始客戶端請求的應答。從現在開始,請求對象將包含這個關聯id,并將在前端應用程序的所有層之間傳遞,當然也會穿透Mule的組件。這個請求訂單,不管是PriceOrderRequest還是StockOrderRequest,將被發送到access層。在access層將有一個準備好的JMS生產者用于將這個信息加入到請求隊列。現在請求訂單將被Mule組件處理。被web服務器分配用來服務于我們http請求的工作線程現在被釋放可以用于服務其他請求,它不需要等待我們的業務處理結束。
3. 我們的請求訂單現在在jms的隊列中,地址是jms://requestQueue。現在處理被轉移到Mule中。
4. 基于對象類型,訂單將被路由到不同的隊列。在我們的案例中,我們有一個PriceOrderRequest,所以信息被路由到jms://priceOrderQueue。
5. 通過使用Apache CXF,一個SOAP請求被生成并發送到web service容器。應答將被發送到jms://responseQueue.
6. 同樣的類似步驟4的場景發生在StockOrderRequest的案例中。
7. 類似步驟5.
8. JMS的消費者池監聽the jms://responseQueue. 這個隊列包含業務請求的應答信息。這個消息包含在步驟2中生成的關聯id元數據,這將容許我們識別請求的發起者。
9. 一旦http應答對象被識別,我們可以發送應答給客戶端。
上面流程的Mule配置信息展示如下:
<jms:activemq-connector name="jmsConnector" brokerURL="tcp://localhost:61616"/>
<model name="Sample">
<service name="Order Service" >
<inbound>
<jms:inbound-endpoint queue="requestQueue"/>
</inbound>
<component class="org.mule.example.Logger"/>
<outbound>
<filtering-router>
<jms:outbound-endpoint queue="priceOrderQueue" />
<payload-type-filter expectedType="org.mule.model.PriceOrderRequest"/>
</filtering-router>
<filtering-router>
<jms:outbound-endpoint queue="stockOrderQueue" />
<payload-type-filter expectedType="org.mule.model.StockOrderRequest" />
</filtering-router>
</outbound>
</service>
<service name="stockService">
<inbound>
<jms:inbound-endpoint queue="stockOrderQueue" transformer-refs="JMSToObject
StockOrderRequestToServiceRequest" />
</inbound>
<outbound>
<chaining-router>
<cxf:outbound-endpoint
address="http://localhost:8080/axis2/services/getStock"
clientClass="org.axis2.service.stock.GetStock_Service"
wsdlPort="getStockHttpSoap12Endpoint"
wsdlLocation="classpath:/Stock.wsdl"
operation="getStock" />
<jms:outbound-endpoint queue="responseQueue"
transformer-refs="ServiceResponseToStockOrderResponse ObjectToJMS"/>
</chaining-router>
</outbound>
<default-service-exception-strategy>
<jms:outbound-endpoint queue="responseQueue"
transformer-refs="ExceptionToResponse ObjectToJMS"/>
</default-service-exception-strategy>
</service>
<service name="priceService">
<inbound>
<jms:inbound-endpoint queue="priceOrderQueue"
transformer-refs="JMSToObject PriceOrderRequestToServiceRequest"/>
</inbound>
<outbound>
<chaining-router>
<cxf:outbound-endpoint
address="http://localhost:8080/axis2/services/getPrice"
clientClass="org.axis2.service.price.GetPrice_Service"
wsdlPort="getPriceHttpSoap12Endpoint"
wsdlLocation="classpath:/Price.wsdl"
operation="getPrice" />
<jms:outbound-endpoint queue="responseQueue"
transformer-refs="ServiceResponseToPriceOrderResponse ObjectToJMS"/>
</chaining-router>
</outbound>
<default-service-exception-strategy>
這個事件驅動的架構模型有一個挑戰性的問題,如何將應答和請求關聯?請求被生成,業務對象被創建,并被作為jsm對象信息的負載在Mule空間中通過多個jms隊列傳輸。這個信息被從一個隊列路由到另一個,通常被用來作為到web service請求的輸入。
容許我們持續追蹤信息的關鍵信息是來自jms規范的關聯id。可以通過使用message.setJMSCorrelationID()來設置。然而如果你在jms隊列中發布設置了這個屬性的信息,Mule似乎會覆蓋這個信息并為消息創建一個將貫穿整個流程的新的關聯id。幸好還有一個內部的名為MULE_CORRELATION_ID的Mule消息屬性。如果Mule發現消息的這個屬性被設置,它將被用于穿越流程中所有的組件,另外如果關聯id沒有被設置,MULE_CORRELATION_ID屬性的值還將被作為關聯id的值使用。
/* set the MULE_CORRELATION_ID property before sending the message to the queue*/
conn=getConnection();
session=conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
producer= session.createProducer(getDestination(Constants.JMS_DESTINATION_REQUEST_QUEUE));
jmsMessage=session.createObjectMessage();
jmsMessage.setObject(request);
jmsMessage.setStringProperty(Constants.PROPS_MULE_CORRELATION_ID, request.getCorrelationID());
producer.send(jmsMessage);
所以每個請求必須在對應的業務對象被發送到Mule入口(一個jms對象)前生成一個唯一的關聯id。
一個可行的方法是生成一個UUID用做關聯id,同樣將UUID映射到CometProcessor接口中的事件方法提供的被包裹為CometEvent對象的HttpServletResponse對象。
/*
* generate the UUID for the CORRELATION ID and map to the HttpServletResponse
*/
public class IdentityCreator extends MethodInterceptorAspect{
protected void beforeInvoke(MethodInvocation method){
Object[] args=method.getArguments();
HttpServletRequest httpRequest=((CometEvent)args[0]).getHttpServletRequest();
String uuid=UuidFactory.getUuid();
httpRequest.setAttribute(Constants.PROPS_MULE_CORRELATION_ID, uuid);
HttpResponseManager.getInstance().saveResponse(uuid, ((CometEvent)args[0]).getHttpServletResponse());
}
protected void afterInvoke(MethodInvocation method){
return;
}
@Override
public void afterExceptionInvoke(MethodInvocation method) throws Throwable {
Object[] args=method.getArguments();
HttpServletRequest httpRequest=((CometEvent)args[0]).getHttpServletRequest();
String uuid=(String)httpRequest.getAttribute(Constants.PROPS_MULE_CORRELATION_ID);
if (uuid!=null) HttpResponseManager.getInstance().removeResponse(uuid);
}
}
當應答消息返回時,我們所需要做的只是從jms消息屬性中獲取關聯對象的值,查找對象的HttpServletResponse對象,然后發送應答給客戶端。
測試
一些測試可以提供我們這個架構優點的清晰見解。使用Apache JMeter,每個案例都執行一個測試,一個架構使用異步servlet和SEDA模型,另一個架構不使用這個模型。測試運行了1個小時,每秒10個線程,兩種類型的請求交互使用。為了這些測試,我們分配了總共6個工作線程。在沒有擴展性提升的案例中,所有6個線程都被Tomcat的線程池占用。
可以非常清楚的看到,吞吐量(綠線)是如何下降到大概 23 請求每分鐘的。
現在讓我們在我們的組件中分配這6個線程。每個組件分配一個單一線程。
在Jakarta Tomcat中,server.xml配置文件中的下面這些行需要修改:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="1" minSpareThreads="0"/>
在Mule的案例中,需要在Mule配置文件中為每個服務組件在service標簽中增加以下行:
<component-threading-profile
maxThreadsActive="1" maxThreadsIdle="0" poolExhaustedAction="RUN"
maxBufferSize="20" threadWaitTimeout="300"/>
異步和SEDA模型架構的測試在下面可以看到。吞吐量在23請求每分鐘保持不變。
如果我們運行性能測試超過1小時,第一個案例的吞吐量還將繼續下降,但是第二個案例依然將保持同樣的值。