背景 運維人員反饋一個容器化的java程式每跑一段時間就會出現OOM問題,重啟後,間隔大概兩天後復現。 問題調查 一查日誌 由於是容器化部署的程式,登上主機後使用docker logs ContainerId查看輸出日誌,並沒有發現任何異常輸出。 使用docker stats查看容器使用的資源情況, ...
背景
運維人員反饋一個容器化的java程式每跑一段時間就會出現OOM問題,重啟後,間隔大概兩天後復現。
問題調查
一查日誌
由於是容器化部署的程式,登上主機後使用docker logs ContainerId查看輸出日誌,並沒有發現任何異常輸出。 使用docker stats查看容器使用的資源情況,分配了2G大小,也沒有發現異常。
二缺失的工具
打算進入容器內部一探究竟,先使用docker ps 找到java程式的ContainerId
,再執行docker exec -it ContainerId /bin/bash進入容器。進入後,本想著使用jmap、jstack 等JVM分析命令來診斷,結果發現命令都不存在,顯示如下:
bash: jstack: command not found
bash: jmap: command not found
bash: jps: command not found
bash: jstat: command not found
突然意識到,可能打鏡像的時候使用的是精簡版的JDK,並沒有這些jVM分析工具,但是這仍然不能阻止我們分析問題的腳步,此時docker cp命令就派上用場了,它的作用是:在容器和宿主機之間拷貝文件。這裡使用的思路是:拷貝一個新的jdk到容器內部,目的是為了執行JVM分析命令,參照用法如下:
Usage: docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH [flags]
有了JVM工具,我們就可以開始分析咯。
三查GC情況
通過jstat查看gc情況
bin/jstat -gcutil 1 1s
看樣子沒有什麼問題,full gc也少。再看一下對象的占用情況,由於是容器內部,進程號為1,執行如下命令:
bin/jmap -histo 1 |more
發現ByteBuffer對象占用最高,這是異常點一。
四查線程快照情況
- 通過jstack查看線程快照情況。
bin/jstack -l 1 > thread.txt
下載快照,這裡推薦一個線上的線程快照分析網站。
https://gceasy.io
上傳後,發現創建的線程近2000個,且大多是TIMED_WAITING狀態。感覺逐漸接近真相了。 點擊詳情發現有大量的kafka-producer-network-thread | producer-X 線程。如果是低版本則是大量的ProducerSendThread線程。(後續驗證得知),可以看出這個是kafka生產者創建的線程,如下是生產者發送模型:
根據生產者的發送模型,我們知道,這個sender線程主要做兩個事,一是獲取kafka集群的Metadata共用給多個生產者,二是把生產者送到本地消息隊列中的數據,發送至遠端集群。而本地消息隊列底層的數據結構就是java NIO的ByteBuffer。
這裡發現了異常點二:創建過多kafka生產者。
由於沒有業務代碼,決定寫一個Demo程式來驗證這個想法,定時2秒創建一個生產者對象,發送當前時間到kafka中,為了更好的觀察,啟動時指定jmx埠,使用jconsole來觀察線程和記憶體情況,代碼如下:
nohup java -jar -Djava.rmi.server.hostname=ip
-Dcom.sun.management.jmxremote.port=18099
-Dcom.sun.management.jmxremote.rmi.port=18099
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false -jar
com.hyq.kafkaMultipleProducer-1.0.0.jar 2>&1 &
連接jconsole後觀察,發現線程數一直增長,使用記憶體也在逐漸增加,具體情況如下圖:
故障原因回顧
分析到這裡,基本確定了,應該是業務代碼中迴圈創建Producer對象導致的。
在kafka生產者發送模型中封裝了 Java NIO中的 ByteBuffer 用來保存消息數據,ByteBuffer的創建是非常消耗資源的,儘管設計了BufferPool來複用,但也經不住每一條消息就創建一個buffer對象,這也就是為什麼jmap顯示ByteBuffer占用記憶體最多的原因。
總結
在日常的故障定位中,多多使用JDK自帶的工具,來幫助我們輔助定位問題。一些其他的知識點:
jmap -histo顯示的對象含義:
[C 代表 char[]
[S 代表 short[]
[I 代表 int[]
[B 代表 byte[]
[[I 代表 int[][]
如果導出的dump文件過大,可以將MAT上傳至伺服器,分析完畢後,下載分析報告查看,命令為:
./mat/ParseHeapDump.sh active.dump org.eclipse.mat.api:suspects
org.eclipse.mat.api:overview org.eclipse.mat.api:top_components
可能儘快觸發Full GC的幾種方式
1) System.gc();或者Runtime.getRuntime().gc();
2 ) jmap -histo:live或者jmap -dump:live。
這個命令執行,JVM會先觸發gc,然後再統計信息。
3) 老生代記憶體不足的時候