在Docker中運行Java:為了防止失敗,你應該知道的


如果你嘗試在容器中運行Java程序,或者專注於Docker,你可能會遇到一些關於JVM和堆大小的問題。本篇文章將介紹如何解決這些問題。

很多開發者會(或者應該)知道,當我們為運行在Linux容器(docker, rkt, runC, lxcfs, etc,)中的Java程序去設置JVM的GC、堆大小和運行時編譯器的參數時並沒有得到預想的效果。當我們通過“java -jar mypplication-fat.jar”的方式而不設置任何參數來運行一個Java應用時,JVM會根據自身的許多參數進行調整,以便在執行環境中獲得最優的性能。

本篇博客將通過簡單的方式向開發人員展示在將Java應用運行在Linux容器內時需要了解的內容。

我們傾向於認為容器可以像虛擬機一樣可以完整的定義虛擬機的CPU個數和虛擬機的內存。容器更像是一個進程級別的資源(CPU、內存、文件系統、網絡等)隔離。這種隔離是依賴於Linux內核中提供的一個 cgroups 的功能。

然而,一些可以從運行時環境中收集信息的應用程序在cgroups功能出現之前已經存在。在容器中執行命令 ‘top‘, ‘free‘, ‘ps’,也包括沒有經過優化的JVM是一個會受到高限制的Linux進程。讓我們來驗證一下。

問題

為了展示遇到的問題,我使用命令“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在虛擬機中創建了一個具有1GB內存的Docker守護進程,接下來在3個Linux容器中執行命令“free -h”,使其只有100MB的內存和Swap。結果顯示所有的容器總內存是995MB。

 

即使是在 Kubernetes/OpenShift集群中,結果也是類似的。我在一個內存是15G的集群中也執行了命令使得Kubernetes Pod有511MB的內存限制(命令:“kubectl run mycentos –image=centos -it –limits=’memory=512Mi’”),總內存顯示為14GB。

想要知道為什么是這樣的結果,可以去閱讀此篇博客文章 “ Memory inside Linux containers – Or why don’t free and top work in a Linux container? ”

我們需要知道Docker參數 (-m, –memory和–memory-swap)和Kubernetes參數 (–limits)會讓Linux內核在一個進程的內存超出限制時將其Kill掉,但是JVM根本不清楚這個限制的存在,當超過這個限制時,不好的事情發生了!

為了模擬當一個進程超出內存限制時會被殺死的場景,我們可以通過命令“docker run -it –name mywildfly -m=50m jboss/wildfly”在一個容器中運行WildFly Application Server並且為其限制內存大小為50MB。在這個容器運行期間,我們可以執行命令“docker stats”來查看容器的限制。

 

但是過了幾秒之后,容器Wildfly將會被中斷並且輸出信息:*** JBossAS process (55) received KILL signal ***

通過命令 “docker inspect mywildfly -f ‘{{json .State}}'”可以查看容器被殺死的原因是發生了OOM(內存不足)。容器中的“state”被記錄為OOMKilled=true 。

 

這將怎樣影響Java應用

在Docker宿主機中創建一個具有1GB內存的虛擬機(在之前使用命令已經創建完畢 “docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”) ,並且限制一個容器的內存為150M,看起來已經足夠運行這個在 Dockerfile中設置過參數-XX: PrintFlagsFinal 和 -XX: PrintGCDetails的Spring Boot application了。這些參數使得我們可以讀取JVM的初始化參數並且獲得 Garbage Collection (GC)的運行詳細情況。

嘗試一下:

$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m 150M rafabene/java-container:openjdk 

我也提供了一個訪問接口“/api/memory/”來使用String對象加載JVM內存,模擬大量的消耗內存,可以調用試試:

$ curl http://X 41X:8080/api/memory 

這個接口將會返回下面的信息 “Allocated more than 80% (219.8 MiB) of the max allowed JVM memory size (241.7 MiB)”

在這里我們至少有2個問題:

為什么JVM會允許241.7MiB的最大內容?

如果容器已經限制了內存為150MB,為什么允許Java分配內存到220MB?

首先,我們應該重新了解在 JVM ergonomic page 中所描述的 “maximum heap size”的定義,它將會使用1/4的物理內存。JVM並不知道它運行在一個容器中,所以它將被允許使用260MB的最大堆大小。通過添加容器初始化時的參數-XX: PrintFlagsFinal,我們可以檢查這個參數的值。

$ docker logs mycontainer150|grep -i MaxHeapSize  

uintx MaxHeapSize := 262144000 {product} 

其次,我們應該理解當在docker命令行中設置了 “-m 150M”參數時,Docker守護進程會限制RAM為150M並且Swap為150M。從結果上看,一個進程可以分配300M的內存,解釋了為什么我們的進程沒有收到任何從Kernel中發出的退出信號。

更多的關於Docker命令中內存限制 (–memory)和Swap (–memory-swap)的差別可以參考 這里 。

更多的內存是解決方案嗎?

開發者如果不理解問題可能會認為運行環境中沒有為JVM提供足夠的內存。通常的解決對策就是為運行環境提供更多的內存,但是實際上,這是一個錯誤的認識。

假如我們將Docker Machine的內存從1GB提高到8GB(使用命令 “docker-machine create -d virtualbox –virtualbox-memory ‘8192’ docker8192”),並且創建的容器從150M到800M:

$ docker run -it --name mycontainer -p 8080:8080 -m 800M rafabene/java-container:openjdk 

此時使用命令 “curl http://X 58X:8080/api/memory” 還不能返回結果,因為在一個擁有8GB內存的JVM環境中經過計算的MaxHeapSize大小是2092957696(~ 2GB)。可以使用命令“docker logs mycontainer|grep -i MaxHeapSize”查看。

 

應用將會嘗試分配超過1.6GB的內存,當超過了容器的限制(800MB的RAM 800MB的Swap),進程將會被kill掉。

很明顯當在容器中運行程序時,通過增加內存和設置JVM的參數不是一個好的方式。當在一個容器中運行Java應用時,我們應該基於應用的需要和容器的限制來設置最大堆大小(參數:-Xmx)。

解決方案是什么?

在Dockerfile中稍作修改,為JVM指定擴展的環境變量。修改內容如下:

CMD java -XX: PrintFlagsFinal -XX: PrintGCDetails $JAVA_OPTIONS -jar java-container.jar 

現在我們可以使用JAVA_OPTIONS的環境變量來設置JVM Heap的大小。300MB看起來對應用足夠了。稍后你可以查看日志,看到Heap的值是 314572800 bytes ( 300MBi)。

Docker下,可以使用“-e”的參數來設置環境變量進行切換。

$ 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]”來設置環境變量進行切換:

$ 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}  

還能再改進嗎?

有什么辦法可以根據容器的限制來自動計算Heap的值?

事實上如果你的基礎Docker鏡像使用的是由Fabric8提供的,那么就可以實現。鏡像fabric8/java-jboss-openjdk8-jdk使用了腳本來計算容器的內存限制,並且使用50%的內存作為上限。也就是有50%的內存可以寫入。你也可以使用這個鏡像來開/關調試、診斷或者其他更多的事情。讓我們看一下一個Spring Boot應用的 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/ 

就這樣!現在,不管容器的內存限制如何,我們的Java應用將在容器中自動的調節Heap大小,而不是再根據宿主機來設置。

在Docker中運行Java:為了防止失敗,你應該知道的

總結到目前為止,Java JVM還不能意識到其是運行在一個容器中 — 某些資源在內存和CPU的使用上會受到限制。因此,你不能讓JVM自己來設置其認為的最優的最大Heap值。

一個解決對策是使用Fabric8作為基礎鏡像,它可以意識到應用程序運行在一個受限制的容器中,並且在你沒有做任何事情的情況下,可以自動的調整最大Heap的值。

在JDK9中已經開始進行嘗試在容器 (i.e. Docker)環境中為JVM提供cgroup功能的內存限制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM