服務假死問題解決過程實記(一)——問題發現篇

一、前言

自三月六日起,筆者所在業務組的開發環境上出現了若干次服務假死,頁面請求無響應的現象。由於筆者在三月六日之前,對 JVM, Tomcat,以及數據庫連接池沒有絲毫調優經驗,所以從三月六日開始的所有與解決該問題的過程,都會記錄到本文,以記錄並紀念筆者的第一次服務調優經歷。

鏈接:

《服務假死問題解決過程實記(一)——問題發現篇》
《服務假死問題解決過程實記(二)——C3P0 數據庫連接池配置引發的血案》
《服務假死問題解決過程實記(三)——緩存問題優化》

二、03.06 記 Tomcat 的一次假死問題解決經歷

注:

  1. 本文是一個前序,記錄問題出現的現象,以及猜測的原因。本文中最後猜測出現的結論,並非服務假死的原因所在。
  2. 2019.03.30 記,該現象的源頭是因爲 C3P0 參數配置問題,現已解決該數據庫連接問題。但這只是問題解決過程中順手解決的另一個問題而已,服務假死的原因應該是因爲其他原因,該問題並非源頭。

1. 測試環境服務假死

現象:未知具體操作,但出現 Tomcat 假死情況,無法使用 jmap, jstat, jstack 指令以及 jvisualVM 工具,且使用 netstat -ano | findstr “12808” 指令後,出現七八個 CLOSE_WAIT 連接。

猜測原因:

  1. ClassLoader 的更換,導致方法區溢出?
  2. 反射使用太多?

待解決方法:

  1. 重啓服務,待正常運行過程中使用 JVisualVM 監控,看方法區以及堆區的大小變化;
  2. 如果還需要重新啓動服務,加入虛擬機 GC 參數:-XX:HeapDumpOnOutOfMemoryError;
  3. 如果確定是 ClassLoader 堆積的問題,則加入類卸載參數:-Xnoclassgc;

相關參考連接:

  1. 關於 CLOSE_WAIT 狀態,來自於關閉 TCP 連接的四次握手:
  1. 《netstat監控大量ESTABLISHED連接數和TIME_WAIT連接數題解決》

2. 再次假死,併成功定位問題

由於昨天有了一次假死,且假死過程中已經不能使用 JVisualVM 連接 Tomcat 服務,所以在服務重啓之前,我就已經打開了 JVisualVM 遠程監控。但到了下午 17:24 再次假死。

(1) 第一個排查方向:方法區與堆內存

師父一直懷疑是因爲業務原因,使用過自定義的 ClassLoader,並存在稍微頻繁加載類的可能性,所以他的檢測重點在於方法區。但開 JVisualVM 發現方法區大小一直穩定在 150+M,從始至終很穩定的很小幅度增長,遠沒有到我們設置的 MaxPermSize = 500M。此外給虛擬機設置的 -Xmx:4096M,但實際使用堆內存只有 800M 左右。

(2) 第二個重點排查方向:激增的線程數量

我的監控重點,在於觀察到 17:24 線程數飆升,從 240 個線程瞬間漲到了 400+ 個。打開 JVisualVM 的線程監控界面,發現幾乎同一時間 pool-22, pool-37 創建了大量線程。使用線程 Dump,讀取 Dump 文件,發現大部分 BLOCK 的線程都是彙總數據的內容,以及在 save, update, delete 方法上加的 After AOP 中的異步日誌記錄方法。所以線程暴增的原因,肯定和 17:24 出現的某種 save 操作有關。果然在我寫的記錄 save 方法操作記錄的日誌文件,結合項目日誌,瞭解到 17:24 分有測試人員調用 XML 解析入庫的方法,且調用了十幾種不同的表的插入操作,估算了一下自己寫的代碼,大概一次 save 操作應該會加入十一二個 task 到阻塞任務隊列中,所以線程數量暴增的問題定位到了。

但是,**以前測試人員也做過很多次類似的解析 XML 入庫的操作,並沒有引發過 Tomcat 假死的錯誤。**我感覺很奇怪,所以還是隻把線程數量激增的情況當成了一種現象,並未完全它是問題的源頭。

(3) 第三個重點排查方向:CLOSE_WAIT 數量

上網搜 “Tomcat 假死”,帖子 《tomcat假死之謎?》 介紹,會出現 100+ 個 CLOSE_WAIT 的現象,導致了 Tomcat 崩潰。那時候老子還不會使用 netstat 指令,甚至連 CLOSE_WAIT 都是前一天晚上臨時抱佛腳學的,就依葫蘆畫瓢輸了個 netstat -ano | findstr “CLOSE_WAIT”,結果出現只有 10 個左右的 CLOSE_WAIT 是和我們的服務有關的,雖然有了找到錯誤的那麼點兒意思,但 10 個 CLOSE_WAIT 也太沒牌面了吧……

(4) 第四個重點排查方向:TIME_WAIT 數量

大概有一個多小時糾結在 CLOSE_WAIT 上面,後來還是嘗試着按照帖子的做法來做,開始監控 TIME_WAIT。改了個 netstat 指令:netstat -ano | findstr “TIME_WAIT”結果發現有 200+ 個 TIME_WAIT!!

”終於上量級了!!有牌面了啊媽蛋!!“——筆者的內心世界

TIME_WAIT 的內容是啥呢?大致內容都是:

......
tcp    16.12.104.133:55177    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:56430    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:55659    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:55141    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:56582    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:55240    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:54811    16.12.181.151:1521   TIME_WAIT   
tcp    16.12.104.133:55772    16.12.181.151:1521   TIME_WAIT   
......

重點來了,雖然不懂 TIME_WAIT,但大片的 1521 也太醒目了吧啊喂!!

1521 是確實是 Oracle 默認的連接端口號,而且對應的 16.12.181.151 也確實是我們要連的數據庫,但是我們的服務又不是直連數據庫的,怎麼會出現這麼多的 1521 端口的 TIME_WAIT 呢?莫非是 RPC?不對,我們的 RPC 應該是直連 DAO 服務的,DAO 服務纔會連接數據庫,所以肯定也不會是 RPC 的原因。

忽然我想到了一個很重要的事情:由於我們的資源有限,只分配了兩臺虛擬機,對於我們這個有四個服務 (兩個應用服務,兩個 DAO 服務) 的業務組,我們把應用服務和 DAO 服務交叉配置,所以在我們服務部署的 16.12.104.133 虛擬機上,確實是有一個 DAO 服務的,而且和我們的應用服務部署在同一個 Tomcat 下

(5) 確定問題所在的證據

首先,我先檢查其他業務組的正常 DAO 服務。遠程連到其他正常 DAO 服務的虛擬機上輸入同樣的指令 netstat -ano | findstr “TIME_WAIT”,以及 netstat -ano | findstr “CLOSE_WAIT”,都不會出現幾行。所以可以確定這個 DAO 服務肯定是有異常的。

然後,端口數量。還是剛纔 16.12.104.133 上連接數據庫的端口數,是很明顯的 +1 遞增趨勢,最大的端口數基本上是 65400+ 左右。我專門查了一下端口最大連接數量,知道了 TCP 最大連接數量是 65536 個,所以這種佔據了大量端口的現象,一定是異常!!

確認了異常的原因,是**部署在 16.12.104.133 上同 Tomcat 下的 DAO 層服務的數據庫連接異常**。這樣也可以解釋,爲什麼 JVM 監控正常,GC 情況無異常,且在假死現象出現後甚至無法使用 JVisualVM 連接服務的諸多情況都可以解釋了。

注:
值得一說的是,對於基於 TCP 的 HTTP 協議,關閉 TCP 連接的是 Server 端,這樣,Server 端會進入 TIME_WAIT 狀態,可想而知,對於訪問量大的 Web Server,會存在大量的 TIME_WAIT 狀態,假如 server 一秒鐘接收 1000 個請求,那麼就會積壓 240*1000=240000 個 TIME_WAIT 的記錄,維護這些狀態給 Server 帶來負擔。當然現代操作系統都會用快速的查找算法來管理這些 TIME_WAIT,所以對於新的 TCP 連接請求,判斷是否 hit 中一個 TIME_WAIT 不會太費時間,但是有這麼多狀態要維護總是不好。
HTTP 協議 1.1 版規定 default 行爲是 Keep-Alive,也就是會重用 TCP 連接傳輸多個 request/response,一個主要原因就是發現了這個問題。


未完待續。下篇《服務假死問題解決過程實記(二)——C3P0 數據庫連接池配置引發的血案》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章