標籤:對象 kernel round 解決方案 記憶體不足 程式 groups scale 虛擬
轉自:https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA==&mid=2649693848&idx=1&sn=4e9ef7e2a9d41b39985899b6ad146298&chksm=889321fbbfe4a8ed58d09e6bcf2f9c2603859c331489c0a8a56b8050e601438415b1398fc1f6&mpshare=1&scene=1&srcid=0419cxIPfJTpcCFP1FDn9cSs&key=7bae48d5a88e60c5ec76efea9c2ee66bec96c6478a3e8f6ce4cfe8531486f2f75c26ece7cd8aff63cdca6913da9eea809fe3561f22dc0bcbab552a20cf3c66bceb7faee2afc241d1742d38d396573d90&ascene=0&uin=MTc0NzU1NQ%3D%3D&devicetype=iMac+MacBookPro11%2C3+OSX+OSX+10.12.2+build(16C68)&version=12010210&nettype=WIFI&fontScale=100&pass_ticket=68WphVpsnWDAsG%2BTtici8KYsWYIyYtTAh%2FPGk04mUCw%3D
很多開發人員會(或者應該)知道,當我們為運行在Linux容器(Docker、rkt、runC、lxcfs等)中的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?”(https://fabiokung.com/2014/03/13/memory-inside-linux-containers/)
我們需要知道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://`docker-machine ip docker1024`: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://X51X: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大小,而不是再根據宿主機來設定。
總結到目前為止,Java JVM還不能意識到其是運行在一個容器中 — 某些資源在記憶體和CPU的使用上會受到限制。因此,你不能讓JVM自己來設定其認為的最優的最大Heap值。
一個解決對策是使用Fabric8作為基礎鏡像,它可以意識到應用程式運行在一個受限制的容器中,並且在你沒有做任何事情的情況下,可以自動的調整最大Heap的值。
在JDK9中已經開始進行嘗試在容器(i.e. Docker)環境中為JVM提供cgroup功能的記憶體限制。相關資訊可以查看:http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49
3 天燒腦式Kubernetes訓練營
本次培訓內容包括:Kubernetes概述和架構、部署和核心機制分析、進階篇——Kubernetes工作原理和程式碼分析等,點擊下面圖片即可查看具體培訓內容。
點擊閱讀原文連結可直接報名。
(轉)在Docker中運行Java:為了防止失敗,你需要知道這些