熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Web請求異步處理降低應用依賴風險

2022-06-13   來源: Java核心技術 

  問題凸現
   
    年關到了商家忙著促銷網站忙著推廣阿裡軟件的服務集成平台也面臨第一次多方大規模的壓力考驗根據該平台版本的壓力測試結果我們估算了一下現有的推廣會帶來的壓力基本上確定了服務集成平台年底不需要擴容SA(System Administrator系統管理員)為了保險起見還是通過請求方式來做定時的心跳檢測保證服務集成平台的可靠性結果阿裡旺旺推廣開始的第一天SA的報警短信就在幾個忙時段不停地發告警但是查看生產環境的服務器狀況以及應用狀況後看不出有什麼問題於是開始懷疑是否告警機制不是很合理幾日的訪問記錄統計報告看過以後發現了幾個問題首先由於推廣是在IM登錄時段集中式的推廣因此高峰期比較集中壓力也很大而告警發生的時刻也是那些時候另外發現那些推廣使用的API的處理時間比較長同時還有些出現了問題這幾天除了服務集成平台告警以外那些API服務器也在告警因此可以看出問題應該是由於API提供商響應速度慢而拖累了服務集成平台的處理能力監控機制在高峰情況下沒有得到及時的響應就認為是服務器已經處於無效狀態
   
    其實這類問題在我們現在的應用體系架構中常常出現原因是現在很少再有純粹封閉式應用對數據庫的依賴對存儲的依賴對第三方系統的依賴等等這也讓我回憶到在前一陣子參加的安全會議中騰迅的安全技術團隊的負責人說安全現在最大的問題就在於合作的第三方的安全不受控而引發的安全潛在影響Web應用未嘗不是從最基本的事務處理要小粒度不要在事務中包含第三方依賴到心跳檢測容錯方案的制定等都已經讓我們對這方面的問題有所注意但是往往這類問題不是局部設計可以看到的如果沒有一個總體架構設計者對於全局的把握協調和防范那麼問題出現並且帶來的影響將會很大
   
    從前對於服務集成平台的壓力測試主要是在ISP服務基本正常的情況下做的但是這次問題的暴露就要求我們在第三方依賴出現邊界問題時及時做出一些措施或者改進設計
   
    問題分析以及解決方案
   
    問題原因
   
    Http請求處理的阻塞方式 後端服務處理時間過長服務質量不穩定 Web Container接受請求線程資源有限
   
    解決方案
   
    改阻塞方式為非阻塞方式來處理請求 設置後端超時時間主動斷開連接回收資源 修改容器配置增加線程池大小以及等待隊列長度
   
    解決方案一是最難做到的後面的篇幅將描述對於這方面技術的探索
   
    解決方案二比較容易允許各個ISP設置自己API容許的最大超時時間
   
    解決方案三Tomcat和JBoss在Connector中有兩個參數配置(maxThreads和acceptCount)可以做調整
   
    第一個方案其實和JDK 支持的NIO是一種想法只是我們在Socket中都已經采用過了而在Http請求處理中因為要依賴於Web Container開發商的實現所以至今還沒有被廣泛應用不過在開源社區已經有用Mina實現的Http協議處理的框架需要注意的是現在Web應用對於Web請求高效處理的需求僅僅是很小的一方面其實還有很多類似於安全緩存監控等等附加功能也占據著很重要的地位
   
    Servlet 規范經過快一年的推廣已經被各大Web Container廠商所接受Tomcat JBoss Jetty 都宣稱自己對Servlet 作了較好的支持而在Servlet 中最廣為關注的一個特性就是異步服務處理Servlet(Async Servlet)這點也是解決我目前面臨問題的最好手段


   
    Servlet 與服務異步處理
   
    Servlet 主要的新特性分成四部分內嵌式的使用模式Annotation的支持Async Servlet的支持和安全提升內嵌式的使用很早就在Jetty中被實現也成為Jetty的優勢之一Annotation也只能說是錦上添花的部分而安全暫時沒有怎麼用到所以最關心的還是Async Servlet部分Async Servlet到底是什麼樣的概念這裡就大致描述一下在Servlet 規范中對它的介紹
   
    支持 Comet(彗星)最早期的Http請求就是無狀態的請求和響應所有的數據一次性在請求後返回給客戶端由客戶端渲染後來發展到AJAX頁面的請求和渲染由全局變成了局部而Comet適合事件驅動的Web應用和對交互性和實時性要求很強的應用通過建立客戶端和服務端的長連接通道在一次請求後可以主動推送服務端數據的變更情況到客戶端長連接建立的策略有兩種Http Streaming和Http Long Polling前者客戶端打開一個單一的與服務器端的 HTTP 持久連接服務器通過此連接把數據發送過來客戶端對它們進行增量處理後者由客戶端向服務器端發出請求並打開一個連接這個連接只有在收到服務器端的數據之後才會關閉服務器端發送完數據之後就立即關閉連接客戶端則馬上再打開一個新的連接等待下一次的數據 支持Suspending a request通過在ServletRequest中增加suspendresumecomplete等其將Http請求處理的block模式轉變成為not block模式同時支持對於狀態的查詢(suspendresumetimeout) 請求處理過程中支持事件機制響應也支持狀態查詢
   
    圖 異步服務請求基本流程
   
    現實中的異步服務處理 Tomcat 的異步服務處理
   
    這裡使用的是Tomcat 版本在Tomcat中對於異步處理描述在Advanced IO中作了說明主要分成兩部分Comet的支持和異步輸出
   
    Comet的支持作用分成兩部分請求讀數據的非阻塞響應處理的異步執行前者可以防止在大流量數據上傳過程中信道空閒等待的資源浪費後者用於在處理請求時依賴於第三方或者本身處理比較耗時的情況下懸掛起請求處理線程提高請求處理能力完成處理後異步輸出結果
   
    Servlet不再是原來對於幾個標准的Http請求類型的方法實現而是對於事件響應的處理Comet定義了個基礎的事件
   
    EventTypeBEGIN:客戶端建立起連接時激發的事件可以用於資源初始化 EventTypeREAD:有數據可以被讀入的事件(熟悉NIO的事件模式應該可以了解) EventTypeEND:請求處理結束時激發的事件可以用於資源清理 EventTypeERROR:當請求處理出現問題時激發的事件(IO異常超時等)
   
    還有一些子事件類型例如超時就屬於ERROR的子事件類型可以在事件處理中更加精確地定位事件類型
   
    必需的配置在serverxml中配置如下(紅色部分)
   
    <Connector port= protocol=yote
   
    connectionTimeout=
   
    redirectPort= />
   
    實際代碼范例如下
   
    //CometProcessor接口必需被實現一旦實現以後則該Servlet在配置好以後不會再調用servicegetpost等方法的實現
   
    public class SIPCometTomcatServlet extends HttpServlet implements CometProcessor
   
    {
   
    @Override
   
    //事件處理響應方法實現
   
    public void event(CometEvent event) throws IOException ServletException
   
    {
   
    if (eventgetEventType() == CometEventEventTypeBEGIN)
   
    {
   
    //設置事件超時時間
   
    eventsetTimeout( *
   
    //另起線程處理後台工作異步返回結果事件響應將不等待後台處理直接返回
   
    new Handler(eventgetHttpServletRequest()eventgetHttpServletResponse())start()
   
    }
   
    else if (eventgetEventType() == CometEventEventTypeERROR)
   
    {
   
    //結束事件回收requestresponse資源
   
    eventclose()
   
    }
   
    else if (eventgetEventType() == CometEventEventTypeEND)
   
    {
   
    eventclose()
   
    }
   
    }
   
    //另起一個線程異步處理請求
   
    class Handler extends javalangThread
   
    {
   
    private HttpServletResponse response;
   
    private HttpServletRequest request;
   
    public Handler(HttpServletRequest requestHttpServletResponse response)
   
    {
   
    thisresponse = response;
   
    thisrequest = request;
   
    }
   
    @Override
   
    public void run()
   
    {
   
    try
   
    {
   
    String id;
   
    id = requestgetParameter(id
   
    if (id == null)
   
    id = no id;
   
    Threadsleep(
   
    PrintWriter pw = responsegetWriter()
   
    pwwrite(id)
   
    pwflush()
   
    } catch (Exception e)
   
    {
   
    eprintStackTrace()
   
    }
   
    }
   
    }
   
    }
   
    使用過程中的一些總結
   
    事件響應框架將服務的請求由完整的一次服務處理切割成為細粒度的多事件處理為請求多階段並行處理提供了框架基礎 Event對象在事件處理方法結束後就被回收了但是request和response在事件處理完以後還可以繼續使用因此可以看出原來的阻塞式的方式已經可以通過事件的切分成為非阻塞的方式 沒有提供Servlet 中描述的suspendresumecomplete方法無法主動控制request的異步處理上面的代碼可以看出我只使用了Begin方法啟動了一個線程但是由於無法主動地結束請求因此在向客戶端返回數據以後還要等到超時才會結束這次會話(看了Tomcat的代碼也想模仿close的動作但是由於它使用了protected無法獲取封裝的request對象因此無法釋放資源)當然也可以通過客戶端配合由客戶端主動發起再次的數據傳輸激發READ事件來結束會話這麼做對客戶端的依賴比較強同時也增加了客戶端的處理復雜度 Tomcat支持異步輸出在APR或者NIO的模式下Tomcat支持在系統壓力增大的時候支持異步回寫大文件數據
   
    總體上來說實現了部分對於Comet的支持但是沒有對異步服務流程作很好的支持無法在開發中使用(簡單順暢的使用)
   
    JBoss的異步服務處理
   
    JBoss 版本配置和使用與Tomcat 類似沒有什麼差異
   
    JBoss 剛剛發布了RC版本對於異步服務處理作了很大的改動與Tomcat配置很不同這裡具體的說一下JBoss中的異步服務使用


   
    JBoss 已經將Tomcat中的HttpNioProtocol給刪除了取而代之的是JBoss自己servlet包內增加的一個HttpEventServlet接口這個接口和Tomcat的CometProcessor類似
   
    首先必須配置JBoss內置的Web容器為APR模式也就是配置jbosswebsar下面的serverxml中Connector 如下
   
    <Connector protocol=yote port= address=${jbossbindaddress}
   
    connectionTimeout= redirectPort= />
   
    其次異步服務處理的Servlet必須實現HttpEventServlet接口接口只有一個方法就是事件處理方法public void event(HttpEvent event)事件定義與Tomcat稍有不同在BEGINERRORREADEND基礎上增加了TIMEOUTEOFEVENTWRITE四個事件同時去掉了SubType
   
    TIMEOUT其實是從原來的Error的SubType分離出來的這個方法是在最後一次處理事件到當前時間超過設定的超時時間而被激發的同時TIMEOUT被激發並不會關閉請求處理流程必須顯示調用事件的close方法才會結束會話 EOF事件將會在客戶端主動斷連的情況下被觸發就好比IE窗口在請求過程中被關閉就會被觸發 EVENT事件在事件對象被調用resume的時候被激發按照原意應該最好可以附帶上一些自定義信息來做一些工作但是我自己使用過程中還沒有發現有什麼好的辦法可以在事件中附帶信息到事件處理中 WRITE方法在調用isWriteReady方法時被激發可以在網絡出現問題或者繁忙的時候異步等待輸出
   
    再則JBoss的事件對象還支持幾個方法來實現異步處理以及Comet機制方法如下
   
    close方法表示一次請求處理的結束會告知客戶端沒有數據返回了同時也會激發END事件 setTimeout方法設置連接超時時間(單位毫秒)計算超時是從最近的事件處理時間開始記錄的如果發生超時則會激發TIMEOUT事件 isReadReady方法如果連接有數據可以讀取則返回true如果這個方法返回falseservlet還試圖去讀去數據則會阻塞 isWriteReady方法如果返回true則連接可以無阻塞的寫出數據如果返回falseservlet必須停止寫數據如果強制寫出則可能會發生IO錯誤或者會采用異步輸出當客戶端的輸出通道可用以後則會激發write事件 suspend方法suspend連接處理線程直到timeout發生或者resume被調用實際上意味著servlet在suspend以後不再收到READ事件READ事件將會在後台被不斷的激發除非被suspend resume方法會激發event事件可以利用這個方法來結束異步處理同時也可以激活因為suspend停止的read事件同時也可以在resume以後再調用suspend方法注意這裡未必是要求必須先suspend以後再resume eventrequestresponse在事件響應過程中都可以被使用但是線程不安全同時在調用了close以後requestresponse資源會被釋放可以通過對event對象做同步來保證線程安全的問題當READ事件和END事件都發生的時候首先會完成READ事件然後再去完成END
   
    具體的實現代碼如下
   
    public class SIPCometJBossServlet extends HttpServlet implements HttpEventServlet
   
    {
   
    @Override
   
    public void event(HttpEvent event) throws IOException ServletException
   
    {
   
    switch (eventgetType())
   
    {
   
    //will be called at the beginning of the processing of the connection
   
    case BEGIN:
   
    {
   
    eventsetTimeout( * //設置超時時間
   
    //eventsuspend()//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:
   
    {
   
    eventclose()
   
    break;
   
    }
   
    //End may be called to end the processing of the request
   
    case END:
   
    {
   
    //eventclose()//可以寫也可以不寫因為進入這個方法也就是調用了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:
   
    {
   
    eventclose()//如果不主動關閉Timeout方法會被循環調用會話不會結束
   
    break;
   
    }
   
    //The end of file of the input has been reached and no further data is available
   
    case EOF:
   
    {
   
    eventclose()
   
    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:
   
    {
   
    eventclose()//作為resume方法調用後主動釋放連接資源的一種手段
   
    break;
   
    }
   
    //Write is sent if the servlet is using the isWriteReady method
   
    case WRITE:
   
    {
   
    break;
   
    }
   
    }
   
    }
   
    class Handler extends javalangThread
   
    {
   
    private HttpEvent event;//event的生命周期已經不限制於事件處理方法因此隨時可以關閉請求處理
   
    private HttpServletResponse response;
   
    private HttpServletRequest request;
   
    public Handler(HttpEvent event)
   
    {
   
    thisevent = event;
   
    thisresponse = eventgetHttpServletResponse()
   
    thisrequest = eventgetHttpServletRequest()
   
    }
   
    @Override
   
    public void run()
   
    {
   
    try
   
    {
   
    String id;
   
    id = requestgetParameter(id
   
    if (id == null)
   
    id = no id;
   
    Threadsleep(
   
    //危險!!!其實eventresponserequest都是線程不安全的因此此時可能response已經被釋放需要同步住event的對象來操作效率可能會降低
   
    PrintWriter pw = responsegetWriter()
   
    pwwrite(id)
   
    pwflush()
   
    eventresume()//發送結束調用resume方法進入event方法結束請求處理
   
    } catch (Exception e)
   
    {
   
    eprintStackTrace()
   
    }
   
    }
   
    }
   
    }
   
    使用總結
   
    對於Servlet描述的異步服務處理有了較好的支持 事件方法比較豐富但是對於可定義事件支持不夠完善 對象並發控制需要開發者自己設計權衡多線程處理的高效以及資源爭奪的消耗
   
    下面對異步服務處理Servlet和普通Servlet做了一下簡單的性能測試
   
    首先我原本想用ab來做一下簡單的壓力測試即可但是ab好像對於apr模式下的測試支持的不好一壓就報錯(apr_poll: The timeout specified has expired ())也可能是自己不會用吧因此就自己寫了一段測試代碼來做測試
   
    測試場景如下
   
    兩類Servlet都可以設置處理時Hold的時間來達到消耗連接數的目的測試客戶端可以設置並發多少用戶每個用戶發起多少次請求下表就是測試的結果
   
    這裡設置的是Servlet都hold秒鐘APR啟動時配置的最大連接數為默認的
   
    客戶端設置 普通Servlet總耗時(ms) 異步Servlet總耗時(ms) 普通Servlet單個線程耗時(ms) 異步Servlet單個線程耗時(ms)
   
    並發線程每個線程執行次請求
   
    並發線程每個線程執行次請求
   
    並發線程每個線程執行次請求
   
    並發線程每個線程執行次請求 retrying requestconnect reject retrying requestconnect reject
   
    從上表可以看出就純粹從處理效率來說采用事件處理方式在線程切換過程中存在著一定的損失但是就我們使用異步請求處理的本意來看對於在高並發下對後端依賴無法避免的性能損耗情況下異步請求解決了連接耗盡的問題
   
    最後再來看我在測試過程中用JProfiler來截取的一些線程創建和使用狀況
   
    上圖是最初的線程創建情況還沒有任何請求被發送到服務端因此線程池也沒有開任何一個連接
   
    這是普通的Servlet在壓力測試下的線程狀況線程就開到了最大值圖中由於程序來Hold請求處理線程出現了紅色阻塞和黃色等待同時客戶端已經開始出現拒絕連接的錯誤下圖就是錯誤的截圖
   
    上圖是異步服務處理Servlet在壓力測試開始的情況可以發現它的http線程還是但是其他事件處理線程在不斷增長下圖已經增長到了多個線程(這裡需要注意的就是這種異步處理資源申請沒有設置上限因此對於資源消耗來說也是比較大的同時要防范攻擊性請求造成服務端垮掉)
   
    結語
   
    多線程分布式計算Erlang等這些編程方式框架設計語言其實都在實現這一個理論那就是分而治之多線程是站在單應用的角度去考慮解決方案分布式計算是在多機協作考慮解決方案Erlang在單機多處理器的角度去考慮解決方案但彼此的理念都是一樣將能夠分割的不相關聯的獨立任務並行處理最終實現最優化的處理效果
   
    對於服務集成平台是否采用這種技術我自己還沒有最終的決定首先就如上面的測試結果來看有得還是有失的其次這種並發異步處理帶來的多線程維護控制復雜度也需要考慮到成本中Jetty的開發者對於是否將異步服務處理Servlet來交由開發者控制而不是容器本身來控制表示出了反對意見的確這樣復雜的控制交給開發者來處理會增加開發者的學習成本以及維護成本


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26410.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.