歷史原因,當前有一個服務(wù)專門用于處理mq消息,mq使用的阿里云rocketmq,sdk版本1.2.6(2016年)。
隨著業(yè)務(wù)的發(fā)展,該應(yīng)用上的consumer越來越多,接近200+,導(dǎo)致該應(yīng)用所在的ecs長時間load高,頻繁報警。
現(xiàn)象分析
該應(yīng)用所在的ecs服務(wù)器load長期飆高(該ecs上只有一個服務(wù)),但cpu、io、內(nèi)存等資源利用率較低,系統(tǒng)負載參考下圖:

ECS配置:4核8G
物理cpu個數(shù)=4
單個物理CPU中核(core)的個數(shù)=1
單核多處理器
在系統(tǒng)負荷方面,多核CPU與多CPU效果類似,考慮系統(tǒng)負荷的時候,把系統(tǒng)負荷除以總的核心數(shù),只要每個核心的負荷不超過1.0,就表明正常運行。 通常,n核cpu時,load<n,系統(tǒng)負載都屬于正常情況。
套用以上規(guī)則: 先觀察load_15m和load_5m,load基本保持在3-5之間,說明系統(tǒng)中長期負載保持在一個較高的量級。 再觀察load_1m可以看出,波動很大,并且很多時間段內(nèi)遠大于cpu核心數(shù)。 短期內(nèi)繁忙,中長期內(nèi)緊張,很可能是一個擁塞的開始。
原因定位
排查導(dǎo)致load高的原因
tips:系統(tǒng)load高,不代表cpu資源不足。Load高只是代表需要運行的隊列累計過多。但隊列中的任務(wù)實際可能是耗cpu的,也可能是耗i/0及其他因素的

圖中l(wèi)oad_15,load_5,load_1均大于核心數(shù)4,超負荷運行
- 用戶進程=8.6%
- 內(nèi)核進程 =9.7%
- 空閑=80%
- I/O等待所占用的cpu時間百分比=0.3%

通過上圖CPU、內(nèi)存、IO使用情況,發(fā)現(xiàn)三者都不高, CPU使用率低負載高,排除cpu資源不足導(dǎo)致load高的可能性。
再通過vmstat查看進程、內(nèi)存、I/O等系統(tǒng)整體運行狀態(tài),如下圖:

從結(jié)果上看,io的block in和block out 并不頻繁,但是system的中斷數(shù)(in)、上下文切換(cs)特別頻繁,進程上下文切換次數(shù)較多的情況下,很容易導(dǎo)致CPU將大量的時間耗費在寄存器、內(nèi)核棧、以及虛擬內(nèi)存等資源的保存和恢復(fù)上,進而縮短了真正運行進程的時間造成load高。
CPU寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。程序計數(shù)器,則是用來存儲CPU正在執(zhí)行的指令的位置,或者即將執(zhí)行的下一條指令的位置。
他們都是CPU在運行任何任務(wù)前,必須依賴的環(huán)境,因此也被叫做CPU上下文。
CPU上下文切換,就是先把前一個任務(wù)的CPU上下文(也就是CPU寄存器和程序計數(shù)器)保存起來,然后加載新任務(wù)的上下文,到這些寄存器和程序計數(shù)器,最后再跳轉(zhuǎn)到程序計數(shù)器所指的新位置,運行新任務(wù)。
排查大方向:頻繁的中斷以及線程切換(由于該臺ecs上只存在一個java服務(wù),主要排查該進程)
通過vmstate只能查看總的cpu上下文切換,可通過pidstat命令查看線程層面的上下文切換信息 pidstat -wt 1(下圖拉的是9s的數(shù)據(jù),總共36w次,平均每秒4w次)

觀察上圖,很容易發(fā)現(xiàn)兩個現(xiàn)象:
第一個是java線程特別多;
第二個是很有規(guī)律的出現(xiàn)每秒上下文切換100+次的線程。(具體原因后面分析)
確認一下這些java線程的來源,查看該應(yīng)用進程下的線程數(shù) cat /proc/17207/status

線程數(shù)9749(非高峰)
排查方向:
- 線程數(shù)過多
- 部分線程每秒上下文切換次數(shù)過高
先排查主要原因,即部分線程上下文切換次數(shù)過高 拉一下線上該進程的堆棧信息,然后找到切換次數(shù)達到100+/每秒的線程id,把線程id轉(zhuǎn)成16進制后在堆棧日志中檢索


從上圖可以看到進程狀態(tài)TIME_WAITING,問題代碼,
com.alibaba.ons.open.trace.core.dispatch.impl.AsyncArrayDispatcher,多查幾個其他上下文切換頻繁的線程,堆棧信息基本相同。
再排查線程數(shù)過多的問題,分析堆棧信息會發(fā)現(xiàn)存在大量ConsumeMessageThread線程(通信、監(jiān)聽、心跳等線程先忽略)

通過線程名稱,在rocketmq源碼中搜索基本能定位到下面這部分代碼

通過兩段代碼,基本可以定位到問題出現(xiàn)在mq consumer初始化以及啟動的流程,后續(xù)根據(jù)代碼進行分析。
代碼分析
1、線程數(shù)過多代碼層面排查,從上面代碼截圖可以看到,ConsumeMessageThread_ 由線程池進行管理,再看一下線程池的關(guān)鍵參數(shù),核心線程數(shù)this.defaultMQPushConsumer.getConsumeThreadMin()、最大線程數(shù)this.defaultMQPushConsumer.getConsumeThreadMax()、無界隊列LinkedBlockingQueue
ps:由于線程池隊列用的LinkedBlockingQueue無界隊列,LinkedBlockingQueue的容量默認大小是Integer.Max,在任務(wù)沒有填滿這個容量之前不會創(chuàng)建新的工作線程,因此最大線程數(shù)沒有任何作用。
再看一下message-consumer對核心線程數(shù)以及最大線程數(shù)的配置,發(fā)現(xiàn)代碼層面沒有特殊配置,因此使用系統(tǒng)默認值,即下圖


至此,大致可以定位到線程數(shù)過多的原因:
由于未指定消費線程數(shù)量(ConsumeThreadNums),采用系統(tǒng)默認核心線程數(shù)20,最大線程數(shù)64.每個consumer初始化的時候都會創(chuàng)建一個核心線程數(shù)等于20的線程池,即大概率每個consumer都會存在20個線程消費消息, 導(dǎo)致線程數(shù)飆升(20*consumer個數(shù)),但發(fā)現(xiàn)這些消費線程大部分都處于sleep/wait狀態(tài),對上下文切換影響不大。
線程上下文切換次數(shù)過高代碼層面排查: 在rocketmq源碼中無法搜索到該段代碼,該應(yīng)用使用阿里云sdk,在sdk中檢索,查看上下文以及調(diào)用鏈路,會發(fā)現(xiàn)這段代碼屬于軌跡回傳模塊。

結(jié)合代碼分析一下軌跡回傳模塊的流程(AsyncArrayDispatcher),總結(jié)如下:

在sdk源碼中定位線程堆棧日志中的代碼,如下:

從這段代碼以及堆棧信息可以看到問題出現(xiàn)在traceContextQueue.poll(5,TimeUnit.MILLISECONDS);其中traceContextQueue為有界阻塞隊列,poll時,如果隊列為空,會阻塞一定時間,因此會導(dǎo)致線程在running和time_wait之間進行頻繁切換。
至于為什么要用poll(5,TimeUnit.MILLISECONDS)而不是take(),個人認為可能是為了減少網(wǎng)絡(luò)io,5ms批量取一次丟到線程池批量上報,避免單個軌跡頻繁上報?
poll()方法會返回隊列的第一個元素,并刪除;如果隊列為空,則返回null,并不會阻塞當前線程;出隊的邏輯調(diào)用的是 dequeue()方法,此外,它還有一個重載的方法,poll(long timeout, TimeUnit unit),如果隊列為空,則會等待一段時間。

軌跡隊列traceContextQueue使用的是ArrayBlockingQueue,一個有界的阻塞隊列,內(nèi)部使用一個數(shù)組來存放元素,通過鎖來實現(xiàn)并發(fā)訪問的,也是按照 FIFO 的原則對元素進行排列。

通過上面的代碼可以看到,其通過reentrantLock 來實現(xiàn)并發(fā)的控制,ReentrantLock 提供了公平鎖與非公平鎖的實現(xiàn),但ArrayBlockingQueue默認情況下,使用的非公平鎖,不保證線程線程公平的訪問隊列。
所謂的公平是指阻塞的線程,按照阻塞的先后順序訪問隊列,非公平是指當隊列可用的時候,阻塞的線程都可以有爭奪線程訪問的資格,有可能先阻塞的線程最后才能訪問隊列。
由于每個consumer都只開了一個軌跡分發(fā)線程,所以這部分不存在競爭。
再看一下ArrayBlockingQueue的阻塞實現(xiàn)原理



通過上面這部分代碼可以看到阻塞最終是通過park方法實現(xiàn),unsafe.park是個native方法
park這個方法會阻塞當前線程,當以下4種情況中的一種發(fā)生時,該方法才會返回
- 與park對應(yīng)的unpark執(zhí)行或已經(jīng)執(zhí)行時
- 線程被中斷時
- 等待完time參數(shù)指定的毫秒數(shù)時
- 異?,F(xiàn)象發(fā)生
至此,系統(tǒng)線程切換以及中斷頻繁原因總結(jié)如下:
阿里云sdk中軌跡回發(fā)模塊,一個consumer有一個分發(fā)線程和一個軌跡隊列以及一個軌跡數(shù)據(jù)回發(fā)線程池,分發(fā)線程從軌跡隊列中取,取不到則阻塞5ms,取到塞到軌跡數(shù)據(jù)回發(fā)線程池,然后數(shù)據(jù)上報。
過多的分發(fā)線程頻繁在running和time_wait狀態(tài)進行切換,導(dǎo)致系統(tǒng)load高。由于代碼層面未設(shè)置每個consumer消息消費的最大最小線程數(shù),導(dǎo)致每個consumer都會開20個核心線程進程消息消費,導(dǎo)致線程數(shù)量過多消耗系統(tǒng)資源以及空跑。
優(yōu)化方案
結(jié)合以上原因,進行針對性優(yōu)化
代碼層面針對每個consumer設(shè)置線程數(shù)配置項,consumer可根據(jù)承載的業(yè)務(wù)等實際情況設(shè)置核心線程數(shù),減少整體的線程數(shù)目,避免大量線程空跑。
以上分析用的是阿里云ons 1.2.6的版本,當前已經(jīng)迭代到了1.8.5版本,通過分析1.8.5版本的軌跡回傳模塊的源碼,發(fā)現(xiàn)對軌跡回傳增加了開關(guān),配置軌跡回傳使用單個線程(單例),即全部consumer使用一個分發(fā)線程、一個軌跡有界隊列、一個軌跡上報線程池來處理,可以考慮驗證通過后升版本。



文源網(wǎng)絡(luò),僅供學(xué)習(xí)之用,如有侵權(quán)請聯(lián)系刪除。
我將面試題和答案都整理成了PDF文檔,還有一套學(xué)習(xí)資料,涵蓋Java虛擬機、spring框架、Java線程、數(shù)據(jù)結(jié)構(gòu)、設(shè)計模式等等,但不僅限于此。
關(guān)注公眾號【java圈子】獲取資料,還有優(yōu)質(zhì)文章每日送達。
file
