容器中Java 程式OOMKilled原因淺析

itanony發表於2019-06-16

背景:

業務的容器化剛剛搞完,線上開始告警,容器重啟,容器重啟。describe pod 檢視原因是OOMKilled

分析:

OOMKilled 是pod 中的程式使用的記憶體超過了.spec.containers[*].resources.limits.memory中定義的記憶體限制,在超出限制後, kubernetes 會向容器中的程式(pid=1)傳送kill -9 訊號。kill -9 訊號對於程式來說是不可捕捉的,程式無法在收到-9 訊號後優雅的退出。 這對於業務來說是有損的。那麼為啥程式會超過容器的limit 限制呢?
檢視容器中程式的啟動引數:

java -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -XX:MetaspaceSize=128m -jar bxr-web-1.0.jar

檢視容器的limit限制

k8s-master-01#kubectl get pods -n calculation bxr-web-dd656458b-8m4fb -o=custom-columns=name:.metadata.name,namespace:.metadata.namespace,memory-limit:.spec.containers[0].resources.limits.memory

name                      namespace     memory-limit
bxr-web-dd656458b-8m4fb   calculation   2000Mi

程式沒有設定記憶體限制,但是這個業務之前在虛擬機器上執行時,配置相同,啟動引數也是如此,為什麼上線到容器中會經常出現OOMKilled 的情況呢。這裡就需要說到docker對程式資源的限制。

docker 通過 cgroup 來控制容器使用的資源配額,包括 CPU、記憶體、磁碟三大方面,基本覆蓋了常見的資源配額和使用量控制。但是在java 的早期版本中(小於1.8.131),不支援讀取cgroup的限制。 預設是從/proc/目錄讀取可用記憶體。但是容器中的/proc目錄預設是掛載的宿主機的記憶體目錄。即java 讀取的到可用的記憶體是宿主機的記憶體。那麼自然會導致程式超出容器limit 限制的問題。
驗證:

起初, 我們採用為程式設定-Xmx引數來限制程式的最大heap(堆)記憶體。例如。 容器的limit限制為3G。 那麼設定java程式的最大堆記憶體為2.8G,採用這種方式後,容器重啟的情況少了很多,但還是偶爾會出現OOMKilled 的情況。因為-xms 只能設定java程式的堆記憶體。 但是其他非堆記憶體的佔用一旦超過預留的記憶體。還是會被kubernetes kil掉。附java 記憶體結構:
image

JVM記憶體結構主要有三大塊:堆記憶體、方法區和棧

堆記憶體是JVM中最大的一塊由年輕代和老年代組成,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來分配;

方法區儲存類資訊、常量、靜態變數等資料,是執行緒共享的區域,為與Java堆區分,方法區還有一個別名Non-Heap(非堆);

棧又分為java虛擬機器棧和本地方法棧主要用於方法的執行。

那麼有沒有辦法能讓java 正確識別容器的記憶體限制呢?這裡有三種方法:

  1. 升級java版本。Java 10支援開箱即用的容器,它將查詢linux cgroup資訊。這允許JVM基於容器限制進行垃圾收集。預設情況下使用標誌開啟它。
-XX:+UseContainerSupport

值得慶幸的是,其中一些功能已被移植到8u131和9以後。可以使用以下標誌開啟它們。

-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
  1. LXCFS,FUSE filesystem for LXC是一個常駐服務,它啟動以後會在指定目錄中自行維護與上面列出的/proc目錄中的檔案同名的檔案,容器從lxcfs維護的/proc檔案中讀取資料時,得到的是容器的狀態資料,而不是整個宿主機的狀態。 這樣。java程式讀取到的就是容器的limit 限制。而不是宿主機記憶體
  2. -XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes` 通過MaxRAM 引數讀取預設的limit限制作為java 記憶體的最大可用記憶體。同時結合-Xmx 設定堆記憶體大小

相關文章