作者 岑文初 發(fā)布于 2009年1月9日 上午8時16分
年關(guān)到了,商家忙著促銷,網(wǎng)站忙著推廣,阿里軟件的服務(wù)集成平臺也面臨第一次多方大規(guī)模的壓力考驗(yàn)。根據(jù)該平臺5.3版本的壓力測試結(jié)果,我們估算了一下現(xiàn)有的推廣會帶來的壓力,基本上確定了服務(wù)集成平臺年底不需要擴(kuò)容。SA(System Administrator,系統(tǒng)管理員)為了保險起見還是通過請求方式來做定時的心跳檢測,保證服務(wù)集成平臺的可靠性。結(jié)果阿里旺旺推廣開始的第一天,SA的報警短信就在幾個忙時段不停地發(fā)告警,但是查看生產(chǎn)環(huán)境的服務(wù)器狀況以及應(yīng)用狀況后看不出有什么問題,于是開始懷疑是否告警機(jī)制不是很合理。幾日的訪問記錄統(tǒng)計報告看過以后,發(fā)現(xiàn)了幾個問題,首先由于推廣是在IM登錄時段集中式的推廣,因此高峰期比較集中,壓力也很大,而告警發(fā)生的時刻也是那些時候;另外發(fā)現(xiàn)那些推廣使用的API的處理時間比較長,同時還有些出現(xiàn)了問題,這幾天除了服務(wù)集成平臺告警以外,那些API服務(wù)器也在告警;因此可以看出問題應(yīng)該是由于API提供商響應(yīng)速度慢而拖累了服務(wù)集成平臺的處理能力,監(jiān)控機(jī)制在高峰情況下沒有得到及時的響應(yīng),就認(rèn)為是服務(wù)器已經(jīng)處于無效狀態(tài)。
從前對于服務(wù)集成平臺的壓力測試主要是在ISP服務(wù)“基本正常”的情況下做的,但是這次問題的暴露就要求我們在第三方依賴出現(xiàn)邊界問題時,及時做出一些措施或者改進(jìn)設(shè)計。
問題原因:
解決方案:
解決方案一是最難做到的,后面的篇幅將描述對于這方面技術(shù)的探索。
解決方案二比較容易,允許各個ISP設(shè)置自己API容許的最大超時時間。
解決方案三Tomcat和JBoss在Connector中有兩個參數(shù)配置(maxThreads和acceptCount)可以做調(diào)整。
第一個方案其實(shí)和JDK 1.5支持的NIO是一種想法,只是我們在Socket中都已經(jīng)采用過了,而在Http請求處理中因?yàn)橐蕾囉赪eb Container開發(fā)商的實(shí)現(xiàn),所以至今還沒有被廣泛應(yīng)用,不過在開源社區(qū)已經(jīng)有用Mina實(shí)現(xiàn)的Http協(xié)議處理的框架。需要注意的是,現(xiàn)在Web應(yīng)用對于Web請求高效處理的需求僅僅是很小的一方面,其實(shí)還有很多類似于安全、緩存、監(jiān)控等等附加功能也占據(jù)著很重要的地位。
Servlet 3規(guī)范經(jīng)過快一年的推廣,已經(jīng)被各大Web Container廠商所接受,Tomcat 6、JBoss 5、Jetty 7都宣稱自己對Servlet 3作了較好的支持,而在Servlet 3中最廣為關(guān)注的一個特性就是異步服務(wù)處理Servlet(Async Servlet),這點(diǎn)也是解決我目前面臨問題的最好手段。
Servlet 3主要的新特性分成四部分:內(nèi)嵌式的使用模式、Annotation的支持、Async Servlet的支持和安全提升。內(nèi)嵌式的使用很早就在Jetty中被實(shí)現(xiàn),也成為Jetty的優(yōu)勢之一,Annotation也只能說是錦上添花的部分,而安全暫時沒有怎么用到,所以最關(guān)心的還是Async Servlet部分。Async Servlet到底是什么樣的概念,這里就大致描述一下在Servlet 3規(guī)范中對它的介紹:
圖 異步服務(wù)請求基本流程
這里使用的是Tomcat 6.0.14版本。在Tomcat中對于異步處理描述在Advanced IO中作了說明,主要分成兩部分:Comet的支持和異步輸出。
Comet的支持作用分成兩部分:請求讀數(shù)據(jù)的非阻塞,響應(yīng)處理的異步執(zhí)行。前者可以防止在大流量數(shù)據(jù)上傳過程中,信道空閑等待的資源浪費(fèi),后者用于在處理請求時,依賴于第三方或者本身處理比較耗時的情況下,懸掛起請求處理線程,提高請求處理能力,完成處理后異步輸出結(jié)果。
Servlet不再是原來對于幾個標(biāo)準(zhǔn)的Http請求類型的方法實(shí)現(xiàn),而是對于事件響應(yīng)的處理。Comet定義了4個基礎(chǔ)的事件:
還有一些子事件類型,例如超時就屬于ERROR的子事件類型,可以在事件處理中更加精確地定位事件類型。
必需的配置:在server.xml中配置如下(紅色部分):
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000"redirectPort="8443" />
實(shí)際代碼范例如下:
//CometProcessor接口必需被實(shí)現(xiàn),一旦實(shí)現(xiàn)以后,則該Servlet在配置好以后不會再調(diào)用service,get,post等方法的實(shí)現(xiàn)。public class SIPCometTomcatServlet extends HttpServlet implements CometProcessor{ @Override //事件處理響應(yīng)方法實(shí)現(xiàn) public void event(CometEvent event) throws IOException, ServletException { if (event.getEventType() == CometEvent.EventType.BEGIN) { //設(shè)置事件超時時間 event.setTimeout(10 * 1000); //另起線程處理后臺工作,異步返回結(jié)果,事件響應(yīng)將不等待后臺處理直接返回 new Handler(event.getHttpServletRequest(),event.getHttpServletResponse()).start(); } else if (event.getEventType() == CometEvent.EventType.ERROR) { //結(jié)束事件,回收request,response資源 event.close(); } else if (event.getEventType() == CometEvent.EventType.END) { event.close(); } } //另起一個線程異步處理請求。 class Handler extends java.lang.Thread { private HttpServletResponse response; private HttpServletRequest request; public Handler(HttpServletRequest request,HttpServletResponse response) { this.response = response; this.request = request; } @Override public void run() { try { String id; id = request.getParameter("id"); if (id == null) id = "no id"; Thread.sleep(5000); PrintWriter pw = response.getWriter(); pw.write(id); pw.flush(); } catch (Exception e) { e.printStackTrace(); } } }}
使用過程中的一些總結(jié):
總體上來說實(shí)現(xiàn)了部分對于Comet的支持,但是沒有對異步服務(wù)流程作很好的支持,無法在開發(fā)中使用(簡單順暢的使用)。
JBoss 4.2.3版本配置和使用與Tomcat 6類似,沒有什么差異。
JBoss 5剛剛發(fā)布了RC版本,對于異步服務(wù)處理作了很大的改動,與Tomcat配置很不同,這里具體的說一下JBoss5中的異步服務(wù)使用。
JBoss 5已經(jīng)將Tomcat中的Http11NioProtocol給刪除了,取而代之的是JBoss自己servlet包內(nèi)增加的一個HttpEventServlet接口,這個接口和Tomcat的CometProcessor類似。
首先,必須配置JBoss內(nèi)置的Web容器為APR模式,也就是配置jbossweb.sar下面的server.xml中Connector 如下:
<Connector protocol="org.apache.coyote.http11.Http11AprProtocol" port="8080" address="${jboss.bind.address}" connectionTimeout="2000" redirectPort="8443" />
其次異步服務(wù)處理的Servlet必須實(shí)現(xiàn)HttpEventServlet接口,接口只有一個方法,就是事件處理方法:public void event(HttpEvent event)
。事件定義與Tomcat稍有不同,在BEGIN,ERROR,READ,END基礎(chǔ)上增加了TIMEOUT,EOF,EVENT,WRITE四個事件,同時去掉了SubType。
再則,JBoss的事件對象還支持幾個方法來實(shí)現(xiàn)異步處理以及Comet機(jī)制,方法如下:
具體的實(shí)現(xiàn)代碼如下:
public class SIPCometJBossServlet extends HttpServlet implements HttpEventServlet{ @Override public void event(HttpEvent event) throws IOException, ServletException { switch (event.getType()) { //will be called at the beginning of the processing of the connection case BEGIN: { event.setTimeout(100 * 1000);//設(shè)置超時時間 //event.suspend();//resume之前不必要一定使用suspend new Handler(event).start(); break; } //Error will be called by the container in the case //where an IO exception or a similar unrecoverable error occurs case ERROR: { event.close(); break; } //End may be called to end the processing of the request case END: { //event.close();//可以寫也可以不寫,因?yàn)檫M(jìn)入這個方法也就是調(diào)用了close方法,起碼暫時還不知道有其他什么入口 break; } //This indicates that input data is available, //and that at least one read call can be made without blocking case READ: { break; } //The connection timed out according to the timeout value which has been set //,but the connection will not be closed unless the servlet uses the close method of the event case TIMEOUT: { event.close();//如果不主動關(guān)閉,Timeout方法會被循環(huán)調(diào)用,會話不會結(jié)束 break; } //The end of file of the input has been reached, and no further data is available case EOF: { event.close(); break; } //Event will be called by the container after the resume() method is called, //during which any operation can be performed, including closing the connection using the close() method. case EVENT: { event.close();//作為resume方法調(diào)用后主動釋放連接資源的一種手段 break; } //Write is sent if the servlet is using the isWriteReady method case WRITE: { break; } } } class Handler extends java.lang.Thread { private HttpEvent event;//event的生命周期已經(jīng)不限制于事件處理方法,因此隨時可以關(guān)閉請求處理 private HttpServletResponse response; private HttpServletRequest request; public Handler(HttpEvent event) { this.event = event; this.response = event.getHttpServletResponse(); this.request = event.getHttpServletRequest(); } @Override public void run() { try { String id; id = request.getParameter("id"); if (id == null) id = "no id"; Thread.sleep(5000);//危險?。?!其實(shí)event,response,request都是線程不安全的,因此此時可能response已經(jīng)被釋放,需要同步住event的對象來操作,效率可能會降低 PrintWriter pw = response.getWriter(); pw.write(id); pw.flush(); event.resume();//發(fā)送結(jié)束調(diào)用resume方法,進(jìn)入event方法,結(jié)束請求處理 } catch (Exception e) { e.printStackTrace(); } } }}
使用總結(jié):
下面對異步服務(wù)處理Servlet和普通Servlet做了一下簡單的性能測試。
首先我原本想用ab來做一下簡單的壓力測試即可,但是ab好像對于apr模式下的測試支持的不好,一壓就報錯(apr_poll: The timeout specified has expired (70007)),也可能是自己不會用吧,因此就自己寫了一段測試代碼來做測試。
測試場景如下:
兩類Servlet都可以設(shè)置處理時Hold的時間,來達(dá)到消耗連接數(shù)的目的。測試客戶端可以設(shè)置并發(fā)多少用戶,每個用戶發(fā)起多少次請求。下表就是測試的結(jié)果:
這里設(shè)置的是Servlet都hold1秒鐘,APR啟動時配置的最大連接數(shù)為默認(rèn)的200個。
客戶端設(shè)置 | 普通Servlet總耗時(ms) | 異步Servlet總耗時(ms) | 普通Servlet單個線程耗時(ms) | 異步Servlet單個線程耗時(ms) |
100并發(fā)線程,每個線程執(zhí)行1次請求 | 263866 | 274430 | 2638 | 2744 |
300并發(fā)線程,每個線程執(zhí)行1次請求 | 550718 | 617082 | 1835 | 2056 |
100并發(fā)線程,每個線程執(zhí)行10次請求 | 1087747 | 1207920 | 10877 | 12079 |
300并發(fā)線程,每個線程執(zhí)行10次請求 | retrying request,connect reject | 5193644 | retrying request,connect reject | 17312 |
從上表可以看出,就純粹從處理效率來說,采用事件處理方式在線程切換過程中存在著一定的損失,但是就我們使用異步請求處理的本意來看,對于在高并發(fā)下對后端依賴無法避免的性能損耗情況下,異步請求解決了連接耗盡的問題。
最后再來看我在測試過程中用JProfiler來截取的一些線程創(chuàng)建和使用狀況:
上圖是最初的線程創(chuàng)建情況,還沒有任何請求被發(fā)送到服務(wù)端,因此線程池也沒有開任何一個連接。
這是普通的Servlet在壓力測試下的線程狀況,線程就開到了200最大值,圖中由于程序來Hold請求處理線程出現(xiàn)了紅色阻塞和黃色等待,同時客戶端已經(jīng)開始出現(xiàn)拒絕連接的錯誤。下圖就是錯誤的截圖:
上圖是異步服務(wù)處理Servlet在壓力測試開始的情況,可以發(fā)現(xiàn)它的http線程還是200,但是其他事件處理線程在不斷增長。下圖已經(jīng)增長到了3000多個線程。(這里需要注意的就是這種異步處理資源申請沒有設(shè)置上限,因此對于資源消耗來說也是比較大的,同時要防范攻擊性請求造成服務(wù)端垮掉)。
多線程、分布式計算、Erlang等這些編程方式、框架設(shè)計、語言其實(shí)都在實(shí)現(xiàn)這一個理論,那就是分而治之,多線程是站在單應(yīng)用的角度去考慮解決方案,分布式計算是在多機(jī)協(xié)作考慮解決方案,Erlang在單機(jī)多處理器的角度去考慮解決方案。但彼此的理念都是一樣,將能夠分割的不相關(guān)聯(lián)的獨(dú)立任務(wù)并行處理,最終實(shí)現(xiàn)最優(yōu)化的處理效果。
對于服務(wù)集成平臺是否采用這種技術(shù),我自己還沒有最終的決定,首先就如上面的測試結(jié)果來看,有得還是有失的,其次這種并發(fā)異步處理帶來的多線程維護(hù)控制復(fù)雜度,也需要考慮到成本中。Jetty的開發(fā)者對于是否將異步服務(wù)處理Servlet來交由開發(fā)者控制而不是容器本身來控制表示出了反對意見,的確,這樣復(fù)雜的控制交給開發(fā)者來處理會增加開發(fā)者的學(xué)習(xí)成本以及維護(hù)成本。
作者介紹:岑文初,就職于阿里軟件公司研發(fā)中心平臺一部,任架構(gòu)師。當(dāng)前主要工作涉及阿里軟件開發(fā)平臺服務(wù)框架(ASF)設(shè)計與實(shí)現(xiàn),服務(wù)集成平臺(SIP)設(shè)計與實(shí)現(xiàn)。沒有什么擅長或者精通,工作到現(xiàn)在唯一提升的就是學(xué)習(xí)能力和速度。個人Blog為:http://blog.csdn.net/cenwenchu79。