一下午連續故障兩次,誰把我們介面堵死了?!

程序员鱼皮發表於2024-07-26

唉。。。

大家好,我是程式設計師魚皮。又來跟著魚皮學習線上事故的處理經驗了喔!

一下午連續故障兩次,誰把我們介面堵死了?!

事故現場

週一下午,我們的 程式設計導航網站 連續出現了兩次故障,每次持續半小時左右,現象是使用者無法正常載入網站,一直轉圈圈。

使用者很快就在群裡炸開鍋了,甚至有使用者表示 “我提前進去了,都不敢重新整理。。”

一下午連續故障兩次,誰把我們介面堵死了?!

看到這些我真的非常難受,我們團隊的開發同學也第一時間展開了排查。

簡單看了下前端向後端發的請求,發現所有的請求都一直阻塞,直到超時。直接請求後端伺服器的介面也是一樣的,等了很久都沒有正常返回資料。最關鍵的是,所有介面都阻塞住了,哪怕只是請求個健康檢查介面(後端直接返回 "ok",不查詢資料庫),也無法正常響應。

我們的後端服務是部署在容器託管平臺的,正常情況下如果資源(比如 CPU 和記憶體)佔用超過一定比例,會自動擴容節點來讓服務承載更多的併發請求,但為什麼這次沒有擴容呢?

其實有經驗的朋友應該已經能猜到介面堵死的原因了,下面我帶大家揭開謎團。

事故排查

根據上面的現象,推測大機率是介面層出了問題,而不涉及到業務和資料庫等依賴資源。由於我們的後端使用的是 Spring Boot + 內嵌的 Tomcat 伺服器,而 Tomcat 同時處理請求的最大執行緒數是固定的(預設是 200),所以當同時處理的請求過多,並且每個請求一直沒有處理完成時,所有的執行緒都在繁忙,沒有辦法處理新的請求,就會導致新的請求排隊等待處理,從而造成了介面堵死(遲遲無法響應)的現象。

這裡我用一個簡單的程式來模擬下介面堵死和排查過程。

首先寫一個非常簡單的測試介面,在返回內容前加一個 Thread.sleep,模擬耗時的操作,讓處理請求的執行緒進入較長的等待。

一下午連續故障兩次,誰把我們介面堵死了?!

然後更改下 Tomcat 的最大執行緒數為 5,便於我們模擬執行緒數不夠的情況:

一下午連續故障兩次,誰把我們介面堵死了?!

啟動專案,在 Thread.sleep 打斷點,然後連續請求 6 次介面。

一下午連續故障兩次,誰把我們介面堵死了?!

應該只有 5 次請求會進入斷點,最後一次請求會一直轉圈卡住,沒有執行緒來處理。這樣我們就還原了事故現場。

一下午連續故障兩次,誰把我們介面堵死了?!

但以上只是推測,實際線上專案中,怎麼去排查確認 Tomcat 執行緒都阻塞了呢?又怎麼確認是哪個介面或程式碼讓 Tomcat 執行緒阻塞等待了呢?

其實很簡單,首先用 jps -l 命令檢視 Java 後端服務對應的程序 PID:

一下午連續故障兩次,誰把我們介面堵死了?!

然後使用 jstack 命令生成執行緒快照,並儲存為檔案。具體命令如下:

jstack <程序PID> > thread_dump.txt

開啟執行緒快照檔案,所有執行緒的狀態一目瞭然,搜尋 http-nio 就能看到 Tomcat 的請求處理執行緒,果然所有的請求處理執行緒狀態都是 TIMED_WAITING ,表示執行緒正在等待另一個執行緒執行特定的動作,但是有一個指定的等待時間。而且能直接看到請求是阻塞在了哪個程式碼位置。

一下午連續故障兩次,誰把我們介面堵死了?!

利用這個方法,我們也很快定位到了程式設計導航介面堵死的原因,是發生在一個從資料庫查詢使用者的方法。由於我們昨天下午執行了簡訊群發召回老使用者的動作,導致大量使用者同時訪問程式設計導航並執行這個方法。由於涉及的資料庫查詢操作執行較慢,每個請求都需要等待資料庫查詢出結果後,才能響應資料,下一個請求才能再進來查詢資料庫,就導致大量 Tomcat 請求處理執行緒阻塞在等待資料庫查詢上,再進一步導致新的請求要排隊等待處理。

真相大白了!

如何解決?

其實我們這次遇到的問題就是典型的 “線上連線池爆滿問題”,面試的時候也是經常問的。前面講了怎麼排查此類問題,那如何解決這類問題呢?

首先遇到連線池爆滿的情況,先保護現場,比如按照魚皮上面的操作 dump 執行緒資訊,然後趕緊重啟服務或啟動新的例項,讓使用者先能正常使用。再進行排查分析和最佳化。

如何最佳化線上連線池爆滿問題?首先肯定還是要最佳化造成請求阻塞的程式碼。比如資料庫查詢慢,我們就透過新增索引來提升查詢速度。

還可以增加資料庫連線池的大小,在 Spring Boot 中,預設使用 HikariCP 作為資料來源連線池,而 HikariCP 的 maximumPoolSize(最大連線池大小)預設值只有 10,顯然是不足以應對高併發場景的。可以把下面的配置數值調大一點:

spring:
datasource:
hikari:
maximum-pool-size: 50

由於後端請求操作不止有查詢資料庫,所以 Tomcat 執行緒池的最大執行緒數和最小空閒執行緒數也可以按需調整,比如下列配置:

# 設定 Tomcat 最大執行緒數
server.tomcat.threads.max=300
# 設定 Tomcat 最小空閒執行緒數
server.tomcat.threads.min-spare=20

適當調大 Tomcat 的最大執行緒數,可以增加併發請求的處理能力。適當調大 Tomcat 的最小空閒執行緒數,可以確保在併發高峰時刻,Tomcat 能迅速響應新的請求,而不需要重新建立執行緒。

其實我們大多數情況下,線上伺服器(容器)的記憶體利用率是不高的,所以可以根據實際的資源和併發情況,適當地改一改配置。記得多做做測試,因為過高的執行緒數可能導致執行緒排程開銷增加,反而降低效能。

現實

好吧,以上只是我遇到此類問題的解決方案。但現實可能沒那麼理想,其實慢 SQL 這個問題我們在上一次故障時就已經定位到,並且在群內同步了。結果你猜怎麼著,我們團隊的開發同學發了一堆監控的截圖,但是沒有一個人真正去解決了這個問題,這才導致了故障在多日之後重新上演!

一下午連續故障兩次,誰把我們介面堵死了?!

一旦發現了問題,就必須要想到儘可能長久支援的解決方案,要不然這監控不是白做了?

為什麼這次事故持續了這麼久呢?也是因為我團隊的開發同學缺少線上問題處理的經驗,在那一通分析,結果忘了恢復服務,過了半個小時使用者還是無法訪問,直到我去提醒。。。

一下午連續故障兩次,誰把我們介面堵死了?!

所以這個時候就知道平時背的八股文有多重要了吧?Tomcat 的聯結器配置和效能最佳化也是一道經典的八股文,也是我們 面試鴨刷題神器 收錄的題目。這些知識等到真出了線上問題時,都是用的上的。

一下午連續故障兩次,誰把我們介面堵死了?!

吃一塹,長一智,經過這次的事件,我相信團隊的同學又一次成長了。讀者朋友們,你們有收穫沒有嘞~

更多

💻 程式設計學習交流:程式設計導航
📃 簡歷快速製作:老魚簡歷
✏️ 面試刷題神器:面試鴨

相關文章