一個執行緒罷工的詭異事件
之前和crossoverJie一起排查了一個線上問題,他將其總結了一下,這裡我做下轉發。這個問題其實是因為一些編碼的規範和對一些新的框架的不熟悉造成的,整個排查過程大家可以學習一下。
背景
事情(事故)是這樣的,突然收到報警,線上某個應用裡業務邏輯沒有執行,導致的結果是資料庫裡的某些資料沒有更新。
雖然是前人寫的程式碼,但作為 Bugmaker&killer
只能咬著牙上了。
因為之前沒有接觸過出問題這塊的邏輯,所以簡單理了下如圖:
有一個生產執行緒一直源源不斷的往佇列寫資料。
消費執行緒也一直不停的取出資料後寫入後續的業務執行緒池。
業務執行緒池裡的執行緒會對每個任務進行入庫操作。
整個過程還是比較清晰的,就是一個典型的生產者消費者模型。
嘗試定位
接下來便是嘗試定位這個問題,首先例行檢查了以下幾項:
是否記憶體有記憶體溢位?
應用 GC 是否有異常?
透過日誌以及監控發現以上兩項都是正常的。
緊接著便 dump 了執行緒快照檢視業務執行緒池中的執行緒都在幹啥。
結果發現所有業務執行緒池都處於 waiting
狀態,佇列也是空的。
同時生產者使用的佇列卻已經滿了,沒有任何消費跡象。
結合上面的流程圖不難發現應該是消費佇列的 Consumer
出問題了,導致上游的佇列不能消費,下有的業務執行緒池沒事可做。
review 程式碼
於是檢視了消費程式碼的業務邏輯,同時也發現消費執行緒是一個單執行緒。
結合之前的執行緒快照,我發現這個消費執行緒也是處於 waiting 狀態,和後面的業務執行緒池一模一樣。
他做的事情基本上就是對訊息解析,之後丟到後面的業務執行緒池中,沒有發現什麼特別的地方。
但是由於裡面的分支特別多(switch case),看著有點頭疼;所以我與寫這個業務程式碼的同學溝通後他告訴我確實也只是入口處解析了一下資料,後續所有的業務邏輯都是丟到執行緒池中處理的,於是我便帶著這個前提去排查了(埋下了伏筆)。
因為這裡消費的佇列其實是一個 disruptor
佇列;它和我們常用的 BlockQueue
不太一樣,不是由開發者自定義一個消費邏輯進行處理的;而是在初始化佇列時直接丟一個執行緒池進去,它會在內部使用這個執行緒池進行消費,同時回撥一個方法,在這個方法裡我們寫自己的消費邏輯。
所以對於開發者而言,這個消費邏輯其實是一個黑盒。
於是在我反覆 review
了消費程式碼中的資料解析邏輯發現不太可能出現問題後,便開始瘋狂懷疑是不是 disruptor
自身的問題導致這個消費執行緒罷工了。
再翻了一陣 disruptor
的原始碼後依舊沒發現什麼問題後我諮詢對 disruptor
較熟的@咖啡拿鐵,在他的幫助下在本地模擬出來和生產一樣的情況。
本地模擬
本地也是建立了一個單執行緒的執行緒池,分別執行了兩個任務。
第一個任務沒啥好說的,就是簡單的列印。
第二個任務會對一個數進行累加,加到 10 之後就丟擲一個未捕獲的異常。
接著我們來執行一下。
發現當任務中丟擲一個沒有捕獲的異常時,執行緒池中的執行緒就會處於 waiting
狀態,同時所有的堆疊都和生產相符。
細心的朋友會發現正常執行的執行緒名稱和異常後處於 waiting 狀態的執行緒名稱是不一樣的,這個後續分析。
解決問題
當加入異常捕獲後又如何呢?
程式肯定會正常執行。
同時會發現所有的任務都是由一個執行緒完成的。
雖說就是加了一行程式碼,但我們還是要搞清楚這裡面的門門道道。
原始碼分析
於是只有直接 debug
執行緒池的原始碼最快了;
透過剛才的異常堆疊我們進入到 ThreadPoolExecutor.java:1142
處。
發現執行緒池已經幫我們做了異常捕獲,但依然會往上拋。
在
finally
塊中會執行processWorkerExit(w,completedAbruptly)
方法。
看過之前《如何優雅的使用和理解執行緒池》的朋友應該還會有印象。
執行緒池中的任務都會被包裝為一個內部 Worker
物件執行。
processWorkerExit
可以簡單的理解為是把當前執行的執行緒銷燬( workers.remove(w)
)、同時新增( addWorker()
)一個 Worker
物件接著處理;
就像是哪個零件壞掉後重新換了一個新的接著工作,但是舊零件負責的任務就沒有了。
接下來看看 addWorker()
做了什麼事情:
只看這次比較關心的部分;新增成功後會直接執行他的 start()
的方法。
由於 Worker
實現了 Runnable
介面,所以本質上就是呼叫了 runWorker()
方法。
在 runWorker()
其實就是上文 ThreadPoolExecutor
丟擲異常時的那個方法。
它會從佇列裡一直不停的獲取待執行的任務,也就是 getTask()
;在 getTask
也能看出它會一直從內建的佇列取出任務。
而一旦佇列是空的,它就會 waiting
在 workQueue.take()
,也就是我們從堆疊中發現的 1067 行程式碼。
執行緒名字的變化
上文還提到了異常後的執行緒名稱發生了改變,其實在 addWorker()
方法中可以看到 newWorker()
時就會重新命名執行緒的名稱,預設就是把字尾的計數+1。
這樣一切都能解釋得通了,真相只有一個:
在單個執行緒的執行緒池中一但丟擲了未被捕獲的異常時,執行緒池會回收當前的執行緒並建立一個新的
Worker
; 它也會一直不斷的從佇列裡獲取任務來執行,但由於這是一個消費執行緒,根本沒有生產者往裡邊丟任務,所以它會一直 waiting 在從佇列裡獲取任務處,所以也就造成了線上的佇列沒有消費,業務執行緒池沒有執行的問題。
總結
所以之後線上的那個問題加上異常捕獲之後也變得正常了,但我還是有點納悶的是:
既然後續所有的任務都是線上程池中執行的,也就是純非同步了,那即便是出現異常也不會拋到消費執行緒中啊。
這不是把我之前儲備的知識點推翻了嘛?不信邪!之後我讓運維給了加上異常捕獲後的線上錯誤日誌。
結果發現在上文提到的眾多 switchcase
中,最後一個竟然是直接操作的資料庫,導致一個非空欄位報錯了?!!
這事也給我個教訓,還是得眼見為實啊。
雖然這個問題改動很小解決了,但覆盤整個過程還是有許多需要改進的:
消費佇列的執行緒名稱竟然和業務執行緒的字首一樣,導致我光找它就花了許多時間,命名必須得調整。
開發規範,防禦式程式設計大家需要養成習慣。
未知的技術棧需要謹慎,比如
disruptor
,之前的團隊應該只是看了個高效能的介紹就直接使用,並沒有深究其原理;導致出現問題後對它拿不準。
例項程式碼:
。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555607/viewspace-2638296/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 執行緒同步的詭異:求指點執行緒
- 詭異的JS非同步單執行緒是如何工作的JS非同步執行緒
- 一個詭異的 Pulsar InterruptedException 異常Exception
- 多執行緒執行順序詭異現象談,你不知道的pthread_create執行緒thread
- java啟動執行緒時 extends與implements的一個差異Java執行緒
- c#關於同步 /異常/多執行緒/事件 事例C#執行緒事件
- 加入一個執行緒執行緒
- 一個看似詭異的Oracle連線問題Oracle
- 怎樣停止一個正在執行的執行緒執行緒
- 怪異的COM 執行緒模型執行緒模型
- 構建一個基於事件分發驅動的EventLoop執行緒模型事件OOP執行緒模型
- 執行緒池OOM異常執行緒OOM
- windows多執行緒同步--事件Windows執行緒事件
- Win32執行緒——等待另一個執行緒結束Win32執行緒
- 【DBAplus】深入Oracle優化器:一條詭異執行計劃的解決之道Oracle優化
- 【Java面試】如何中斷一個正在執行的執行緒?Java面試執行緒
- 什麼?一個核同時執行兩個執行緒?執行緒
- 畫江湖之 PHP 多執行緒開發 【建立一個新的執行緒】PHP執行緒
- 畫江湖之 PHP 多執行緒開發 [建立一個新的執行緒]PHP執行緒
- 一個SystemC執行緒與SystemVerilog執行緒通訊的例子執行緒
- 如何優雅的停止一個執行緒?執行緒
- thread 描述執行緒的一個類thread執行緒
- Java NIO 執行緒 的一個問題Java執行緒
- 一個多執行緒的PushbackInputStream問題執行緒
- vmware平臺下兩次網路不通的詭異事件事件
- 執行緒同步(windows平臺):事件執行緒Windows事件
- (轉)Qt 的執行緒與事件迴圈QT執行緒事件
- MySQL update一個詭異現象的分析--個人未分析出MySql
- disruptor如何實現每CPU執行一個執行緒?執行緒
- C#多執行緒學習(二) 如何操縱一個執行緒C#執行緒
- C# 多執行緒學習(2) :如何操縱一個執行緒C#執行緒
- 一個使用執行緒計數器的例子執行緒
- 請教一個多執行緒的問題執行緒
- 執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐執行緒
- Java執行緒池一:執行緒基礎Java執行緒
- 多執行緒系列(一):認識執行緒執行緒
- 將建立執行緒的API-pthread_create封裝成一個執行緒類執行緒APIthread封裝
- 我們該如何正確的中斷一個執行緒的執行??執行緒