您好,登錄后才能下訂單哦!
為啥要用HahSet?
假如我們現在想要在一大堆數據中查找X數據。LinkedList的數據結構就不說了,查找效率低的可怕。ArrayList哪,如果我們不知道X的位置序號,還是一樣要全部遍歷一次直到查到結果,效率一樣可怕。HashSet天生就是為了提高查找效率的。
背景
上午剛到公司,準備開始一天的摸魚之旅時突然收到了一封監控中心的郵件。
心中暗道不好,因為監控系統從來不會告訴我應用完美無 bug,其實系統挺猥瑣。
打開郵件一看,果然告知我有一個應用的線程池隊列達到閾值觸發了報警。
由于這個應用出問題非常影響用戶體驗;于是立馬讓運維保留現場 dump 線程和內存同時重啟應用,還好重啟之后恢復正常。于是開始著手排查問題。
分析
首先了解下這個應用大概是做什么的。
簡單來說就是從 MQ 中取出數據然后丟到后面的業務線程池中做具體的業務處理。
而報警的隊列正好就是這個線程池的隊列。
跟蹤代碼發現構建線程池的方式如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());; put(poolName,executor);
采用的是默認的 LinkedBlockingQueue 并沒有指定大小(這也是個坑),于是這個隊列的默認大小為 Integer.MAX_VALUE。
由于應用已經重啟,只能從僅存的線程快照和內存快照進行分析。
內存分析
先利用 MAT 分析了內存,的到了如下報告。
其中有兩個比較大的對象,一個就是之前線程池存放任務的 LinkedBlockingQueue,還有一個則是 HashSet。
當然其中隊列占用了大量的內存,所以優先查看,HashSet 一會兒再看。
由于隊列的大小給的夠大,所以結合目前的情況來看應當是線程池里的任務處理較慢,導致隊列的任務越堆越多,至少這是目前可以得出的結論。
線程分析
再來看看線程的分析,這里利用fastthread.io 這個網站進行線程分析。
因為從表現來看線程池里的任務遲遲沒有執行完畢,所以主要看看它們在干嘛。
正好他們都處于 RUNNABLE 狀態,同時堆棧如下:
發現正好就是在處理上文提到的 HashSet,看這個堆棧是在查詢 key 是否存在。通過查看 312 行的業務代碼確實也是如此。
這里的線程名字也是個坑,讓我找了好久。
定位
分析了內存和線程的堆棧之后其實已經大概猜出一些問題了。
這里其實有一個前提忘記講到:
這個告警是凌晨三點發出的郵件,但并沒有電話提醒之類的,所以大家都不知道。
到了早上上班時才發現并立即 dump 了上面的證據。
所有有一個很重要的事實:這幾個業務線程在查詢 HashSet 的時候運行了 6 7 個小時都沒有返回。
通過之前的監控曲線圖也可以看出:
操作系統在之前一直處于高負載中,直到我們早上看到報警重啟之后才降低。
同時發現這個應用生產上運行的是 JDK1.7 ,所以我初步認為應該是在查詢 key 的時候進入了 HashMap 的環形鏈表導致 CPU 高負載同時也進入了死循環。
為了驗證這個問題再次 review 了代碼。
整理之后的偽代碼如下:
//線程池 private ExecutorService executor; private Set<String> set = new hashSet(); private void execute(){ while(true){ //從 MQ 中獲取數據 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)){ //數據庫查詢 if(queryDB(key)){ set.add(key); return; } } //達到某種條件時清空 set if(flag){ set = null ; } } }
大致的流程如下:
這里有一個很明顯的問題,那就是作為共享資源的 Set 并沒有做任何的同步處理。
這里會有多個線程并發的操作,由于 HashSet 其實本質上就是 HashMap,所以它肯定是線程不安全的,所以會出現兩個問題:
第一個問題相對于第二個還能接受。
通過上文的內存分析我們已經知道這個 set 中的數據已經不少了。同時由于初始化時并沒有指定大小,僅僅只是默認值,所以在大量的并發寫入時候會導致頻繁的擴容,而在 1.7 的條件下又可能會形成環形鏈表。
不巧的是代碼中也有查詢操作(contains()),觀察上文的堆棧情況:
發現是運行在 HashMap 的 465 行,來看看 1.7 中那里具體在做什么:
已經很明顯了。這里在遍歷鏈表,同時由于形成了環形鏈表導致這個 e.next 永遠不為空,所以這個循環也不會退出了。
到這里其實已經找到問題了,但還有一個疑問是為什么線程池里的任務隊列會越堆越多。我第一直覺是任務執行太慢導致的。
仔細查看了代碼發現只有一個地方可能會慢:也就是有一個數據庫的查詢。
把這個 SQL 拿到生產環境執行發現確實不快,查看索引發現都有命中。
但我一看表中的數據發現已經快有 7000W 的數據了。同時經過運維得知 MySQL 那臺服務器的 IO 壓力也比較大。
所以這個原因也比較明顯了:
由于每消費一條數據都要去查詢一次數據庫,MySQL 本身壓力就比較大,加上數據量也很高所以導致這個 IO 響應較慢,導致整個任務處理的就比較慢了。
但還有一個原因也不能忽視;由于所有的業務線程在某個時間點都進入了死循環,根本沒有執行完任務的機會,而后面的數據還在源源不斷的進入,所以這個隊列只會越堆越多!
這其實是一個老應用了,可能會有人問為什么之前沒出現問題。
這是因為之前數據量都比較少,即使是并發寫入也沒有出現并發擴容形成環形鏈表的情況。這段時間業務量的暴增正好把這個隱藏的雷給揪出來了。所以還是得信墨菲他老人家的話。
總結
至此整個排查結束,而我們后續的調整措施大概如下:
HashMap 的死循環問題在網上層出不窮,沒想到還真被我遇到了。現在要滿足這個條件還是挺少見的,比如 1.8 以下的 JDK 這一條可能大多數人就碰不到,正好又證實了一次墨菲定律。
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。