Java應用上雲後被kill問題分析與解決

張哥說技術發表於2023-02-01

Java應用上雲後被kill問題分析與解決


前言
自從公司 2021 年 11 月份開始全面容器化後,酒店報價中心團隊快速響應,遷移了 98% 的應用,由原來的 kvm 或實體機器到容器上,我們的多個應用出現了頻繁被 kill 的情況,主要包括兩大類:

  • 因為 GC 時間過長導致 k8s 檢活失敗,被 kill 掉

  • 因為記憶體碎片的問題,導致 OOM 被 kill 掉

本文主要介紹發現問題以及解決問題的過程。
問題一:應用長時間 GC
應用 GC 時間長導致 k8s 檢活失敗,k8s 會 kill 掉業務應用。

  • 具體現象和分析

Java應用上雲後被kill問題分析與解決

當時我們團隊的兩個應用在釋出到 docker上以後,出現了頻繁重啟的現象,釋出後一天內重啟次數高達 29 次左右,提示也僅僅是 “時間:2021-11-25T21:34:58+08:00 原因:Error kill” 看不到具體的問題,只能到對應的容器內部去尋找線索了。具體排查過程如下:
1、確認為啥會被 kill ?

Java應用上雲後被kill問題分析與解決

Java應用上雲後被kill問題分析與解決

首先透過 dmesg 命令,檢視容器所在主機上的日誌。
發現是存在 OOM kill 掉的 Java 應用,但是對比了下這個 total-vm 和我們自己的應用配置發現差別很大,不是我們的應用程式,我們的配置是:

Java應用上雲後被kill問題分析與解決

捨棄了這個方向後轉向了 k8s 的日誌(ps:為啥第一次不看這個,是因為這個日誌的檢視時需要許可權的,前期沒有許可權,只能自己動手先看方便的),發現了在被 kill 之前出現了3次 unhealthy 的 k8s 日誌,且返回請求狀態不是200。

Java應用上雲後被kill問題分析與解決

看下為什麼會有檢活請求異常,檢查業務訪問日誌,發現這個時間點是有接收到請求,手動訪問也是成功的。
但是當時為啥會有訪問狀態不是 200 的問題呢?懷疑當時業務程式是有大量任務在跑,響應超時問題導致, 所以開始排查業務的具體日誌。
2、發現問題
在排查日誌的時候發現了導致該問題的根本原因,本質是 GC 時間過長導致。檢視重啟前的容器 GC 日誌:

Java應用上雲後被kill問題分析與解決

容器被重啟是在 2021-11-25T21:34:58+08:00 重啟的。在這個時間點前,也就是被 kill 之前的一次 GC 時間高達 18s + 7s 。至此,原因就很清晰了:
因為應用程式的 GC 導致服務不能正常響應 k8s 的檢活請求,k8s 認為應用“死”了,觸發了 kill 和重啟操作!

  • 解決
  • 透過 GC 日誌,分析主要耗時點。推薦 GC 分析工具:,調整 JVM引數。
  • k8s 調整了檢活機制,由原來超時 10s、20s,最後調整為 2min。
  • 透過分析日誌發現主要的長時 GC 是因為新生代晉升失敗,擴大 young 區和堆大小最佳化 JVM 引數。

問題二:記憶體碎片

  • 現象

問題現象是應用管理平臺上出現了容器 “OOM kill” 的提示。

Java應用上雲後被kill問題分析與解決

  • JVM堆記憶體分析

基於 OOM kill 的提示,開始分析應用日誌,發現堆區、棧區並沒有出現 OOM 的問題,懷疑是堆外記憶體記憶體溢位導致,因此,嘗試新增相應的JVM引數以觀察堆外記憶體的使用情況。

Java應用上雲後被kill問題分析與解決

觀察是否是有堆外記憶體沒有釋放,再加上 OOM 沒有明顯徵兆,寫了指令碼定時 30s 看下使用情況。

Java應用上雲後被kill問題分析與解決

下圖是在容器啟動後的 1 分鐘 到 容器即將被 kill 時的 JVM 記憶體分配對比圖:

Java應用上雲後被kill問題分析與解決

Java應用上雲後被kill問題分析與解決

發現 JVM 的使用記憶體並沒有明顯變化(12491M→ 12705M),且整體沒有超過 docker 分配的記憶體限制(docker limit Memory:12G),但是為什麼會有 OOM 呢?哪塊的記憶體使用升高導致了 OOM 呢?
查詢了大量的資料,排查方向轉向記憶體這塊。

  • pmap 記憶體對映分析

使用 pmap 分析 Java 程式的記憶體對映關係:

Java應用上雲後被kill問題分析與解決

Java應用上雲後被kill問題分析與解決










pmap說明Address: 記憶體開始地址Kbytes: 佔用記憶體的位元組數(KB)RSS: 保留記憶體的位元組數(KB)Dirty: 髒頁的位元組數(包括共享和私有的)(KB)Mode: 記憶體的許可權:read、write、execute、shared、private (寫時複製)Mapping: 佔用記憶體的檔案、或[anon](分配的記憶體)、或[stack](堆疊)Offset: 檔案偏移Device: 裝置名 (major:minor)
發現可疑的地方有兩個 1029712KB(1005M)的記憶體塊和較多64M記憶體塊,linux 預設使用的 glibc 的 ptmalloc 記憶體分配器,有這個問題。
Glibc為什麼會有64M的記憶體塊的問題?

  • 引入記憶體分配器

在程式申請記憶體時,根據需要分配的記憶體大小由記憶體分配器來想核心申請具體的記憶體區域,那麼為什麼會有記憶體分配器來申請記憶體,而不是程式直接向系統申請呢?因為系統呼叫的開銷比較大,這樣做是非常不值的。同時,在 linux下分配堆記憶體需要使用 brk系統呼叫,而這個系統呼叫只是簡單地改變堆頂指標而已,也就是將堆擴大或者縮小。舉個例子,程式分別申請了 M2 和 M1 兩塊記憶體,執行了一段時間後,M2 記憶體不需要了,需要回收了。

Java應用上雲後被kill問題分析與解決

使用系統處理的話,只能使用 brk 移動指標,那麼 M1 也會被回收掉,這樣顯然是不行的。所以引入記憶體分配器,把 M2 的記憶體快取下來,等到程式需要再次申請記憶體空間時不需要使用系統呼叫,而是直接從快取中分配,這個動作就是由記憶體分配器完成的。記憶體分配器不僅提升了執行效率,還提高了記憶體的使用率。

Java應用上雲後被kill問題分析與解決

  • ptmalloc 解讀

glibc 的記憶體分配器(ptmalloc)的結構如圖所示,一個程式就有一個主分配區和若干個從分配區。所有的執行緒申請記憶體時,都要經過主分配區申請,多執行緒時就需要透過鎖機制來保證分配的正確性,從分割槽就應運而生了,ptmalloc 根據系統對分配區的爭用情況動態增加分配區的數量。

Java應用上雲後被kill問題分析與解決

在申請記憶體時,glibc 每次申請新的記憶體時,主分配區是可以透過 brk 或者 mmap 來向系統申請的,但是非主分配的記憶體只能透過 mmap 申請了,在64位機器上每次申請的虛擬記憶體區塊大小是 64MB ,最大為8倍的 CPU 數量。且從分配區一旦建立,就不會被回收了。這個就是該問題中發現的 64MB 記憶體塊產生的原因。
程式申請記憶體的簡單步驟如下:

1. 透過 fastbins 查詢合適記憶體塊,

2. 1沒有,從 small bin 中獲取,

3. 2沒有,從 unsorted bin 中獲取,

4. 3沒有,從 large bin 中獲取,

5. 4沒有,從 top chunk 中,

6. 5不夠,向系統申請 brk/mmap。
記憶體回收的簡單步驟如下:

1. 判斷是否是 mmap 對映,是直接回收

2. 判斷是否鄰近 top chunk

3. 不是2,根據 size 放到不同的 bins 中

4. 是2,判斷 top chunk 中鄰近記憶體是否在使用 是 合併 top chunk

5. 合併後判斷 top chunk 大小,超過閾值(預設128k),但是開始分配128k不會回收。
透過翻閱資料發現,現在市面上有不少記憶體分配器的實現,如 tcmalloc ,jemalloc 等,在這裡我們選擇了 jemalloc。

  • 替換記憶體分配器解決記憶體碎片問題

jemalloc
jemalloc 是一個通用的 malloc(3) 實現,強調碎片避免和可擴充套件的併發支援。
避免記憶體碎片和效能點提升

  • 執行緒記憶體池:在一個程式中每個執行緒會有自己的記憶體池,用來管理自己的記憶體使用,會大幅度減少併發時鎖的效能損失。

  • 鎖粒度:使用非公平鎖,替換自旋鎖,減少 CPU 空轉。

效能對比

Java應用上雲後被kill問題分析與解決

在灰度環境嘗試替換為 jemalloc 的記憶體管理器:
安裝 jemalloc

Java應用上雲後被kill問題分析與解決

也可嘗試安裝 tcmalloc

Java應用上雲後被kill問題分析與解決

驗證下是否使用成功了

Java應用上雲後被kill問題分析與解決

發現 '64MB' 的空間對映已經不存在了,且在觀察時間範圍內還有一次記憶體的下降, 觀察一週一直平穩執行,沒有出現 OOM kill 的問題。
Java應用上雲後被kill問題分析與解決
Java應用上雲後被kill問題分析與解決
總結
問題處理流程

Java應用上雲後被kill問題分析與解決

發現問題,就是個人前進的一大步。要發現問題,就要抓細節,不放棄再加上有頭腦的處理問題!
工具最佳化

  • 最佳化 dump 體驗。原來容器 dump 時會存在 dump 到一半機器就重啟的問題,跟基礎架構 和技術運營的同學溝通後,對該部分做了最佳化,讓業務分析 GC 時間過長有了實質性幫助。

  • 確認監控問題。之前大家看到容器使用監控上應用重啟都是因為記憶體翻倍使用,但實際情況是容器在重啟後,監控平臺把兩個容器使用的記憶體求和了,沒有單獨分開處理。

  • 支援可選擇分配器。基礎架構部門對 jemalloc 和 tcmalloc 的記憶體分配器進行支援。

Java應用上雲後被kill問題分析與解決

參考檔案:

  1. 華庭 :《Glibc記憶體管理-Ptmalloc2 原始碼分析》

  2. JeMalloc-UncP 知乎

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2933589/,如需轉載,請註明出處,否則將追究法律責任。

相關文章