【JVM故障問題排查心得】「記憶體診斷系列」JVM記憶體與Kubernetes中pod的記憶體、容器的記憶體不一致所引發的OOMKilled問題總結(上)

背景介紹

在我們日常的工作當中,通常應用都會採用Kubernetes進行容器化部署,但是總是會出現一些問題,例如,JVM堆小於Docker容器中設置的記憶體大小和Kubernetes的記憶體大小,但是還是會被OOMKilled。在此我們介紹一下K8s的OOMKilled的Exit Code編碼。

Exit Code 137

  • 表明容器收到了 SIGKILL 訊號,進程被殺掉,對應kill -9,引發SIGKILL的是docker kill。這可以由用戶或由docker守護程式來發起,手動執行:docker kill
  • 137比較常見,如果 pod 中的limit 資源設置較小,會運行記憶體不足導致 OOMKilled,此時state 中的 」OOMKilled」 值為true,你可以在系統的dmesg -T 中看到OOM日誌。

為什麼我設置的大小關係沒有錯,還會OOMKilled?

因為我的heap大小肯定是小於Docker容器以及Pod的大小的,為啥還是會出現OOMKilled?

原因分析

這種問題常發生在JDK8u131或者JDK9版本之後所出現在容器中運行JVM的問題:在大多數情況下,JVM將一般默認會採用宿主機Node節點的記憶體為Native VM空間(其中包含了堆空間、直接記憶體空間以及棧空間),而並非是是容器的空間為標準。

例如在我的機器

docker run -m 100MB openjdk:8u121 java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 444.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

以上的資訊出現了矛盾,我們在運行的時候將容器記憶體設置為100MB,而-XshowSettings:vm列印出的JVM將最大堆大小為444M,如果按照這個記憶體進行分配記憶體的話很可能會導致節點主機在某個時候殺死我的JVM。

如何解決此問題

JVM 感知 cgroup 限制

一種方法解決 JVM 記憶體超限的問題,這種方法可以讓JVM自動感知 docker 容器的 cgroup 限制,從而動態的調整堆記憶體大小。JDK8u131在JDK9中有一個很好的特性,即JVM能夠檢測在Docker容器中運行時有多少記憶體可用。為了使jvm保留根據容器規範的記憶體,必須設置標誌-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。

注意:如果將這兩個標誌與Xms和Xmx標誌一起設置,那麼jvm的行為將是什麼?-Xmx標誌將覆蓋-XX:+ UseCGroupMemoryLimitForHeap標誌。

總結一下
  • 標誌-XX:+ UseCGroupMemoryLimitForHeap使JVM可以檢測容器中的最大堆大小。

  • -Xmx標誌將最大堆大小設置為固定大小。

  • 除了JVM的堆空間,還會對於非堆和jvm的東西,還會有一些額外的記憶體使用情況。

使用JDK9的容器感知機制嘗試

$ docker run -m 100MB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 44.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

可以看出來通過記憶體感知之後,JVM能夠檢測到容器只有100MB,並將最大堆設置為44M。我們調整一下記憶體大小看看是否可以實現動態化調整和感知記憶體分配,如下所示。

docker run -m 1GB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 228.00M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

我們設置了容器有1GB記憶體分配,而JVM使用228M作為最大堆。因為容器中除了JVM之外沒有其他進程在運行,所以我們還可以進一步擴大一下對於Heap堆的分配?

$ docker run -m 1GB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XX:MaxRAMFraction=1 -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 910.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

在較低的版本的時候可以使用-XX:MaxRAMFraction參數,它告訴JVM使用可用記憶體/MaxRAMFract作為最大堆。使用-XX:MaxRAMFraction=1,我們將幾乎所有可用記憶體用作最大堆。從上面的結果可以看出來記憶體分配已經可以達到了910.50M。

問題分析
  1. 最大堆佔用總記憶體是否仍然會導致你的進程因為記憶體的其他部分(如「元空間」)而被殺死?
  • 答案:MaxRAMFraction=1仍將為其他非堆記憶體留出一些空間。

但如果容器使用堆外記憶體,這可能會有風險,因為幾乎所有的容器記憶體都分配給了堆。您必須將-XX:MaxRAMFraction=2設置為堆只使用50%的容器記憶體,或者使用Xmx

容器內部感知CGroup資源限制

Docker1.7開始將容器cgroup資訊掛載到容器中,所以應用可以從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取記憶體、 CPU等設置,在容器的應用啟動命令中根據Cgroup配置正確的資源設置 -Xmx, -XX:ParallelGCThreads等參數

在Java10中,改進了容器集成。
  • Java10+廢除了-XX:MaxRAM參數,因為JVM將正確檢測該值。在Java10中,改進了容器集成。無需添加額外的標誌,JVM將使用1/4的容器記憶體用於堆。

  • java10+確實正確地識別了記憶體的docker限制,但您可以使用新的標誌MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是舊的MaxRAMFraction,以便更精確地調整堆的大小,而不是其餘的(堆棧、本機…)

  • java10+上的UseContainerSupport選項,而且是默認啟用的,不用設置。同時 UseCGroupMemoryLimitForHeap 這個就棄用了,不建議繼續使用,同時還可以通過 -XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage 這些參數更加細膩的控制 JVM 使用的記憶體比率。

Java 程式在運行時會調用外部進程、申請 Native Memory 等,所以即使是在容器中運行 Java 程式,也得預留一些記憶體給系統的。所以 -XX:MaxRAMPercentage 不能配置得太大。當然仍然可以使用-XX:MaxRAMFraction=1選項來壓縮容器中的所有記憶體。

參考資料

Exit mobile version