最近的一個線上項目(認證服務器)老是出現服務延遲的情況。具體的問題描述:
(1)客戶端發(fā)送一個請求A(長連接),在服務器端的業(yè)務層需要20秒以上才能接收到。
(2)客戶端發(fā)送一個請求B(端連接),在服務器端的業(yè)務層可以迅速接收到。
從現象大致知道問題出在服務器端的網絡接收層,大量通過長連接發(fā)送過來的請求都堵塞在網絡層得不到處理(在網絡層排隊,還沒到應用層)。
(友情提示:本博文章歡迎轉載,但請注明出處:hankchen,http://www.tkk7.com/hankchen)
后來經過排查,發(fā)現是Netty中的OrderedMemoryAwareThreadPoolExecutor原因。相關代碼如下:
MemoryAwareThreadPoolExecutor executor = new OrderedMemoryAwareThreadPoolExecutor(threadNums, maxChannelMemorySize,
maxTotalMemorySize, keepAliveTime,
TimeUnit.SECONDS);
ExecutionHandler executionHandler = new ExecutionHandler(executor);
public ChannelPipeline getPipeline() throws Exception
{
ChannelPipeline pipeline = pipeline();
pipeline.addLast("decoder", new AuthDecoder());
pipeline.addLast("encoder", new AuthEncoder());
pipeline.addLast("executor", executionHandler);
pipeline.addLast("handler", new AuthServerHandler(commandFactory));
return pipeline;
}
先介紹下背景知識,再來分析問題。
大家都知道,Netty是一個基于事件的NIO框架。在Netty中,一切網絡動作都是通過事件來傳播并處理的,例如:Channel讀、Channel寫等等。回憶下Netty的流處理模型:
Boss線程(一個服務器端口對于一個)---接收到客戶端連接---生成Channel---交給Work線程池(多個Work線程)來處理。
具體的Work線程---讀完已接收的數據到ChannelBuffer---觸發(fā)ChannelPipeline中的ChannelHandler鏈來處理業(yè)務邏輯。
注意:執(zhí)行ChannelHandler鏈的整個過程是同步的,如果業(yè)務邏輯的耗時較長,會將導致Work線程長時間被占用得不到釋放,從而影響了整個服務器的并發(fā)處理能力。
所以,為了提高并發(fā)數,一般通過ExecutionHandler線程池來異步處理ChannelHandler鏈(worker線程在經過ExecutionHandler后就結束了,它會被ChannelFactory的worker線程池所回收)。在Netty中,只需要增加一行代碼:
public ChannelPipeline
getPipeline() {
return Channels
.pipeline(
new DatabaseGatewayProtocolEncoder(),
new DatabaseGatewayProtocolDecoder(),
executionHandler, // Must be shared
new DatabaseQueryingHandler());
}
例如:
ExecutionHandler
executionHandler = new ExecutionHandler
(
new OrderedMemoryAwareThreadPoolExecutor
(16, 1048576, 1048576))
對于ExecutionHandler需要的線程池模型,Netty提供了兩種可選:
1) MemoryAwareThreadPoolExecutor 通過對線程池內存的使用控制,可控制Executor中待處理任務的上限(超過上限時,后續(xù)進來的任務將被阻塞),并可控制單個Channel待處理任務的上限,防止內存溢出錯誤;
2) OrderedMemoryAwareThreadPoolExecutor 是 MemoryAwareThreadPoolExecutor 的子類。除了MemoryAwareThreadPoolExecutor 的功能之外,它還可以保證同一Channel中處理的事件流的順序性,這主要是控制事件在異步處理模式下可能出現的錯誤的事件順序,但它并不保證同一Channel中的事件都在一個線程中執(zhí)行(通常也沒必要)。
例如:
Thread X: --- Channel A (Event A1) --. .-- Channel B (Event B2) --- Channel B (Event B3) --->
\ /
X
/ \
Thread Y: --- Channel B (Event B1) --' '-- Channel A (Event A2) --- Channel A (Event A3) --->
上圖表達的意思有幾個:
(1)對整個線程池而言,處理同一個Channel的事件,必須是按照順序來處理的。例如,必須先處理完Channel A (Event A1) ,再處理Channel A (Event A2)、Channel A (Event A3)
(2)同一個Channel的多個事件,會分布到線程池的多個線程中去處理。
(3)不同Channel的事件可以同時處理(分擔到多個線程),互不影響。
OrderedMemoryAwareThreadPoolExecutor 的這種事件處理有序性是有意義的,因為通常情況下,請求發(fā)送端希望服務器能夠按照順序處理自己的請求,特別是需要多次握手的應用層協(xié)議。例如:XMPP協(xié)議。
現在回到具體業(yè)務上來,我們這里的認證服務也使用了OrderedMemoryAwareThreadPoolExecutor。認證服務的其中一個環(huán)節(jié)是使用長連接,不斷處理來自另外一個服務器的認證請求。通信的數據包都很小,一般都是200個字節(jié)以內。一般情況下,處理這個過程很快,所以沒有什么問題。但是,由于認證服務需要調用第三方的接口,如果第三方接口出現延遲,將導致這個過程變慢。一旦一個事件處理不完,由于要保持事件處理的有序性,其他事件就全部堵塞了!而短連接之所以沒有問題,是因為短連接一個Channel就一個請求數據包,處理完Channel就關閉了,根本不存在順序的問題,所以在業(yè)務層可以迅速收到請求,只是由于同樣的原因(第三方接口),處理時間會比較長。
其實,認證過程都是獨立的請求數據包(單個帳號),每個請求數據包之間是沒有任何關系的,保持這樣的順序沒有意義!
最后的改進措施:
1、去掉OrderedMemoryAwareThreadPoolExecutor,改用MemoryAwareThreadPoolExecutor。
2、減少調用第三方接口的超時時間,讓處理線程盡早回歸線程池。
(友情提示:本博文章歡迎轉載,但請注明出處:hankchen,http://www.tkk7.com/hankchen)