案例分析|執行緒池相關故障梳理&總結

帶你聊技術發表於2024-01-05

  本文作者梳理和分享了執行緒池類的故障,分別從故障視角和技術視角兩個角度來分析總結,故障視角可以看到現象和教訓,而技術視角可以透過現象看到本質更進一步可以看看如何避免。

   背景

  團隊新同學反饋想學習瞭解執行緒池類的故障,由筆者做梳理和分享(所梳理的故障材料來自團隊多年積累的故障覆盤報告),內容對外部開發者來說也有借鑑意義,因此發出來希望能幫助到一些開發者。

  我會從故障視角和技術視角兩個角度來分析總結,故障視角可以看到現象和血淋淋的教訓,而技術視角可以透過現象看到本質更進一步可以看看如何避免。

   故障視角

  筆者在管控團隊耕耘多年,經歷了很多大大小小的故障,總結來看確實有很多執行緒池滿導致服務不可用的故障。一般執行緒池滿只是結果,誘因還是系統某個地方慢了,最典型的一類 Case 就是資料庫 SQL 慢導致資料庫連線池滿,資料庫連線池滿進而導致對外提供服務的業務執行緒池(如 Dubbo 執行緒池)滿,執行緒池一旦滿了,就大機率無法響應新的請求,或者能響應新的請求但一直在排隊無法及時處理請求導致請求耗時增加,在使用者側看來就變成了超時-服務不可用。

  下面貼一些典型的常見 Case,開發同學基本一看就懂並不神奇。

  資料庫相關

  熱更新

  在事務裡熱更新同一條資料容易引發鎖等待造成慢 SQL,常見於一些 update count,update quota 類的業務場景。

  故障案例1:某次壓測對 DB 產生瞬時 60w+ QPS 的壓力,期間同一條資料(更新 count 欄位)在事務裡大量熱點更新導致了行鎖爭搶產生慢 SQL。

  故障案例2:幾個大使用者高併發操作,其中涉及單條熱點資料在事務裡的更新,排查發現單次更新耗時高達5-6秒,積壓的執行緒引起 Dubbo 對外服務執行緒池堆積,最終執行緒池滿導致無法對外服務。

  線下模擬測試發現 1200 併發進行熱點資料的更新(在特定的資料庫版本和配置下),開啟事務需要1分鐘,不開啟事務需要3秒。

  大表加欄位

  DDL 變更有多種方式,最原始的方式會造成鎖表問題進而引發大量相關聯 SQL 鎖等待產生慢 SQL;DDL 變更建議走 Online DDL。歷史上出現過的一些鎖表的 Case 應該是沒有走 Online DDL,也可能當時資料庫版本不支援 Online DDL。

  故障案例:大表新增欄位未採用 Online DDL,在最後階段會對錶加 Metadata Lock 原子鎖,使得大量相關 SQL 鎖等待產生慢 SQL,進而快速打滿應用執行緒池。

  索引沒走對(走了主鍵全表掃描)

  常見於 order by id limit 場景,就算 where 條件裡的欄位有索引還是有可能走全表掃描。可以透過 IGNORE INDEX(PRIMARY),FORCE INDEX(idx_xxx) 等方式來解決。

  故障案例:凌晨 3 點多突然收到報警資料庫 CPU 100%,排查發現某查詢 SQL 走了主鍵索引觸發了全表掃描(SQL 樣例為:where a= and b= and c= and d= order by id desc limit 20,當時只有 idx_a_b_e 的聯合索引),期間在資料庫運維平臺手工無差別限流 SQL 有所緩解但很快 CPU 又會飈上來,也嘗試了物理刪除一些無效資料減少資料量,多管齊下,最後透過臨時增加一個 idx_a_b_c_d 新的全欄位覆蓋的索引止血。

  深分頁

  資料量大時深分頁引發慢 SQL 也是個常見的經典問題。解法可以是使用 NexToken 或者叫遊標的方式查詢,目前阿里雲有很多 OpenAPI 已經提供了 NextToken 的查詢方式。

  故障案例:某賬號(資料量巨大)呼叫某查詢介面分頁查詢引發慢 SQL 導致資料庫連線池滿進而導致 Dubbo 執行緒池滿無法對外服務,緊急限流該賬號對該介面的呼叫後恢復。

  呼叫量大

  故障案例1:故障恢復後,短時間重試待處理任務到單機執行,量太大導致單機執行緒池滿導致服務受損。

  解法:系統層面需要做一定的限流策略,單機任務瓶頸時應切換到網格型任務。

  故障案例2:壓測未預熱,直接一次性併發到壓測值導致執行緒池滿,導致資料庫有很多事務等待的慢 SQL。

  解法:壓測應按照一定節奏逐步上量,觀察系統負載並及時暫定,而不是開局就決戰。

  其他

  故障案例:查詢沒加 Limit 導致應用 Full GC

  該 Case 不涉及執行緒池滿問題,但筆者覺得有一定的代表性因此也分享下。不管是查詢還是刪除還是更新資料,不管是程式碼還是日常的 SQL 訂正,建議都增加 Limit 來兜底保護自己,縮小影響面。

   技術視角

  執行緒池類的故障,一般都是某個地方慢了堵了,從技術角度大多是:

  1、遠端呼叫 IO 慢導致耗時增加;

  2、計算密集型應用 CPU 飆升導致耗時增加;

  3、自定義業務執行緒池滿造成排隊等待導致耗時增加;

  其中 2 不算常見,筆者也遇到過,發生於某 CPU 密集運算的應用系統,突增的高併發請求引起 CPU 100%;其中 1 比較常見,一般遠端呼叫有:Dubbo、Http、DB、Redis,這些實踐中都會使用連線池來與遠端服務互動,凡是連線池都是有共性的,有兩個需要關注的點:

  1、儘量減少遠端呼叫本身的 超時時間 以實現 fast-fail 快速失敗。一般是設定 ConnectionTimeout 即握手時間 和 SocketTimeout 即業務執行超時時間。

  2、在連線池滿了以後,獲取新的連線的 超時時間 也需要設定的小一些以實現 fast-fail 快速失敗,這個是很容易忽略的一個點。如 Druid 裡設定 MaxWait,Http 連線池裡設定 ConnectionRequestTimeout。

  下面列一下各個連線池需要關注的點。

  Dubbo 執行緒池

  1、執行緒池做好隔離,避免互相影響

  如內部運維介面和對外服務的介面做隔離。

  對外服務裡核心介面和非核心介面做隔離。

  2、Dubbo consumer 側設定 timeout,根據 fast-fail 理念設定的越小越好;provider 側的 timeout 僅僅是起到宣告的效果供 consumer 參考,無實際超時殺執行緒的作用。

  Http 連線池

  1、設定 ConnectTimeout、SocketTimeout、ConnectionRequestTimeout

  故障案例:某次釋出的程式碼引入了一個 SDK,該 SDK 整合了 HttpClient,但並沒有設定 ConnectionTimeout,在某次網路抖動發生時,Http 連線池被迅速打滿,進而導致業務執行緒池滿導致服務受損。

  2、DefaultMaxPerRoute 太小也容易導致阻塞。

  故障案例:某 SDK 預設設定的 128,在某次壓測中發現客戶端耗時較高,但服務端耗時並無波動,排查後懷疑是 DefaultMaxPerRoute 太小導致的阻塞,調大後問題解決。

  資料庫連線池 Druid

  1、設定 ConnectTimeout、SocketTimeout。

  故障案例:凌晨 1 點多收到 API 成功率降低報警,排查發現部分 SQL 執行超時,原因是資料庫發生了主備切換,進一步排查發現應用側對資料庫連線池沒有設定 SocketTimeout 導致切換前的老的連線不會被超時 Kill 導致相關 SQL 執行超時,直到 900秒系統預設超時後才會斷開連線再次重連。

  2、設定 TransactionTimeout 即事務超時時間,事務就是一把鎖,超時時間越長鎖越久,導致不在事務裡的相關 SQL 鎖等待導致效能差。

  故障案例:在某次變更時由於程式碼有 bug 導致事務未提交,同時由於事務沒設定超時時間,導致大量相關 SQL 超時服務受損。

  3、設定 Ibatis 的 defaultStatementTimeout、queryTimeout。

  4、設定 MaxWait:獲取新連線的等待超時時間。

  小插曲:之前 Druid 預設設定的 60 秒,後來筆者與作者有過溝通反饋這個預設值太長容易坑大家,後來發現已經改為了 6 秒[1]

  自定義執行緒池

  1、執行緒池設定的佇列過長容易造成阻塞影響吞吐。

  2、future.get,預設沒有超時時間,需顯式傳入。

  故障案例:Dubbo 執行緒池滿報警,排查後發現是業務程式碼裡使用了 future.get 沒有設定超時時間,同時執行緒池的拒絕策略設定的是 DiscardPolicy,會導致線上程池滿後新的任務被丟棄時 future.get 阻塞,進而導致 Dubbo 執行緒池滿服務受損。

  Redis連線池

  1、設定 Jedis pool MaxWait,與 Druid 的 MaxWait 類似,也與 Http 連線池的 ConnectionRequestTimeout 類似。

  2、設定 ConnectionTimeout、SocketTimeout,與 Druid/Http 連線池的類似。

   總結

  fast-fail 理念

  1、本質上是不浪費系統資源,一些超時時間設定過長其實是在做無效的 IO 等待。

  2、有一些個人的經驗值貼一下:ConnectionTimeout 建議1-3 秒更優,最大不超過 5 秒。SocketTimeout 根據業務請求時間情況設定建議最大不超過 10 秒,MaxWait/ConnectionTimeout 建議 3~5 秒,最大不超過 6 秒。

  保護好自己:流控/背壓

  1、資料庫後臺運維平臺設定自動限流,緊急情況下收到預警後第一時間手動執行限流。

  2、實現 單機維度、叢集維度(Region/AZ)、使用者維度、介面維度 流控。

  3、訊息中介軟體拉取訊息的 Client 實現背壓機制。

  謹慎重試

  Retry 會加速系統雪崩,AWS 有一篇部落格介紹了相關的經驗,Link> [2]。核心要點如下:

  不在最上層自動重試,在單個節點裡重試

  令牌桶控制重試的速率

  定時、週期性的作業需要打散,分散高峰。這塊我們也遇到過類似的故障案例:

  故障案例1:某客戶端曾經出過一個類似故障:客戶端的定時心跳同一秒傳送到服務端,導致服務端扛不住,此類情況需適當打散。

  故障案例2:某系統大量定時任務都是整點執行,一瞬間對系統壓力過大引發線上問題,定時任務的週期需適當打散。

  最後,本文有很多血淋淋的教訓,大多是常見問題,本文肯定有不全面的地方,歡迎評論區多多指教。

來自 “ 阿里雲開發者 ”, 原文作者:青逸;原文連結:https://server.it168.com/a2024/0104/6835/000006835911.shtml,如有侵權,請聯絡管理員刪除。

相關文章