背景:
業務的容器化剛剛搞完,線上開始告警,容器重新開機,容器重新開機。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 記憶體結構:

JVM記憶體結構主要有三大塊:堆記憶體、方法區和棧
堆記憶體是JVM中最大的一塊由年輕代和老年代組成,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來配置設定;
方法區存儲類資訊、常量、靜态變量等資料,是線程共享的區域,為與Java堆區分,方法區還有一個别名Non-Heap(非堆);
棧又分為java虛拟機棧和本地方法棧主要用于方法的執行。
那麼有沒有辦法能讓java 正确識别容器的記憶體限制呢?這裡有三種方法:
更新java版本。Java 10支援開箱即用的容器,它将查找linux cgroup資訊。這允許JVM基于容器限制進行垃圾收集。預設情況下使用标志打開它。
-XX:+UseContainerSupport
值得慶幸的是,其中一些功能已被移植到8u131和9以後。可以使用以下标志打開它們。
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
LXCFS,FUSE filesystem for LXC是一個常駐服務,它啟動以後會在指定目錄中自行維護與上面列出的/proc目錄中的檔案同名的檔案,容器從lxcfs維護的/proc檔案中讀取資料時,得到的是容器的狀态資料,而不是整個主控端的狀态。 這樣。java程序讀取到的就是容器的limit 限制。而不是主控端記憶體
-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes` 通過MaxRAM 參數讀取預設的limit限制作為java 記憶體的最大可用記憶體。同時結合-Xmx 設定堆記憶體大小