当前位置: 首页>C语言>正文

生產問題:一個線程罷工的詭異事件

生產問題:一個線程罷工的詭異事件

事情(事故)是這樣的,突然收到報警,線上某個應用里業務邏輯沒有執行,導致的結果是數據庫里的某些數據沒有更新。

雖然是前人寫的代碼,但作為 Bugmaker&killer 只能咬著牙上了。

因為之前沒有接觸過出問題這塊的邏輯,所以簡單理了下如圖:

生產問題:一個線程罷工的詭異事件

?

  1. 有一個生產線程一直源源不斷的往隊列寫數據。
  2. 消費線程也一直不停的取出數據后寫入后續的業務線程池。
  3. 業務線程池里的線程會對每個任務進行入庫操作。

整個過程還是比較清晰的,就是一個典型的生產者消費者模型。

嘗試定位

接下來便是嘗試定位這個問題,首先例行檢查了以下幾項:

  • 是否內存有內存溢出?
  • 應用 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 中,最后一個竟然是直接操作的數據庫,導致一個非空字段報錯了!!

這事也給我個教訓,還是得眼見為實啊。

雖然這個問題改動很小解決了,但復盤整個過程還是有許多需要改進的:

  1. 消費隊列的線程名稱竟然和業務線程的前綴一樣,導致我光找它就花了許多時間,命名必須得調整。
  2. 開發規范,防御式編程大家需要養成習慣。
  3. 未知的技術棧需要謹慎,比如 disruptor,之前的團隊應該只是看了個高性能的介紹就直接使用,并沒有深究其原理;導致出現問題后對它拿不準。

轉載于:https://www.cnblogs.com/CQqf2019/p/11021946.html

https://www.zydui.com/af07bUG8CDQ9TBF8G.html
>

相关文章:

  • 詳解PyTorch中的contiguous
  • PyTorch中的contiguous解讀
  • Pytorch中contiguous()函數理解
  • ios自定義UITabBar-仿寫掌上英雄聯盟的UITabBar
  • 基于Cocos2d-x的英雄聯盟皮膚選擇菜單
  • lol-登陸英雄聯盟出錯
  • JS中雙層for循環執行順序
  • 關于for循環執行順序
  • 上古卷軸5boss計算機丟失,上古卷軸5常見BUG解決辦法
  • 上古世紀服務器維護真情禮,4月9日例行維護懷舊服合服公告
  • 塔羅牌張數
  • 工程管理中的工程技術
  • 銳派出品:LOL新年特輯S4各類細節之下路篇
  • 藍城兄弟Q4業績背后,垂直社區具備多少想象力?
  • Mac OS啟動服務優化高級篇(launchd tuning)
  • #Geek Point# 為什么現在要去印度看一看?
  • vm 流程運行mac os_什么是“商務”流程,為什么在我的Mac上運行?
  • # 陌生人社交產品:需求、困境與破局之道
  • mac 不受信任在哪里更改_什么是受信任的,為什么它可以在Mac上運行?
  • 車行軌跡分類實踐
  • 智慧車行預約小程序 v9.1
  • i12藍牙耳機充電倉怎么看充滿電_車行藍牙耳機價格高性價比的選擇
  • 車行平安
  • 論文閱讀——《基于卷積神經網絡的車行環境多類障礙物檢測與識別》
  • 飛槳開發者創意薈:PaddleHub一鍵部署,AI創意實現原來如此簡單
  • eclipse左側欄目即包資源管理器怎么打開
  • 卷毛機器人符文_卷毛S6娜美輔助天賦 娜美輔助符文天賦S6最新
  • 天賦介紹
  • 蘋果怎么沒有4g信號還無服務器,不顯示4g信號怎么回事?蘋果手機不顯示4g信號的解決方法...
  • c4D體積生成和Quad Remesher重新拓撲減面插件