問題凸現
年關到了商家忙著促銷網站忙著推廣阿裡軟件的服務集成平台也面臨第一次多方大規模的壓力考驗根據該平台版本的壓力測試結果我們估算了一下現有的推廣會帶來的壓力基本上確定了服務集成平台年底不需要擴容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中增加suspend
resume
complete等
其將Http請求處理的block模式轉變成為not block模式
同時支持對於狀態的查詢(suspend
resume
timeout)
請求處理過程中支持事件機制
響應也支持狀態查詢
圖 異步服務請求基本流程
現實中的異步服務處理 Tomcat 的異步服務處理
這裡使用的是Tomcat
版本
在Tomcat中對於異步處理描述在Advanced IO中作了說明
主要分成兩部分
Comet的支持和異步輸出
Comet的支持作用分成兩部分
請求讀數據的非阻塞
響應處理的異步執行
前者可以防止在大流量數據上傳過程中
信道空閒等待的資源浪費
後者用於在處理請求時
依賴於第三方或者本身處理比較耗時的情況下
懸掛起請求處理線程
提高請求處理能力
完成處理後異步輸出結果
Servlet不再是原來對於幾個標准的Http請求類型的方法實現
而是對於事件響應的處理
Comet定義了
個基礎的事件
EventType
BEGIN:客戶端建立起連接時激發的事件
可以用於資源初始化
EventType
READ:有數據可以被讀入的事件
(熟悉NIO的事件模式應該可以了解) EventType
END:請求處理結束時激發的事件
可以用於資源清理
EventType
ERROR:當請求處理出現問題時激發的事件
(IO異常
超時等)
還有一些子事件類型
例如超時就屬於ERROR的子事件類型
可以在事件處理中更加精確地定位事件類型
必需的配置
在server
xml中配置如下(紅色部分)
<Connector port=
protocol=
yote
connectionTimeout=
redirectPort=
/>
實際代碼范例如下
//CometProcessor接口必需被實現
一旦實現以後
則該Servlet在配置好以後不會再調用service
get
post等方法的實現
public class SIPCometTomcatServlet extends HttpServlet implements CometProcessor
{
@Override
//事件處理響應方法實現
public void event(CometEvent event) throws IOException
ServletException
{
if (event
getEventType() == CometEvent
EventType
BEGIN)
{
//設置事件超時時間
event
setTimeout(
*
)
//另起線程處理後台工作
異步返回結果
事件響應將不等待後台處理直接返回
new Handler(event
getHttpServletRequest()
event
getHttpServletResponse())
start()
}
else if (event
getEventType() == CometEvent
EventType
ERROR)
{
//結束事件
回收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(
)
PrintWriter pw = response
getWriter()
pw
write(id)
pw
flush()
} catch (Exception e)
{
e
printStackTrace()
}
}
}
}
使用過程中的一些總結
事件響應框架將服務的請求由完整的一次服務處理切割成為細粒度的多事件處理
為請求多階段並行處理提供了框架基礎
Event對象在事件處理方法結束後就被回收了
但是request和response在事件處理完以後還可以繼續使用
因此可以看出原來的阻塞式的方式已經可以通過事件的切分成為非阻塞的方式
沒有提供Servlet
中描述的suspend
resume
complete方法
無法主動控制request的異步處理
上面的代碼可以看出我只使用了Begin方法啟動了一個線程
但是由於無法主動地結束請求
因此在向客戶端返回數據以後還要等到超時才會結束這次會話
(看了Tomcat的代碼
也想模仿close的動作但是由於它使用了protected無法獲取封裝的request對象
因此無法釋放資源)
當然也可以通過客戶端配合
由客戶端主動發起再次的數據傳輸激發READ事件來結束會話
這麼做對客戶端的依賴比較強
同時也增加了客戶端的處理復雜度
Tomcat支持異步輸出
在APR或者NIO的模式下
Tomcat支持在系統壓力增大的時候
支持異步回寫大文件數據
總體上來說實現了部分對於Comet的支持
但是沒有對異步服務流程作很好的支持
無法在開發中使用(簡單順暢的使用)
JBoss的異步服務處理
JBoss
版本配置和使用與Tomcat
類似
沒有什麼差異
JBoss
剛剛發布了RC版本
對於異步服務處理作了很大的改動
與Tomcat配置很不同
這裡具體的說一下JBoss
中的異步服務使用
JBoss
已經將Tomcat中的Http
NioProtocol給刪除了
取而代之的是JBoss自己servlet包內增加的一個HttpEventServlet接口
這個接口和Tomcat的CometProcessor類似
首先
必須配置JBoss內置的Web容器為APR模式
也就是配置jbossweb
sar下面的server
xml中Connector 如下
<Connector protocol=
yote
port=
address=
${jboss
bind
address}
connectionTimeout=
redirectPort=
/>
其次異步服務處理的Servlet必須實現HttpEventServlet接口
接口只有一個方法
就是事件處理方法
public void event(HttpEvent event)
事件定義與Tomcat稍有不同
在BEGIN
ERROR
READ
END基礎上增加了TIMEOUT
EOF
EVENT
WRITE四個事件
同時去掉了SubType
TIMEOUT其實是從原來的Error的SubType分離出來的
這個方法是在最後一次處理事件到當前時間超過設定的超時時間而被激發的
同時TIMEOUT被激發並不會關閉請求處理流程
必須顯示調用事件的close方法才會結束會話
EOF事件將會在客戶端主動斷連的情況下被觸發
就好比IE窗口在請求過程中被關閉就會被觸發
EVENT事件在事件對象被調用resume的時候被激發
按照原意應該最好可以附帶上一些自定義信息來做一些工作
但是我自己使用過程中還沒有發現有什麼好的辦法可以在事件中附帶信息到事件處理中
WRITE方法在調用isWriteReady方法時被激發
可以在網絡出現問題或者繁忙的時候異步等待輸出
再則
JBoss的事件對象還支持幾個方法來實現異步處理以及Comet機制
方法如下
close方法
表示一次請求處理的結束
會告知客戶端沒有數據返回了
同時也會激發END事件
setTimeout方法
設置連接超時時間(單位毫秒)
計算超時是從最近的事件處理時間開始記錄的
如果發生超時
則會激發TIMEOUT事件
isReadReady方法
如果連接有數據可以讀取則返回true
如果這個方法返回false
servlet還試圖去讀去數據
則會阻塞
isWriteReady方法
如果返回true
則連接可以無阻塞的寫出數據
如果返回false
servlet必須停止寫數據
如果強制寫出
則可能會發生IO錯誤或者會采用異步輸出
當客戶端的輸出通道可用以後
則會激發write事件
suspend方法
suspend連接處理線程直到timeout發生或者resume被調用
實際上意味著servlet在suspend以後不再收到READ事件
READ事件將會在後台被不斷的激發
除非被suspend
resume方法
會激發event事件
可以利用這個方法來結束異步處理
同時也可以激活因為suspend停止的read事件
同時也可以在resume以後再調用suspend方法
注意
這裡未必是要求必須先suspend以後再resume
event
request
response在事件響應過程中都可以被使用
但是線程不安全
同時在調用了close以後
request
response資源會被釋放
可以通過對event對象做同步來保證線程安全的問題
當READ事件和END事件都發生的時候
首先會完成READ事件
然後再去完成END
具體的實現代碼如下
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(
*
)
//設置超時時間
//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()
//可以寫也可以不寫
因為進入這個方法也就是調用了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()
//如果不主動關閉
Timeout方法會被循環調用
會話不會結束
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方法調用後主動釋放連接資源的一種手段
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的生命周期已經不限制於事件處理方法
因此隨時可以關閉請求處理
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(
)
//危險!!!其實event
response
request都是線程不安全的
因此此時可能response已經被釋放
需要同步住event的對象來操作
效率可能會降低
PrintWriter pw = response
getWriter()
pw
write(id)
pw
flush()
event
resume()
//發送結束調用resume方法
進入event方法
結束請求處理
} catch (Exception e)
{
e
printStackTrace()
}
}
}
}
使用總結
對於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 request
connect reject
retrying request
connect reject
從上表可以看出
就純粹從處理效率來說
采用事件處理方式在線程切換過程中存在著一定的損失
但是就我們使用異步請求處理的本意來看
對於在高並發下對後端依賴無法避免的性能損耗情況下
異步請求解決了連接耗盡的問題
最後再來看我在測試過程中用JProfiler來截取的一些線程創建和使用狀況
上圖是最初的線程創建情況
還沒有任何請求被發送到服務端
因此線程池也沒有開任何一個連接
這是普通的Servlet在壓力測試下的線程狀況
線程就開到了
最大值
圖中由於程序來Hold請求處理線程出現了紅色阻塞和黃色等待
同時客戶端已經開始出現拒絕連接的錯誤
下圖就是錯誤的截圖
上圖是異步服務處理Servlet在壓力測試開始的情況
可以發現它的http線程還是
但是其他事件處理線程在不斷增長
下圖已經增長到了
多個線程
(這裡需要注意的就是這種異步處理資源申請沒有設置上限
因此對於資源消耗來說也是比較大的
同時要防范攻擊性請求造成服務端垮掉)
結語
多線程
分布式計算
Erlang等這些編程方式
框架設計
語言其實都在實現這一個理論
那就是分而治之
多線程是站在單應用的角度去考慮解決方案
分布式計算是在多機協作考慮解決方案
Erlang在單機多處理器的角度去考慮解決方案
但彼此的理念都是一樣
將能夠分割的不相關聯的獨立任務並行處理
最終實現最優化的處理效果
對於服務集成平台是否采用這種技術
我自己還沒有最終的決定
首先就如上面的測試結果來看
有得還是有失的
其次這種並發異步處理帶來的多線程維護控制復雜度
也需要考慮到成本中
Jetty的開發者對於是否將異步服務處理Servlet來交由開發者控制而不是容器本身來控制表示出了反對意見
的確
這樣復雜的控制交給開發者來處理會增加開發者的學習成本以及維護成本
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26410.html