摘要:打開郵件一看,果然告知我有一個應(yīng)用的線程池隊列達到閾值觸發(fā)了報警。線程池的名稱一定得取的有意義,不然是自己給自己增加難度。根據(jù)監(jiān)控將線程池的隊列大小調(diào)整為一個具體值,并且要有拒絕策略。
背景
上午剛到公司,準備開始一天的摸魚之旅時突然收到了一封監(jiān)控中心的郵件。
心中暗道不好,因為監(jiān)控系統(tǒng)從來不會告訴我應(yīng)用完美無 bug,其實系統(tǒng)挺猥瑣。
打開郵件一看,果然告知我有一個應(yīng)用的線程池隊列達到閾值觸發(fā)了報警。
由于這個應(yīng)用出問題非常影響用戶體驗;于是立馬讓運維保留現(xiàn)場 dump 線程和內(nèi)存同時重啟應(yīng)用,還好重啟之后恢復(fù)正常。于是開始著手排查問題。
分析首先了解下這個應(yīng)用大概是做什么的。
簡單來說就是從 MQ 中取出數(shù)據(jù)然后丟到后面的業(yè)務(wù)線程池中做具體的業(yè)務(wù)處理。
而報警的隊列正好就是這個線程池的隊列。
跟蹤代碼發(fā)現(xiàn)構(gòu)建線程池的方式如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());; put(poolName,executor);
采用的是默認的 LinkedBlockingQueue 并沒有指定大小(這也是個坑),于是這個隊列的默認大小為 Integer.MAX_VALUE。
由于應(yīng)用已經(jīng)重啟,只能從僅存的線程快照和內(nèi)存快照進行分析。
內(nèi)存分析先利用 MAT 分析了內(nèi)存,的到了如下報告。
其中有兩個比較大的對象,一個就是之前線程池存放任務(wù)的 LinkedBlockingQueue,還有一個則是 HashSet。
當然其中隊列占用了大量的內(nèi)存,所以優(yōu)先查看,HashSet 一會兒再看。
由于隊列的大小給的夠大,所以結(jié)合目前的情況來看應(yīng)當是線程池里的任務(wù)處理較慢,導(dǎo)致隊列的任務(wù)越堆越多,至少這是目前可以得出的結(jié)論。線程分析
再來看看線程的分析,這里利用 fastthread.io 這個網(wǎng)站進行線程分析。
因為從表現(xiàn)來看線程池里的任務(wù)遲遲沒有執(zhí)行完畢,所以主要看看它們在干嘛。
正好他們都處于 RUNNABLE 狀態(tài),同時堆棧如下:
發(fā)現(xiàn)正好就是在處理上文提到的 HashSet,看這個堆棧是在查詢 key 是否存在。通過查看 312 行的業(yè)務(wù)代碼確實也是如此。
這里的線程名字也是個坑,讓我找了好久。定位
分析了內(nèi)存和線程的堆棧之后其實已經(jīng)大概猜出一些問題了。
這里其實有一個前提忘記講到:
這個告警是凌晨三點發(fā)出的郵件,但并沒有電話提醒之類的,所以大家都不知道。
到了早上上班時才發(fā)現(xiàn)并立即 dump 了上面的證據(jù)。
所有有一個很重要的事實:這幾個業(yè)務(wù)線程在查詢 HashSet 的時候運行了 6 7 個小時都沒有返回。
通過之前的監(jiān)控曲線圖也可以看出:
操作系統(tǒng)在之前一直處于高負載中,直到我們早上看到報警重啟之后才降低。
同時發(fā)現(xiàn)這個應(yīng)用生產(chǎn)上運行的是 JDK1.7 ,所以我初步認為應(yīng)該是在查詢 key 的時候進入了 HashMap 的環(huán)形鏈表導(dǎo)致 CPU 高負載同時也進入了死循環(huán)。
為了驗證這個問題再次 review 了代碼。
整理之后的偽代碼如下:
//線程池 private ExecutorService executor; private Setset = new hashSet(); private void execute(){ while(true){ //從 MQ 中獲取數(shù)據(jù) String key = subMQ(); executor.excute(new Worker(key)) ; } } public class Worker extends Thread{ private String key ; public Worker(String key){ this.key = key; } @Override private void run(){ if(!set.contains(key)){ //數(shù)據(jù)庫查詢 if(queryDB(key)){ set.add(key); return; } } //達到某種條件時清空 set if(flag){ set = null ; } } }
大致的流程如下:
源源不斷的從 MQ 中獲取數(shù)據(jù)。
將數(shù)據(jù)丟到業(yè)務(wù)線程池中。
判斷數(shù)據(jù)是否已經(jīng)寫入了 Set。
沒有則查詢數(shù)據(jù)庫。
之后寫入到 Set 中。
這里有一個很明顯的問題,那就是作為共享資源的 Set 并沒有做任何的同步處理。
這里會有多個線程并發(fā)的操作,由于 HashSet 其實本質(zhì)上就是 HashMap,所以它肯定是線程不安全的,所以會出現(xiàn)兩個問題:
Set 中的數(shù)據(jù)在并發(fā)寫入時被覆蓋導(dǎo)致數(shù)據(jù)不準確。
會在擴容的時候形成環(huán)形鏈表。
第一個問題相對于第二個還能接受。
通過上文的內(nèi)存分析我們已經(jīng)知道這個 set 中的數(shù)據(jù)已經(jīng)不少了。同時由于初始化時并沒有指定大小,僅僅只是默認值,所以在大量的并發(fā)寫入時候會導(dǎo)致頻繁的擴容,而在 1.7 的條件下又可能會形成環(huán)形鏈表。
不巧的是代碼中也有查詢操作(contains()),觀察上文的堆棧情況:
發(fā)現(xiàn)是運行在 HashMap 的 465 行,來看看 1.7 中那里具體在做什么:
已經(jīng)很明顯了。這里在遍歷鏈表,同時由于形成了環(huán)形鏈表導(dǎo)致這個 e.next 永遠不為空,所以這個循環(huán)也不會退出了。
到這里其實已經(jīng)找到問題了,但還有一個疑問是為什么線程池里的任務(wù)隊列會越堆越多。我第一直覺是任務(wù)執(zhí)行太慢導(dǎo)致的。
仔細查看了代碼發(fā)現(xiàn)只有一個地方可能會慢:也就是有一個數(shù)據(jù)庫的查詢。
把這個 SQL 拿到生產(chǎn)環(huán)境執(zhí)行發(fā)現(xiàn)確實不快,查看索引發(fā)現(xiàn)都有命中。
但我一看表中的數(shù)據(jù)發(fā)現(xiàn)已經(jīng)快有 7000W 的數(shù)據(jù)了。同時經(jīng)過運維得知 MySQL 那臺服務(wù)器的 IO 壓力也比較大。
所以這個原因也比較明顯了:
由于每消費一條數(shù)據(jù)都要去查詢一次數(shù)據(jù)庫,MySQL 本身壓力就比較大,加上數(shù)據(jù)量也很高所以導(dǎo)致這個 IO 響應(yīng)較慢,導(dǎo)致整個任務(wù)處理的就比較慢了。
但還有一個原因也不能忽視;由于所有的業(yè)務(wù)線程在某個時間點都進入了死循環(huán),根本沒有執(zhí)行完任務(wù)的機會,而后面的數(shù)據(jù)還在源源不斷的進入,所以這個隊列只會越堆越多!
這其實是一個老應(yīng)用了,可能會有人問為什么之前沒出現(xiàn)問題。
這是因為之前數(shù)據(jù)量都比較少,即使是并發(fā)寫入也沒有出現(xiàn)并發(fā)擴容形成環(huán)形鏈表的情況。這段時間業(yè)務(wù)量的暴增正好把這個隱藏的雷給揪出來了。所以還是得信墨菲他老人家的話。
總結(jié)至此整個排查結(jié)束,而我們后續(xù)的調(diào)整措施大概如下:
HashSet 不是線程安全的,換為 ConcurrentHashMap同時把 value 寫死一樣可以達到 set 的效果。
根據(jù)我們后面的監(jiān)控,初始化 ConcurrentHashMap 的大小盡量大一些,避免頻繁的擴容。
MySQL 中很多數(shù)據(jù)都已經(jīng)不用了,進行冷熱處理。盡量降低單表數(shù)據(jù)量。同時后期考慮分表。
查數(shù)據(jù)那里調(diào)整為查緩存,提高查詢效率。
線程池的名稱一定得取的有意義,不然是自己給自己增加難度。
根據(jù)監(jiān)控將線程池的隊列大小調(diào)整為一個具體值,并且要有拒絕策略。
升級到 JDK1.8。
再一個是報警郵件酌情考慮為電話通知
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/72073.html
摘要:線程啟動規(guī)則對象的方法先行發(fā)生于此線程的每一個動作。所以局部變量是不被多個線程所共享的,也就不會出現(xiàn)并發(fā)問題。通過獲取到數(shù)據(jù),放入當前線程處理完之后將當前線程中的信息移除。主線程必須在啟動其他線程后立即調(diào)用方法。 一、線程安全性 定義:當多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式,或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行...
摘要:語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點。有針對不同系統(tǒng)的特定實現(xiàn),,,目的是使用相同的字節(jié)碼,它們都會給出相同的結(jié)果。項目主要基于捐贈的源代碼。 本文來自于我的慕課網(wǎng)手記:Java編程中那些再熟悉不過的知識點,轉(zhuǎn)載請保留鏈接 ;) 1. 面向?qū)ο蠛兔嫦蜻^程的區(qū)別 面向過程 優(yōu)點: 性能比面向?qū)ο蟾?。因為類調(diào)用時需要實例...
摘要:中的使用及在中的沖突方案引言簡稱是在作為的替代選擇新引入的,是包的重要成員。為了解決在頻繁沖突時性能降低的問題,中使用平衡樹來替代鏈表存儲沖突的元素。目前,只有和會在頻繁沖突的情況下使用平衡樹。 java中ConcurrentHashMap的使用及在Java 8中的沖突方案 1、引言 ConcurrentHashMap(簡稱CHM)是在Java 1.5作為Hashtable的替代選擇新...
摘要:減少鎖的持有時間降低發(fā)生競爭可能性的一種有效方式就是盡可能縮短鎖的持有時間。代替獨占鎖第三種降低競爭鎖的影響的技術(shù)就是放棄使用獨占鎖,從而有助于使用一種友好并發(fā)的方式來管理共享狀態(tài)。 序 本文介紹一下提升并發(fā)可伸縮性的一些方式:減少鎖的持有時間,降低鎖的粒度,鎖分段、避免熱點域以及采用非獨占的鎖或非阻塞鎖來代替獨占鎖。 減少鎖的持有時間 降低發(fā)生競爭可能性的一種有效方式就是盡可能縮短鎖...
摘要:把內(nèi)存分成兩種,一種叫做棧內(nèi)存,一種叫做堆內(nèi)存在函數(shù)中定義的一些基本類型的變量和對象的引用變量都是在函數(shù)的棧內(nèi)存中分配。堆內(nèi)存用于存放由創(chuàng)建的對象和數(shù)組。 一次慘痛的阿里技術(shù)面 就在昨天,有幸接到了阿里的面試通知,本來我以為自己的簡歷應(yīng)該不會的到面試的機會了,然而機會卻這么來了,我卻沒有做好準備,被面試官大大一通血虐。因此,我想寫點東西紀念一下這次的經(jīng)歷,也當一次教訓(xùn)了。其實面試官大大...
閱讀 687·2021-10-09 09:41
閱讀 672·2019-08-30 15:53
閱讀 1099·2019-08-30 15:53
閱讀 1235·2019-08-30 11:01
閱讀 1595·2019-08-29 17:31
閱讀 1018·2019-08-29 14:05
閱讀 1746·2019-08-29 12:49
閱讀 433·2019-08-28 18:17