背景
生產(chǎn)環(huán)境存在一些接口强重,其因為后端服務(wù)涉及到大量數(shù)據(jù)庫讀寫操作,因此接口非常耗時膳叨。比如商品導(dǎo)入功能,經(jīng)過事務(wù)拆分痘系、拆分查詢并組裝數(shù)據(jù)等手段對功能進行了優(yōu)化菲嘴,在不改變業(yè)務(wù)設(shè)計的前提下幾乎無任何優(yōu)化空間。
容器(tomcat)中線程的數(shù)量是一定的汰翠,容器處理大量耗時請求時龄坪,勢必會影響其他接口的正常訪問。
因此复唤,在不改變業(yè)務(wù)的前提下健田,在高并發(fā)場景提高商品服務(wù)的吞吐率優(yōu)化是非常必要的。
技術(shù)實現(xiàn)方案
采用Spring MVC異步處理方案(適用于耗時同步交易場景)佛纫。Spring MVC異步處理實現(xiàn)方案通常支持3種方式:
- Callable實現(xiàn)
- WebAsyncTask實現(xiàn)
- DefferedResult實現(xiàn)
我們使用DefferedResult + 線程池 + 阻塞隊列LinkedBlockingQueue 實現(xiàn)請求的異步處理同步響應(yīng)妓局。
關(guān)于DeferredResult
DeferredResult從 Spring 3.2 開始可用,有助于將長時間運行的計算從 http-worker 線程卸載到單獨的線程呈宇。
盡管另一個線程會占用一些資源進行計算好爬,但工作線程在此期間不會被阻塞并且可以處理傳入的其他客戶端請求。
異步請求處理模型非常有用甥啄,因為它有助于在高負載期間很好地擴展應(yīng)用程序抵拘,尤其是對于 IO 密集型操作。
DeferredResult處理流程
DeferredResult的處理過程與Callback類似型豁,不一樣的地方在于它的結(jié)果不是DeferredResult直接返回的僵蛛,而是由其它線程通過同步的方式設(shè)置到該對象中。它的執(zhí)行過程如下所示:
- 客戶端請求服務(wù)
SpringMVC調(diào)用Controller迎变,Controller返回一個DeferredResult對象 - SpringMVC調(diào)用ruquest.startAsync
- DispatcherServlet以及Filters等從應(yīng)用服務(wù)器線程中結(jié)束(釋放容器線程)充尉,但Response仍舊是打開狀態(tài),也就是說暫時還不返回給客戶端
- 異步線程處理實際業(yè)務(wù)并將結(jié)果設(shè)置到DeferredResult中衣形,SpringMVC將請求發(fā)送給應(yīng)用服務(wù)器繼續(xù)處理
- DispatcherServlet再次被調(diào)用并且繼續(xù)處理DeferredResult中的結(jié)果驼侠,最終將其返回給客戶端。
重要技術(shù)點
線程池
線程池維護多個線程谆吴,等待監(jiān)督管理者分配可并發(fā)執(zhí)行的任務(wù)倒源。這種做法,一方面避免了處理任務(wù)時創(chuàng)建銷毀線程開銷的代價句狼,另一方面避免了線程數(shù)量膨脹導(dǎo)致的過分調(diào)度問題笋熬,保證了對內(nèi)核的充分利用。
LinkedBlockingQueue
LinkedBlockingQueue實現(xiàn)是線程安全的腻菇,實現(xiàn)了先進先出等特性胳螟,是作為生產(chǎn)者消費者的首選昔馋,LinkedBlockingQueue 可以指定容量,也可以不指定糖耸,不指定的話秘遏,默認最大是Integer.MAX_VALUE,其中主要用到put和take方法嘉竟,put方法在隊列滿的時候會阻塞直到有隊列成員被消費邦危,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來舍扰。這里需要注意直接使用LinkedBlockingQueue阻塞隊列作為線程池會存在一個問題倦蚪,當workcount > corePool時優(yōu)先進入隊列排隊,因此當請求并發(fā)過多會導(dǎo)致請求緩慢妥粟,甚至因為隊列過多出現(xiàn)內(nèi)存溢出(JDK是先排隊再漲線程池)
網(wǎng)絡(luò)上基本找得到的DeferredResult相關(guān)的技術(shù)博客或文章都是直接創(chuàng)建線程或使用LinkedBlockingQueue的線程池审丘,實際在壓力測試過程當模擬接口并發(fā)到50就出現(xiàn)大量延遲,比不優(yōu)化時性能還差勾给。有興趣的可以直接使用LinkedBlockingQueue作為線程池隊列壓力測試看看效果滩报。
Tomcat的線程池
org.apache.tomcat.util.threads.TaskQueue
org.apache.tomcat.util.threads.ThreadPoolExecutor
因為我們優(yōu)化的是web接口請求,不能因為LinkedBlockingQueue的排隊導(dǎo)致接口出現(xiàn)大量延遲和緩慢播急,因此我們在實現(xiàn)過程不直接使用LinkedBlockingQueue作為線程池的阻塞隊列脓钾,而是使用tomcat的線程池TaskQueue,TaskQueue繼承了JDK的LinkedBlockingQueue 并擴展了JDK線程池的功能桩警,主要體現(xiàn)在兩點:
- Tomcat的ThreadPoolExecutor使用的TaskQueue可训,是無界的LinkedBlockingQueue,但是通過taskQueue的offer方法覆蓋了LinkedBlockingQueue的offer方法捶枢,改寫了規(guī)則握截,使得線程池能在任務(wù)較多的情況下增長線程池數(shù)量——JDK是先排隊再漲線程池,Tomcat則是先漲線程池再排隊烂叔。
- Tomcat的ThreadPoolExecutor改寫了execute方法谨胞,當任務(wù)被reject時,捕獲異常蒜鸡,并強制入隊胯努。
代碼實現(xiàn)
創(chuàng)建處理耗時任務(wù)的線程池
public static ThreadPoolExecutor executor = null;
private TaskQueue taskqueue;
protected int maxQueueSize = Integer.MAX_VALUE;
protected int threadPriority = 5;
protected boolean daemon = true;
protected String namePrefix = "testsleep-";
protected int minSpareThreads = 25;
protected int maxThreads = 200;
protected int maxIdleTime = 60000;
protected long threadRenewalDelay = 1000L;
protected boolean prestartminSpareThreads = false;
/**
* 初始化時啟動監(jiān)聽請求隊列
*/
@PostConstruct
public void init() {
/*cachedThreadPool = new ThreadPoolExecutor(4,
50,
0,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(50),
r -> new Thread(r));*/
// 任務(wù)隊列:這里你看到的是一個無界隊列,但是隊列里面進行了特殊處理
taskqueue = new TaskQueue(maxQueueSize);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon, threadPriority);
// 創(chuàng)建線程池逢防,這里的ThreadPoolExecutor是Tomcat繼承自JDK的ThreadPoolExecutor
executor = new ThreadPoolExecutor(
minSpareThreads, maxThreads, // 核心線程數(shù)與最大線程數(shù)
maxIdleTime, TimeUnit.MILLISECONDS, // 默認6萬毫秒的超時時間叶沛,也就是一分鐘
taskqueue, tf); // 玄機在任務(wù)隊列的設(shè)置
executor.setThreadRenewalDelay(threadRenewalDelay);
if (prestartminSpareThreads) {
executor.prestartAllCoreThreads(); // 預(yù)熱所有的核心線程
}
taskqueue.setParent(executor);
}
重構(gòu)請求,使用DeferredResult實現(xiàn)異步處理
這里我們直接使用Thread.sleep模擬一個耗時任務(wù)
@GetMapping("/users-anon/test/{testkey}")
public DeferredResult<Result<String>> testSleep(@PathVariable String testkey) {
//return service.testSleep(); //直接調(diào)用耗時業(yè)務(wù)處理
DeferredResult<Result<String>> output = new DeferredResult<>(1000 * 30L);
output.onTimeout(() ->
output.setErrorResult(
ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body("Request timeout occurred.")));
log.info("[ TestSleepController ] 接到請求");
//轉(zhuǎn)到后臺線程
QueueListener.executor.execute(() -> {
log.info("開始執(zhí)行耗時任務(wù):{}", System.currentTimeMillis());
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}
log.info("執(zhí)行耗時任務(wù)結(jié)束:{}", System.currentTimeMillis());
output.setResult(Result.succeed());
});
log.info("[ TestSleepController ] 返回DeferredResult忘朝,并釋放容器線程.");
return output;
}
壓力測試
機器參數(shù)
CentOS Linux release 7.3.1611 (Core) 1核 2.30GHz 8G內(nèi)存
服務(wù)部署
Docker容器
測試結(jié)果
單接口測試
在測試DeferredResult前灰署,我們先模擬一個耗時接口,并對接口進行壓力測試:
100線程 循環(huán)一次
200線程 循環(huán)一次
500線程 循環(huán)一次
當耗時接口在100、200并發(fā)下接口基本正常氓侧,當達到500時接口響應(yīng)出現(xiàn)明顯的遲緩脊另。
混合接口測試
我們建立兩個線程組导狡,一個是正常的耗時任務(wù)A(線程組2)约巷,一個是使用DeferredResult優(yōu)化的耗時任務(wù)B(線程組1)。
任務(wù)A旱捧、B 各50線程 循環(huán)一次
壓測結(jié)果如下:
整體響應(yīng)都差不多独郎。
任務(wù)A 50線程 循環(huán)一次 任務(wù)B 500線程 循環(huán)一次
DeferredResult優(yōu)化的耗時任務(wù)壓測結(jié)果:
正常任務(wù)壓測結(jié)果:
當任務(wù)BDeferredResult優(yōu)化后的接口并發(fā)爆發(fā)式增長后,接口的響應(yīng)仍和優(yōu)化前一樣出現(xiàn)大范圍的延遲枚赡,但是任務(wù)A的接口響應(yīng)并未收到影響氓癌。
結(jié)果分析
通過壓測結(jié)果分析我們可以得出DeferredResult優(yōu)化的耗時任務(wù)雖然不能提示耗時接口本身的響應(yīng)速度,但是能極大減少耗時任務(wù)對服務(wù)容器線程的占用贫橙,提升應(yīng)用在高并發(fā)場景下本身的吞吐量贪婉。