天天看點

Kubernetes之路 1 - Java應用資源限制的迷思

Kubernetes之路 1 - Java應用資源限制的迷思

本系列文章記錄了企業客戶在應用Kubernetes時的一些常見問題

<a href="https://yq.aliyun.com/articles/562440">第一篇:Java應用資源限制的迷思</a>

<a href="https://yq.aliyun.com/articles/566208">第二篇:利用LXCFS提升容器資源可見性</a>

<a href="https://yq.aliyun.com/articles/573791">第三篇:解決服務依賴</a>

随着容器技術的成熟,越來越多的企業客戶在企業中選擇Docker和Kubernetes作為應用平台的基礎。然而在實踐過程中,還會遇到很多具體問題。本系列文章會記錄阿裡雲容器服務團隊在支援客戶中的一些心得體會和最佳實踐。我們也歡迎您通過郵件和釘釘群和我們聯系,分享您的思路和遇到的問題。

在對Java應用容器化部署的過程中,有些同學反映:自己設定了容器的資源限制,但是Java應用容器在運作中還是會莫名奇妙地被OOM Killer幹掉。

這背後一個非常常見的原因是:沒有正确設定容器的資源限制以及對應的JVM的堆空間大小。

下面是一個Kubernetes的Pod的定義描述:

Pod中的<code>app</code>是一個初始化容器,負責把一個JSP應用拷貝到<code>tomcat</code> 容器的 “webapps”目錄下。注: 鏡像中JSP應用index.jsp用于顯示JVM和系統資源資訊。

<code>tomcat</code> 容器會保持運作,而且我們限制了容器最大的記憶體用量為256MB記憶體。

我們執行如下指令來部署、測試應用

我們可以看到HTML格式的系統CPU/Memory等資訊,我們也可以用 html2text 指令将其轉化成為文本格式。

注意:本文是在一個 2C 4G的節點上進行的測試,在不同環境中測試輸出的結果會有所不同

我們可以發現,容器中看到的系統記憶體是 3951MB,而JVM Heap Size最大是 878MB。納尼?!我們不是設定容器資源的容量為256MB了嗎?如果這樣,當應用記憶體的用量超出了256MB,JVM還沒對其進行GC,而JVM程序就會被系統直接OOM幹掉了。

問題的根源在于:

對于JVM而言,如果沒有設定Heap Size,就會按照主控端環境的記憶體大小預設設定自己的最大堆大小。

Docker容器利用CGroup對程序使用的資源進行限制,而在容器中的JVM依然會利用主控端環境的記憶體大小和CPU核數進行預設設定,這導緻了JVM Heap的錯誤計算。

類似,JVM預設的GC、JIT編譯線程數量取決于主控端CPU核數。如果我們在一個節點上運作多個Java應用,即使我們設定了CPU的限制,應用之間依然有可能因為GC線程搶占切換,導緻應用性能收到影響。

了解了問題的根源,我們就可以非常簡單地解決問題了

其用法就是添加如下參數

我們在上文示例的<code>tomcat</code>容器添加環境變量 “JAVA_OPTS”參數

我們部署一個新的Pod,并重複相應的測試

我們看到JVM最大的Heap大小變成了112MB,這很不錯,這樣就能保證我們的應用不會輕易被OOM了。随後問題又來了,為什麼我們設定了容器最大記憶體限制是256MB,而JVM隻給Heap設定了112MB的最大值呢?

這就涉及到JVM的記憶體管理的細節了,JVM中的記憶體消耗包含Heap和Non-Heap兩類;類似Class的元資訊,JIT編譯過的代碼,線程堆棧(thread stack),GC需要的記憶體空間等都屬于Non-Heap記憶體,是以JVM還會根據CGroup的資源限制預留出部分記憶體給Non Heap,來保障系統的穩定。(在上面的示例中我們可以看到,tomcat啟動後Non Heap占用了近32MB的記憶體)

在JDK 8u191和JDK 10之後,社群對JVM在容器中運作做了進一步的優化和增強。JVM可以自動感覺Docker容器内部的CPU和記憶體資源限制。Java程序可用CPU核數由cpu sets, cpu shares 和 cpu quotas等參數計算而來。

設定記憶體

設定 CPU Set

設定 CPU Shares

如需禁止自動資源計算,可以指明如下參數

如果需要指明JVM可用CPU核數,可以指明如下參數

參考資訊

<a href="https://blog.docker.com/2018/04/improved-docker-container-integration-with-java-10/">https://blog.docker.com/2018/04/improved-docker-container-integration-with-java-10/</a>

<a href="https://bugs.openjdk.java.net/browse/JDK-8196595">https://bugs.openjdk.java.net/browse/JDK-8196595</a>

如果無法利用JDK 8/9的新特性,比如還在使用JDK6的老應用,我們還可以在容器内部利用腳本來擷取容器的CGroup資源限制,并通過設定JVM的Heap大小。

Docker1.7開始将容器cgroup資訊挂載到容器中,是以應用可以從 <code>/sys/fs/cgroup/memory/memory.limit_in_bytes</code> 等檔案擷取記憶體、 CPU等設定,在容器的應用啟動指令中根據Cgroup配置正确的資源設定 -Xmx, -XX:ParallelGCThreads等參數

本文分析了Java應用在容器使用中一個常見Heap設定的問題。容器與虛拟機不同,其資源限制通過CGroup來實作。而容器内部程序如果不感覺CGroup的限制,就進行記憶體、CPU配置設定可能導緻資源沖突和問題。

我們可以非常簡單地利用JVM的新特性和自定義腳本來正确設定資源限制。這個可以解決絕大多數資源限制的問題。