容器重啓23次,原因竟然是。。。。

最煩的事情,莫過於服務莫名其妙的重啓,當你看到一個服務一天重啓23次,你會是怎樣的一個感覺,反正博主我快要摔電腦了。。。。

 

問題既然已經發生了,肯定得動手術刀解決它。在開始看代碼之前,我們可以先來假想一下,發生服務重啓的原因可能有哪些,然後再根據可能性一條條的排查,這種方式可以快速的幫助我們分析並找到最終的問題點。

服務重啓的可能原因:

  1. 第三方軟件失效導致容器重啓(MySQL、Redis、MQ等)
  2. 併發過高,導致cpu滿負荷,服務宕機重啓
  3. 容器所需資源被其它容器所幹擾,導致資源不夠重啓
  4. 佔用內存過多,被linux進程殺死

1.第三方軟件失效

因爲項目日誌級別是error,所以一旦發生第三方配置失效,會馬上打印出錯誤信息,但是查看了錯誤日誌並沒有此類的錯誤日誌,而且查看了MySQL、Redis、MQ等運行情況,沒有發現任何異常信息,所以基本可以排除是因爲第三方導致容器重啓。

2.併發過高

併發的可能性,我們可以通過APM(應用性能管理)來查看,這邊博主的系統中APM是採用SkyWalking管理工具,併發的指標如下圖所示:

系統的併發爲平均每分鐘224.20,可以看出該服務的併發不是特別高,服務的配置完全可以支撐的住,所以也不太可能是因爲併發問題導致的服務重啓。

3.容器被幹擾

至於第三個原因,因爲博主的項目是部署在騰訊雲上面,可以精確的對各個容器資源進行設置,所以也不可能出現互相干擾的情況。

4.內存問題

分析到這邊,問題就轉化爲JVM內存溢出問題排查了,我們首先開啓JVM垃圾回收日誌,JVM命令如下所示:

-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

運行一段時間後,我們可以看到如下所示的JVM垃圾回收日誌:

如果童靴們看不懂這些日誌的話,可以參考一下下面兩幅說明圖:

Major GC:

Full GC:

通過GC日誌,我們可以看到,在很短的時間內發生了多次Major GC而且相對於老年代,年輕代的內存設置很小,我們來看看這個問題服務的JVM參數配置信息:

-Xms4000m
-Xmx4000m
-Xss512k
-Xmn800m
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

這裏的JVM很簡單,就設置了最大堆、最小堆、年輕代、永久代、線程大小、以及GC日誌相關配置,那這個JVM參數問題出現在哪裏?

童靴們要知道JVM參數的配置是要根據實際項目來的,不同類型的項目JVM參數會截然不同,這裏博主就簡單以這個問題項目舉例說明,如果想了解更多JVM內容,可以自行百度學習,這邊不進行過多稱述。

首先要明確項目類型是數據服務,還是API服務,博主這個服務就是一個API服務,所以對實時性要求比較高。如果對實時性要求比較高,就要控制GC的停頓時間,因爲在垃圾回收的時候不管是什麼垃圾回收算法都會有一定的性能損耗。比如新生代的複製算法,老年代的標記整理清除算法等都是有一定性能損耗的。

推測一:大對象推測導致服務重啓

這些GC的細節博主就不過多陳述了,我們來分析分析重啓服務GC參數的問題,爲什麼會導致服務重啓呢?按照以往的經驗,博主第一時間想到的會不會是大對象、大集合引起的內存泄漏導致的。後面博主通過RamUsageEstimator.humanSizeOf()工具類來打印對象內存使用情況

List<TAmzdbProduct> resultList = tAmzdbProductDao.getProudctList(map);
long ramSize = RamUsageEstimator.sizeOf(resultList) / (1024 * 1024);
// 內存大小大於5M
if (ramSize >= 5) {
    log.error("getProudctList接口,list數據過大,size:{},accountId:{}", ramSize, accountId);
}

從測試結果可以得出,沒有超過5M的大對象,所以不可能是因爲一個突然的大對象導致的內存泄漏問題。(當然也可以通過jmap 、jstat 、jstack等命令查看)

推測二:Full GC不及時

如果不是大對象引起的內存泄漏問題,那還會是什麼呢?博主通過認真分析JVM垃圾回收日誌,終於發現了蛛絲馬跡,JVM垃圾回收日誌顯示,Major GC特別頻繁,Full GC幾乎很少發生,那我們是不是可以推測,是因爲年輕代設置的太小,然後每次用戶請求所產生的內存對象也很小,導致對象隨着時間的推移全部跑到老年代中,而且因爲對象內存很小,所以不會促發Full GC,等老年代滿的時候着正要觸發Full GC的時候,因爲服務內存使用過大,導致服務被進程殺死呢?

針對這一推測,我們先來調整一下JVM參數,將年輕代設置大一些,老年代在達到內存70%的時候就觸發Full GC,通過這種方式就可以解決上面的問題。

JVM參數

-Xms4000m
-Xmx4000m
-Xss512k
-Xmn1500m 
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+DisableExplicitGC
-XX:SurvivorRatio=6
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=2
-XX:+CMSClassUnloadingEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

JVM參數設置完畢之後,通過持續性跟蹤服務運行狀況,結果很喜人,運行了2天多的時間,服務沒有重啓過一次,所以可以充分說法我們上述的推測是正確的。

問題已經得到修復,但是修復問題並不是我們最終的目的,博主寫這篇文章的目的,主要是爲了分享基於已經存在的問題,我們應該運用怎麼樣的思維模式去解決它呢?如何才能更高效,更便捷呢?當然每個人的思考模式都不一樣,正所謂一千個讀者就有一千個哈姆雷特,童鞋們不必死記硬背,活學活用纔是最終的目的。

流程總結:

  1. 發現問題
  2. 通過問題分析,提出可能引發問題的原因
  3. 一一驗證假設原因,排除錯誤的假設原因
  4. 確定假設原因,縮小範圍繼續排查
  5. 通過測試實驗得出結果
  6. 修改代碼併發布進行結果驗證
  7. 驗證通過,問題解決

想要更多幹貨、技術猛料的孩子,快點拿起手機掃碼關注我,我在這裏等你哦~

林老師帶你學編程https://wolzq.com

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