作者:Rafael Benevides 譯者:李強(qiáng) 原文鏈接
如果你嘗試在容器中運(yùn)行Java程序茎芭,或者專注于Docker犀呼,你可能會(huì)遇到一些關(guān)于JVM和堆大小的問(wèn)題炭菌。本篇文章將介紹如何解決這些問(wèn)題。
很多開(kāi)發(fā)者會(huì)(或者應(yīng)該)知道旷坦,當(dāng)我們?yōu)檫\(yùn)行在Linux容器(docker, rkt, runC, lxcfs, etc,)中的Java程序去設(shè)置JVM的GC辆琅、堆大小和運(yùn)行時(shí)編譯器的參數(shù)時(shí)并沒(méi)有得到預(yù)想的效果扬霜。當(dāng)我們通過(guò)“java -jar mypplication-fat.jar”的方式而不設(shè)置任何參數(shù)來(lái)運(yùn)行一個(gè)Java應(yīng)用時(shí)余蟹,JVM會(huì)根據(jù)自身的許多參數(shù)進(jìn)行調(diào)整,以便在執(zhí)行環(huán)境中獲得最優(yōu)的性能鲤嫡。
本篇博客將通過(guò)簡(jiǎn)單的方式向開(kāi)發(fā)人員展示在將Java應(yīng)用運(yùn)行在Linux容器內(nèi)時(shí)需要了解的內(nèi)容。
我們傾向于認(rèn)為容器可以像虛擬機(jī)一樣可以完整的定義虛擬機(jī)的CPU個(gè)數(shù)和虛擬機(jī)的內(nèi)存丛肢。容器更像是一個(gè)進(jìn)程級(jí)別的資源(CPU、內(nèi)存颠蕴、文件系統(tǒng)员舵、網(wǎng)絡(luò)等)隔離韭邓。這種隔離是依賴于Linux內(nèi)核中提供的一個(gè)cgroups的功能。
然而袜茧,一些可以從運(yùn)行時(shí)環(huán)境中收集信息的應(yīng)用程序在cgroups功能出現(xiàn)之前已經(jīng)存在裳凸。在容器中執(zhí)行命令 ‘top‘, ‘free‘, ‘ps’件甥,也包括沒(méi)有經(jīng)過(guò)優(yōu)化的JVM是一個(gè)會(huì)受到高限制的Linux進(jìn)程。讓我們來(lái)驗(yàn)證一下悦陋。
問(wèn)題
為了展示遇到的問(wèn)題俺驶,我使用命令“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在虛擬機(jī)中創(chuàng)建了一個(gè)具有1GB內(nèi)存的Docker守護(hù)進(jìn)程,接下來(lái)在3個(gè)Linux容器中執(zhí)行命令“free -h”棍辕,使其只有100MB的內(nèi)存和Swap暮现。結(jié)果顯示所有的容器總內(nèi)存是995MB。
即使是在 Kubernetes/OpenShift集群中楚昭,結(jié)果也是類(lèi)似的栖袋。我在一個(gè)內(nèi)存是15G的集群中也執(zhí)行了命令使得Kubernetes Pod有511MB的內(nèi)存限制(命令:“kubectl run mycentos –image=centos -it –limits=’memory=512Mi’”),總內(nèi)存顯示為14GB抚太。
想要知道為什么是這樣的結(jié)果塘幅,可以去閱讀此篇博客文章 “Memory inside Linux containers – Or why don’t free and top work in a Linux container?”
我們需要知道Docker參數(shù) (-m, –memory and –memory-swap)和Kubernetes參數(shù) (–limits)會(huì)讓Linux內(nèi)核在一個(gè)進(jìn)程的內(nèi)存超出限制時(shí)將其Kill掉,但是JVM根本不清楚這個(gè)限制的存在尿贫,當(dāng)超過(guò)這個(gè)限制時(shí)电媳,不好的事情發(fā)生了!
為了模擬當(dāng)一個(gè)進(jìn)程超出內(nèi)存限制時(shí)會(huì)被殺死的場(chǎng)景帅霜,我們可以通過(guò)命令“docker run -it –name mywildfly -m=50m jboss/wildfly”在一個(gè)容器中運(yùn)行WildFly Application Server并且為其限制內(nèi)存大小為50MB匆背。在這個(gè)容器運(yùn)行期間,我們可以執(zhí)行命令“docker stats”來(lái)查看容器的限制身冀。
但是過(guò)了幾秒之后钝尸,容器Wildfly將會(huì)被中斷并且輸出信息:*** JBossAS process (55) received KILL signal ***
通過(guò)命令 “docker inspect mywildfly -f ‘{{json .State}}'”可以查看容器被殺死的原因是發(fā)生了OOM(內(nèi)存不足)括享。容器中的“state”被記錄為OOMKilled=true 。
這將怎樣影響Java應(yīng)用
在DockerDaemon中創(chuàng)建一個(gè)具有1GB內(nèi)存的Machine(在之前使用命令已經(jīng)創(chuàng)建完畢 “docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”) 珍促,并且限制一個(gè)容器的內(nèi)存為150M铃辖,看起來(lái)已經(jīng)足夠運(yùn)行這個(gè)在 Dockerfile中設(shè)置過(guò)參數(shù)-XX:+PrintFlagsFinal 和 -XX:+PrintGCDetails的Spring Boot application了。這些參數(shù)使得我們可以讀取JVM的初始化參數(shù)并且獲得 Garbage Collection (GC)的運(yùn)行詳細(xì)情況猪叙。
嘗試一下:
$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m 150M rafabene/java-container:openjdk
我也提供了一個(gè)訪問(wèn)接口“/api/memory/”來(lái)使用String對(duì)象加載JVM內(nèi)存娇斩,模擬大量的消耗內(nèi)存,可以調(diào)用試試:
$ curl http://docker-machine ip docker1024
:8080/api/memory
這個(gè)接口將會(huì)返回下面的信息 “Allocated more than 80% (219.8 MiB) of the max allowed JVM memory size (241.7 MiB)”
在這里我們至少有2個(gè)問(wèn)題:
1.為什么JVM會(huì)允許241.7MiB的最大內(nèi)容穴翩?
2.如果容器已經(jīng)限制了內(nèi)存為150MB犬第,為什么允許Java分配內(nèi)存到220MB?
首先芒帕,我們應(yīng)該重新了解在JVM ergonomic page中所描述的 “maximum heap size”的定義歉嗓,它將會(huì)使用1/4的物理內(nèi)存。JVM并不知道它運(yùn)行在一個(gè)容器中背蟆,所以它將被允許使用260MB的最大堆大小鉴分。通過(guò)添加容器初始化時(shí)的參數(shù)-XX:+PrintFlagsFinal,我們可以檢查這個(gè)參數(shù)的值带膀。
$ docker logs mycontainer150|grep -i MaxHeapSize
uintx MaxHeapSize := 262144000 {product}
其次志珍,我們應(yīng)該理解當(dāng)在docker命令行中設(shè)置了 “-m 150M”參數(shù)時(shí),Docker守護(hù)進(jìn)程會(huì)限制RAM為150M并且Swap為150M垛叨。從結(jié)果上看伦糯,一個(gè)進(jìn)程可以分配300M的內(nèi)存,解釋了為什么我們的進(jìn)程沒(méi)有收到任何從Kernel中發(fā)出的退出信號(hào)嗽元。
更多的關(guān)于Docker命令中內(nèi)存限制 (–memory)和Swap (–memory-swap)的差別可以參考這里舔株。
更多的內(nèi)存是解決方案嗎?
開(kāi)發(fā)者如果不理解問(wèn)題可能會(huì)認(rèn)為運(yùn)行環(huán)境中沒(méi)有為JVM提供足夠的內(nèi)存。通常的解決對(duì)策就是為運(yùn)行環(huán)境提供更多的內(nèi)存还棱,但是實(shí)際上,這是一個(gè)錯(cuò)誤的認(rèn)識(shí)惭等。
假如我們將Docker Machine的內(nèi)存從1GB提高到8GB(使用命令 “docker-machine create -d virtualbox –virtualbox-memory ‘8192’ docker8192”)珍手,并且創(chuàng)建的容器從150M到800M:
$ docker run -it --name mycontainer -p 8080:8080 -m 800M rafabene/java-container:openjdk
此時(shí)使用命令 “curl http://docker-machine ip docker8192
:8080/api/memory” 還不能返回結(jié)果,因?yàn)樵谝粋€(gè)擁有8GB內(nèi)存的JVM環(huán)境中經(jīng)過(guò)計(jì)算的MaxHeapSize大小是2092957696(~ 2GB)辞做×找可以使用命令“docker logs mycontainer|grep -i MaxHeapSize”查看。
應(yīng)用將會(huì)嘗試分配超過(guò)1.6GB的內(nèi)存秤茅,當(dāng)超過(guò)了容器的限制(800MB的RAM + 800MB的Swap)稚补,進(jìn)程將會(huì)被kill掉。
很明顯當(dāng)在容器中運(yùn)行程序時(shí)框喳,通過(guò)增加內(nèi)存和設(shè)置JVM的參數(shù)不是一個(gè)好的方式课幕。當(dāng)在一個(gè)容器中運(yùn)行Java應(yīng)用時(shí)厦坛,我們應(yīng)該基于應(yīng)用的需要和容器的限制來(lái)設(shè)置最大堆大小(參數(shù):-Xmx)乍惊。
解決方案是什么?
在 Dockerfile 中稍作修改杜秸,為JVM指定擴(kuò)展的環(huán)境變量。修改內(nèi)容如下:
CMD java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar java-container.jar
現(xiàn)在我們可以使用JAVA_OPTIONS的環(huán)境變量來(lái)設(shè)置JVM Heap的大小润绎。300MB看起來(lái)對(duì)應(yīng)用足夠了撬碟。稍后你可以查看日志,看到Heap的值是 314572800 bytes ( 300MBi)莉撇。
Docker下呢蛤,可以使用“-e”的參數(shù)來(lái)設(shè)置環(huán)境變量進(jìn)行切換。
$ docker run -d --name mycontainer8g -p 8080:8080 -m 800M -e JAVA_OPTIONS='-Xmx300m' rafabene/java-container:openjdk-env
$ docker logs mycontainer8g|grep -i MaxHeapSize
uintx MaxHeapSize := 314572800 {product}
在 Kubernetes 中棍郎,可以使用“–env=[key=value]”來(lái)設(shè)置環(huán)境變量進(jìn)行切換:
$ kubectl run mycontainer --image=rafabene/java-container:openjdk-env --limits='memory=800Mi' --env="JAVA_OPTIONS='-Xmx300m'"
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
mycontainer-2141389741-b1u0o 1/1 Running 0 6s
$ kubectl logs mycontainer-2141389741-b1u0o|grep MaxHeapSize
uintx MaxHeapSize := 314572800 {product}
還能再改進(jìn)嗎其障?
有什么辦法可以根據(jù)容器的限制來(lái)自動(dòng)計(jì)算Heap的值?
事實(shí)上如果你的基礎(chǔ)Docker鏡像使用的是由Fabric8提供的坝撑,那么就可以實(shí)現(xiàn)静秆。鏡像fabric8/java-jboss-openjdk8-jdk使用了腳本來(lái)計(jì)算容器的內(nèi)存限制,并且使用50%的內(nèi)存作為上限巡李。也就是有50%的內(nèi)存可以寫(xiě)入抚笔。你也可以使用這個(gè)鏡像來(lái)開(kāi)/關(guān)調(diào)試、診斷或者其他更多的事情侨拦。讓我們看一下一個(gè)Spring Boot應(yīng)用的 Dockerfile文件:
FROM fabric8/java-jboss-openjdk8-jdk:1.2.3
ENV JAVA_APP_JAR java-container.jar
ENV AB_OFF true
EXPOSE 8080
ADD target/$JAVA_APP_JAR /deployments/
就這樣殊橙!現(xiàn)在,不管容器的內(nèi)存限制如何狱从,我們的Java應(yīng)用將在容器中自動(dòng)的調(diào)節(jié)Heap大小膨蛮,而不是再根據(jù)宿主機(jī)來(lái)設(shè)置。
總結(jié)
到目前為止季研,Java JVM還不能意識(shí)到其是運(yùn)行在一個(gè)容器中 — 某些資源在內(nèi)存和CPU的使用上會(huì)受到限制敞葛。因此,你不能讓JVM自己來(lái)設(shè)置其認(rèn)為的最優(yōu)的最大Heap值与涡。
一個(gè)解決對(duì)策是使用Fabric8作為基礎(chǔ)鏡像惹谐,它可以意識(shí)到應(yīng)用程序運(yùn)行在一個(gè)受限制的容器中,并且在你沒(méi)有做任何事情的情況下驼卖,可以自動(dòng)的調(diào)整最大Heap的值氨肌。
在JDK9中已經(jīng)開(kāi)始進(jìn)行嘗試在容器 (i.e. Docker)環(huán)境中為JVM提供cgroup功能的內(nèi)存限制。相關(guān)信息可以查看:http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49