生产问题:一个线程罢工的诡异事件
事情(事故)是這樣的,突然收到報警,線上某個應(yīng)用里業(yè)務(wù)邏輯沒有執(zhí)行,導(dǎo)致的結(jié)果是數(shù)據(jù)庫里的某些數(shù)據(jù)沒有更新。
雖然是前人寫的代碼,但作為 Bugmaker&killer 只能咬著牙上了。
因為之前沒有接觸過出問題這塊的邏輯,所以簡單理了下如圖:
?
整個過程還是比較清晰的,就是一個典型的生產(chǎn)者消費(fèi)者模型。
嘗試定位
接下來便是嘗試定位這個問題,首先例行檢查了以下幾項:
- 是否內(nèi)存有內(nèi)存溢出?
- 應(yīng)用 GC 是否有異常?
通過日志以及監(jiān)控發(fā)現(xiàn)以上兩項都是正常的。
緊接著便 dump 了線程快照查看業(yè)務(wù)線程池中的線程都在干啥。
?
結(jié)果發(fā)現(xiàn)所有業(yè)務(wù)線程池都處于 waiting 狀態(tài),隊列也是空的。
同時生產(chǎn)者使用的隊列卻已經(jīng)滿了,沒有任何消費(fèi)跡象。
結(jié)合上面的流程圖不難發(fā)現(xiàn)應(yīng)該是消費(fèi)隊列的 Consumer 出問題了,導(dǎo)致上游的隊列不能消費(fèi),下有的業(yè)務(wù)線程池沒事可做。
review 代碼
于是查看了消費(fèi)代碼的業(yè)務(wù)邏輯,同時也發(fā)現(xiàn)消費(fèi)線程是一個單線程。
?
結(jié)合之前的線程快照,我發(fā)現(xiàn)這個消費(fèi)線程也是處于 waiting 狀態(tài),和后面的業(yè)務(wù)線程池一模一樣。
他做的事情基本上就是對消息解析,之后丟到后面的業(yè)務(wù)線程池中,沒有發(fā)現(xiàn)什么特別的地方。
但是由于里面的分支特別多(switch case),看著有點(diǎn)頭疼;所以我與寫這個業(yè)務(wù)代碼的同學(xué)溝通后他告訴我確實(shí)也只是入口處解析了一下數(shù)據(jù),后續(xù)所有的業(yè)務(wù)邏輯都是丟到線程池中處理的,于是我便帶著這個前提去排查了(埋下了伏筆)。
因為這里消費(fèi)的隊列其實(shí)是一個 disruptor 隊列;它和我們常用的 BlockQueue不太一樣,不是由開發(fā)者自定義一個消費(fèi)邏輯進(jìn)行處理的;而是在初始化隊列時直接丟一個線程池進(jìn)去,它會在內(nèi)部使用這個線程池進(jìn)行消費(fèi),同時回調(diào)一個方法,在這個方法里我們寫自己的消費(fèi)邏輯。
所以對于開發(fā)者而言,這個消費(fèi)邏輯其實(shí)是一個黑盒。
于是在我反復(fù) review 了消費(fèi)代碼中的數(shù)據(jù)解析邏輯發(fā)現(xiàn)不太可能出現(xiàn)問題后,便開始瘋狂懷疑是不是 disruptor 自身的問題導(dǎo)致這個消費(fèi)線程罷工了。
再翻了一陣 disruptor 的源碼后依舊沒發(fā)現(xiàn)什么問題后我咨詢對 disruptor 較熟的@咖啡拿鐵,在他的幫助下在本地模擬出來和生產(chǎn)一樣的情況。
本地模擬
?
?
本地也是創(chuàng)建了一個單線程的線程池,分別執(zhí)行了兩個任務(wù)。
- 第一個任務(wù)沒啥好說的,就是簡單的打印。
- 第二個任務(wù)會對一個數(shù)進(jìn)行累加,加到 10 之后就拋出一個未捕獲的異常。
接著我們來運(yùn)行一下。
?
?
發(fā)現(xiàn)當(dāng)任務(wù)中拋出一個沒有捕獲的異常時,線程池中的線程就會處于 waiting 狀態(tài),同時所有的堆棧都和生產(chǎn)相符。
細(xì)心的朋友會發(fā)現(xiàn)正常運(yùn)行的線程名稱和異常后處于 waiting 狀態(tài)的線程名稱是不一樣的,這個后續(xù)分析。
解決問題
?
當(dāng)加入異常捕獲后又如何呢?
?
程序肯定會正常運(yùn)行。
同時會發(fā)現(xiàn)所有的任務(wù)都是由一個線程完成的。
雖說就是加了一行代碼,但我們還是要搞清楚這里面的門門道道。
源碼分析
于是只有直接 debug 線程池的源碼最快了;
?
?
通過剛才的異常堆棧我們進(jìn)入到 ThreadPoolExecutor.java:1142 處。
- 發(fā)現(xiàn)線程池已經(jīng)幫我們做了異常捕獲,但依然會往上拋。
- 在 finally 塊中會執(zhí)行 processWorkerExit(w,completedAbruptly) 方法。
?
看過之前《如何優(yōu)雅的使用和理解線程池》的朋友應(yīng)該還會有印象。
線程池中的任務(wù)都會被包裝為一個內(nèi)部 Worker 對象執(zhí)行。
processWorkerExit 可以簡單的理解為是把當(dāng)前運(yùn)行的線程銷毀( workers.remove(w))、同時新增( addWorker())一個 Worker 對象接著處理;
就像是哪個零件壞掉后重新?lián)Q了一個新的接著工作,但是舊零件負(fù)責(zé)的任務(wù)就沒有了。
接下來看看 addWorker() 做了什么事情:
?
只看這次比較關(guān)心的部分;添加成功后會直接執(zhí)行他的 start() 的方法。
?
由于 Worker 實(shí)現(xiàn)了 Runnable 接口,所以本質(zhì)上就是調(diào)用了 runWorker() 方法。
在 runWorker() 其實(shí)就是上文 ThreadPoolExecutor 拋出異常時的那個方法。
?
?
它會從隊列里一直不停的獲取待執(zhí)行的任務(wù),也就是 getTask();在 getTask 也能看出它會一直從內(nèi)置的隊列取出任務(wù)。
而一旦隊列是空的,它就會 waiting 在 workQueue.take(),也就是我們從堆棧中發(fā)現(xiàn)的 1067 行代碼。
線程名字的變化
?
?
?
上文還提到了異常后的線程名稱發(fā)生了改變,其實(shí)在 addWorker() 方法中可以看到 newWorker()時就會重新命名線程的名稱,默認(rèn)就是把后綴的計數(shù)+1。
這樣一切都能解釋得通了,真相只有一個:
在單個線程的線程池中一但拋出了未被捕獲的異常時,線程池會回收當(dāng)前的線程并創(chuàng)建一個新的 Worker;
它也會一直不斷的從隊列里獲取任務(wù)來執(zhí)行,但由于這是一個消費(fèi)線程,根本沒有生產(chǎn)者往里邊丟任務(wù),所以它會一直 waiting 在從隊列里獲取任務(wù)處,所以也就造成了線上的隊列沒有消費(fèi),業(yè)務(wù)線程池沒有執(zhí)行的問題。
總結(jié)
所以之后線上的那個問題加上異常捕獲之后也變得正常了,但我還是有點(diǎn)納悶的是:
既然后續(xù)所有的任務(wù)都是在線程池中執(zhí)行的,也就是純異步了,那即便是出現(xiàn)異常也不會拋到消費(fèi)線程中啊。
這不是把我之前儲備的知識點(diǎn)推翻了嘛?不信邪!之后我讓運(yùn)維給了加上異常捕獲后的線上錯誤日志。
結(jié)果發(fā)現(xiàn)在上文提到的眾多 switchcase 中,最后一個竟然是直接操作的數(shù)據(jù)庫,導(dǎo)致一個非空字段報錯了!!
這事也給我個教訓(xùn),還是得眼見為實(shí)啊。
雖然這個問題改動很小解決了,但復(fù)盤整個過程還是有許多需要改進(jìn)的:
轉(zhuǎn)載于:https://www.cnblogs.com/CQqf2019/p/11021946.html
總結(jié)
以上是生活随笔為你收集整理的生产问题:一个线程罢工的诡异事件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HailStone序列
- 下一篇: Hibernate save, save