原文 https://www.jianshu.com/p/0897d0581872
背景:眾所周知,當我們執行沒有任何調優參數(如“java-jar mypplication-fat.jar”)的 Java 應用程序時,JVM 會自動調整幾個參數,以便在執行環境中具有最佳性能。
但是許多開發者發現,如果讓 JVM ergonomics (即JVM人體工程學,用於自動選擇和行為調整)對垃圾收集器、堆大小和運行編譯器使用默認設置值,運行在 Linux 容器(docker,rkt,runC,lxcfs 等)中的 Java 進程會與我們的預期表現嚴重不符。
本篇文章采用簡單的方法來向開發人員展示在 Linux 容器中打包 Java 應用程序時應該知道什么。
懶人超精簡閱讀版:
a.JVM 做不了內存限制,一旦超出資源限制,容器就會出錯
b.即使你多給些內存資源,也沒什么卵用,只會錯上加錯
c.解決方案:用 Dockfile 中的環境變量來定義 JVM 的額外參數
d.更進一步:使用由 Fabric8 社區提供的基礎 Docker 鏡像來定義 Java 應用程序,將始終根據容器調整堆大小
詳細全文:
我們往往把容器當虛擬機,讓它定義一些虛擬 CPU 和虛擬內存。其實容器更像是一種隔離機制:它可以讓一個進程中的資源(CPU,內存,文件系統,網絡等)與另一個進程中的資源完全隔離。Linux 內核中的 cgroups 功能用於實現這種隔離。
然而,一些從執行環境收集信息的應用程序已經在 cgroups 存在之前就被執行了。“top”,“free”,“ps”,甚至 JVM 等工具都沒有針對在容器內執行高度受限的 Linux 進程進行優化。
1.存在的問題
為了演示,我用“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在1GB RAM的虛擬機中創建了 docker daemon。接下來,在一個虛擬內存為100MB的容器里面跑三個不同的Linux distribution,執行 “free -h”命令,結果是:它們都顯示了995MB的總內存。

即使在 Kubernetes / OpenShift 集群中,結果也類似。
我在一個15GB內存的集群中跑一個 Kubernetes Pod ,並將 Pod 的內存限制為512M (通過“kubectl run mycentos –image=centos -it –limits=’memory=512Mi'”命令實現),最后顯示的總內存卻是14GB。

如果想知道為什么會發生這種情況,建議您閱讀博客“Memoryinside Linux containers – Or why don’t free and top work in a Linux container?”(https://fabiokung.com/2014/03/13/memory-inside-linux-containers/)
docker switches(-m,-memory和-memory-swap)和kubernetes switch(–limits)在進程超過限制的情況下,會指示Linux內核殺死該進程;但JVM是完全不知道限制,所以在進程超過限制的時候,糟糕的事情就發生了!
為了模擬在超過指定的內存限制后被殺死的進程,我們可以通過“docker run -it –name mywildfly -m=50m jboss/wildfly”命令在50MB內存限制的容器中跑WildFly應用server,用“dockerstats”命令來檢查容器限制。

但是在幾秒鍾之后,Wildfly的容器執行將被中斷並顯示:*** JBossAS process (55) received KILL signal ***
“docker inspect mywildfly -f ‘{{json.State}}'”命令顯示由於OOM(內存不足),該容器已被殺死。注意容器“state”中的OOMKilled = true。

2.JAVA的應用程序是如何被影響的?
在docker daemon里用Dockerfile中定義的參數-XX:+ PrintFlagsFinal和-XX:+ PrintGCDetails起一個java應用。
其中 machine:1GB RAM容器內存:限制為150M(對於這個Spring Boot應用,似乎夠用)
這些參數允許我們讀取初始JVM人機工程學參數,並了解有關垃圾收集(GC)執行的詳細信息。
動手試一下:

我已經在“/ api / memory /”上准備了一個端點,它使用String對象加載JVM內存來模擬消耗大量內存的操作。我們來調用一次:

此端點將回復“分配超過80%(219.8 MiB)的最大允許JVM內存大小(241.7 MiB)”
在這里我們可以提至少兩個問題:
為什么JVM最大允許內存241.7 MiB?
如果這個容器將內存限制為150MB,那為什么它允許Java分配近220MB?
首先,我們需要回顧一下JVM人機工程學頁面上關於“最大堆大小”的內容:是物理內存的1/4。由於JVM不知道它在一個容器內執行,所以允許最大堆大小將接近260MB。鑒於我們在容器初始化期間添加了-XX:+ PrintFlagsFinal標志,我們可以檢查這個值:

其次,我們需要了解,當我們在docker命令行中使用參數“-m 150M”時,docker daemon將在RAM中限制150M,在Swap中限制為150M。因此,該過程可以分配300M。這就解釋了為什么我們的進程沒有被殺死。
docker命令行中的內存限制(-memory)和swap(-memory-swap)之間的更多組合可以在這里(https://docs.docker.com/engine/reference/run/#example-run-htop-inside-a-container)找到。
3.提供更多內存是否靠譜?
不了解問題的開發者往往認為環境不能為執行JVM提供足夠的內存。所以通常的解決辦法是提供更多內存,這實際上會使事情變得更糟。
我們假設將daemon從1GB更改為8GB(使用“docker-machinecreate -d virtualbox –virtualbox-memory ‘8192’ docker8192”創建),並將容器內存從150M更改為800M:

請注意這次,“curl http://`docker-machine ipdocker8192`:8080/api/memory”命令甚至沒有執行完,因為在8GB環境中計算的JVM的MaxHeapSize為2092957696字節(〜2GB)。檢查“docker logs mycontainer|grep -i MaxHeapSize”

該應用將嘗試分配超過1.6GB的內存,這超出了此容器的限制(RAM中的800MB + Swap中的800MB),並且該進程將被殺掉。
很顯然,用增加內存且讓JVM自定義參數的方式在容器里跑Java,不是什么好主意。在容器內部運行Java應用程序時,我們應該根據應用程序需求和容器限制設置最大堆大小(-Xmx參數)。
4.解決方案
Dockerfile的一個細微變化允許用戶指定一個環境變量來定義JVM的額外參數。檢查以下行:

現在我們可以使用JAVA_OPTIONS環境變量來通知JVM堆的大小。對於這個應用程序,300M就夠了。稍后可以檢查日志並獲取314572800字節(300MBi)的值
對於docker,您可以使用“-e”switch指定環境變量。

在Kubernetes中,您可以使用switch“-env = [key = value]”設置環境變量:

再進一步
如果可以根據容器限制自動計算堆的值,該怎么做?
使用由Fabric8社區提供的基礎Docker鏡像,就可以搞定。這個鏡像fabric8 / java-jboss-openjdk8-jdk使用一個腳本來計算容器限制,並使用50%的可用內存作為上限。請注意,這個50%的內存比可以被復寫。您還可以使用此鏡像來啟用/禁用調試,診斷等。

下面一起看看Dockerfile是如何作用於這個Spring Boot應用程序:
搞定!現在,無論容器內存限制是多少,我們的Java應用程序將始終根據容器調整堆大小,而不是根據daemon調整堆大小。

5.結論
直到現在,Java JVM依然沒有提供什么支持,讓大家可以理解它在容器內是如何運行的,而且它有一些資源是內存和CPU限制的。因此,您不能讓JVM人體工程學本身決定最大堆大小。
解決此問題的一種方法是使用能夠理解它在受限容器內運行的Fabric8 Base鏡像。
在JVM中有一個實驗支持,已經包含在JDK9中以支持容器(即Docker)環境中的cgroup內存限制。可以參考:http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49
原文評論:更好的方法是以exec表單定義您的CMD指令,這將確保java是PID 1進程-這對於允許Java在容器停止時正常關閉至關重要。
Exec表單不支持環境變量替換,但您可以通過設置JAVA_TOOL_OPTIONS環境變量來傳遞其他命令行標志(請參閱http://bit.ly/2mTIDUt)
作者:Tenxcloud
鏈接:https://www.jianshu.com/p/0897d0581872
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。