記一次使用Java8并行流導(dǎo)致的服務(wù)瓶頸問(wèn)題排查

一、業(yè)務(wù)背景#

二、服務(wù)架構(gòu)#

服務(wù)使用線(xiàn)程池對(duì)請(qǐng)求進(jìn)行業(yè)務(wù)處理,corePoolSize=32,maximumPoolSize=128。

三、問(wèn)題描述#

服務(wù)部署到測(cè)試環(huán)境,將線(xiàn)上流量通過(guò)tcp-copy打到服務(wù)上后,測(cè)試反饋出現(xiàn)丟失消息的情況。查看服務(wù)日志,發(fā)現(xiàn)了
service overload discard msg
即業(yè)務(wù)線(xiàn)程處理緩慢造成消息堆積隊(duì)列超限,后續(xù)消息被ExecutorService的Reject策略主動(dòng)丟棄。
查詢(xún)服務(wù)請(qǐng)求QPS為500/s,不算高。理論上業(yè)務(wù)處理流程沒(méi)有很重很慢的操作,監(jiān)控外部依賴(lài)接口的響應(yīng)速度也在ms級(jí)別,所以很奇怪為什么服務(wù)的性能瓶頸這么低。

四、問(wèn)題查解#

1、系統(tǒng)監(jiān)控####

查詢(xún)系統(tǒng)監(jiān)控指標(biāo),包括CPU使用率(18%)、網(wǎng)卡流量、內(nèi)存使用率和IO時(shí)間等,未發(fā)現(xiàn)異常。
查詢(xún)jvm 內(nèi)存使用及GC情況,未發(fā)現(xiàn)異常。

2、jvm 線(xiàn)程堆棧####

jstack pid >pid.txt打印服務(wù)進(jìn)程線(xiàn)程棧信息:

線(xiàn)程堆棧信息

發(fā)現(xiàn)異常:
128個(gè)業(yè)務(wù)線(xiàn)程中有126個(gè)線(xiàn)程狀態(tài)為java.lang.Thread.State: WAITING (on object monitor),只有2個(gè)線(xiàn)程狀態(tài)為正常RUNNABLE。

WAITING狀態(tài)線(xiàn)程數(shù)
RUNNABLE狀態(tài)的兩個(gè)業(yè)務(wù)線(xiàn)程

顯然大量業(yè)務(wù)線(xiàn)程阻塞等待在異常位置:


等待處對(duì)應(yīng)的代碼位置

這段代碼使用了java8提供的并行流parallelStream來(lái)將消息分發(fā)給下面的listeners集合進(jìn)行處理。

查詢(xún)資料(可參看文章 http://www.cnblogs.com/gaobig/p/4874400.html )找到原因:


在開(kāi)發(fā)中,我們常常通過(guò)以下方法,實(shí)現(xiàn)并行流執(zhí)行并行任務(wù):
myList.parallelStream.map(obj -> longRunningOperation())
但是這存在一個(gè)嚴(yán)重的問(wèn)題:在 JVM 的后臺(tái),使用通用的 fork/join 池來(lái)完成上述功能,該池是所有并行流共享的。默認(rèn)情況,fork/join 池會(huì)為每個(gè)處理器分配一個(gè)線(xiàn)程。假設(shè)你有一臺(tái)16核的機(jī)器,這樣你就只能創(chuàng)建16個(gè)線(xiàn)程。對(duì) CPU 密集型的任務(wù)來(lái)說(shuō),這樣是有意義的,因?yàn)槟愕臋C(jī)器確實(shí)只能執(zhí)行16個(gè)線(xiàn)程。但是真實(shí)情況下,不是所有的任務(wù)都是 CPU 密集型的,這就會(huì)導(dǎo)致線(xiàn)程因IO等待浪費(fèi)CPU資源,降低系統(tǒng)處理性能。


而測(cè)試機(jī)的CPU核數(shù)確實(shí)是2核,這就解釋了上面128個(gè)業(yè)務(wù)線(xiàn)程中只有2個(gè)線(xiàn)程處于RUNNABLE狀態(tài),而其他126個(gè)業(yè)務(wù)線(xiàn)程都在等待的原因。->因?yàn)闃I(yè)務(wù)線(xiàn)程內(nèi)部使用了parallelStream處理業(yè)務(wù)數(shù)據(jù),所以所有業(yè)務(wù)線(xiàn)程內(nèi)部都需要使用jvm for/join線(xiàn)程進(jìn)行業(yè)務(wù)處理,因?yàn)橹挥衘vm fork/join線(xiàn)程,所以同時(shí)只能處理兩個(gè)業(yè)務(wù)線(xiàn)程的執(zhí)行邏輯,其他業(yè)務(wù)線(xiàn)程排隊(duì)等待執(zhí)行。

五、解決方法#

1、代碼修改####

將并行流parallelStream()改為stream()。


修改后的代碼

2、回歸驗(yàn)證####

重新部署測(cè)試服務(wù)器后,發(fā)現(xiàn)已無(wú)消息丟失報(bào)錯(cuò)。
觀察機(jī)器負(fù)載,CPU使用率上升到40%,說(shuō)明CPU資源得到了更充分的利用。
jvm YongGC頻率加快到10s一次,每次GC時(shí)間為10ms,可以接受。
jstack pid > pid.txt觀察現(xiàn)在的線(xiàn)程棧:


RUNNABLE狀態(tài)的業(yè)務(wù)線(xiàn)程數(shù)

剩余WAITING狀態(tài)線(xiàn)程

1)同時(shí)有16個(gè)業(yè)務(wù)線(xiàn)程處于執(zhí)行狀態(tài),系統(tǒng)并發(fā)性大幅提升;
2)剩余處于TIMED_WAITING狀態(tài)的業(yè)務(wù)線(xiàn)程數(shù)也降為44個(gè),棧信息顯示其在等待從線(xiàn)程池任務(wù)隊(duì)列中取出下一個(gè)任務(wù)執(zhí)行,線(xiàn)程池任務(wù)隊(duì)列無(wú)堆積。

至此,問(wèn)題處理完畢。請(qǐng)大家注意Java8中并行流parallelStream的使用避免才坑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容