容器中的JVM資源該如何被安全的限制?
前言
Java與Docker的結合,雖然更好的解決了application的封裝問題。但也存在着不兼容,比如Java並不能自動的發現Docker設置的內存限制,CPU限制。
這將導致JVM不能穩定服務業務!容器會殺死你JVM進程,而健康檢查又將拉起你的JVM進程,進而導致你監控你的pod一天重啟次數甚至能達到幾百次。
我們希望當Java進程運行在容器中時,java能夠自動識別到容器限制,獲取到正確的內存和CPU信息,而不用每次都需要在kubernetes的yaml描述文件中顯示的配置完容器,還需要配置JVM參數。
使用JVM MaxRAM參數或者解鎖實驗特性的JVM參數,升級JDK到10+,我們可以解決這個問題(也許吧~.~)。
首先Docker容器本質是是宿主機上的一個進程,它與宿主機共享一個/proc目錄,也就是說我們在容器內看到的/proc/meminfo
,/proc/cpuinfo
與直接在宿主機上看到的一致,如下。
-
Host
1
2
3
4cat /proc/meminfo
MemTotal: 197869260 kB
MemFree: 3698100 kB
MemAvailable: 62230260 kB -
容器
1
2
3
4docker run -it --rm alpine cat /proc/meminfo
MemTotal: 197869260 kB
MemFree: 3677800 kB
MemAvailable: 62210088 kB
那么Java是如何獲取到Host的內存信息的呢?沒錯就是通過/proc/meminfo
來獲取到的。
默認情況下,JVM的Max Heap Size是系統內存的1/4,假如我們系統是8G,那么JVM將的默認Heap≈2G。
Docker通過CGroups完成的是對內存的限制,而/proc目錄是已只讀形式掛載到容器中的,由於默認情況下Java
壓根就看不見CGroups的限制的內存大小,而默認使用/proc/meminfo
中的信息作為內存信息進行啟動,
這種不兼容情況會導致,如果容器分配的內存小於JVM的內存,JVM進程會被理解殺死。
內存限制不兼容
我們首先來看一組測試,這里我們采用一台內存為188G的物理機。
1 |
|
以下的測試中,我們將包含openjdk的hotspot虛擬機,IBM的openj9虛擬機。
以下測試中,我們把正確識別到限制的jdk,稱之為安全(即不會超出容器限制不會被kill),反之稱之為危險。
測試用例1(OPENJDK)
這一組測試我們使用最新的openjdk8-12,給容器限制內存為4G,看JDK默認參數下的最大堆為多少?看看我們默認參數下多少版本的JDK是安全的
-
命令如下,如果你也想試試看,可以用一下命令。
1
2
3
4
5docker run -m 4GB --rm openjdk:8-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:9-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:10-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:11-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:12 java -XshowSettings:vm -version -
OpenJDK8(並沒有識別容器限制,26.67G) 危險
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:8-jre-slim java -XshowSettings:vm -version |
- OpenJDK8 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正確的識別容器限制,910.50M)安全
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:8-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm -version |
- OpenJDK 9(並沒有識別容器限制,26.67G)危險
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:9-jre-slim java -XshowSettings:vm -version |
- OpenJDK 9 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正確的識別容器限制,1G)安全
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:9-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm -version |
- OpenJDK 10(正確的識別容器限制,1G)安全
1 |
[root@xiaoke-test ~]# docker run -m 32GB --rm openjdk:10-jre-slim java -XshowSettings:vm -XX:MaxRAMFraction=1 -version |
- OpenJDK 11(正確的識別容器限制,1G)安全
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:11-jre-slim java -XshowSettings:vm -version |
- OpenJDK 12(正確的識別容器限制,1G)安全
1 |
[root@xiaoke-test ~]# docker run -m 4GB --rm openjdk:12 java -XshowSettings:vm -version |
測試用例2(IBMOPENJ9)
1 |
docker run -m 4GB --rm adoptopenjdk/openjdk8-openj9:alpine-slim java -XshowSettings:vm -version |
-
openjdk8-openj9 (正確的識別容器限制,3G)安全
1
2
3
4
5
6
7
8
9
10
11
12[root@xiaoke-test ~]# docker run -m 4GB --rm adoptopenjdk/openjdk8-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Ergonomics Machine Class: server
Using VM: Eclipse OpenJ9 VM
openjdk version "1.8.0_192"
OpenJDK Runtime Environment (build 1.8.0_192-b12_openj9)
Eclipse OpenJ9 VM (build openj9-0.11.0, JRE 1.8.0 Linux amd64-64-Bit Compressed References 20181107_95 (JIT enabled, AOT enabled)
OpenJ9 - 090ff9dcd
OMR - ea548a66
JCL - b5a3affe73 based on jdk8u192-b12) -
openjdk9-openj9 (正確的識別容器限制,3G)安全
1
2
3
4
5
6
7
8
9
10
11[root@xiaoke-test ~]# docker run -m 4GB --rm adoptopenjdk/openjdk9-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "9.0.4-adoptopenjdk"
OpenJDK Runtime Environment (build 9.0.4-adoptopenjdk+12)
Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 9 Linux amd64-64-Bit Compressed References 20180814_248 (JIT enabled, AOT enabled)
OpenJ9 - 24e53631
OMR - fad6bf6e
JCL - feec4d2ae based on jdk-9.0.4+12) -
openjdk10-openj9 (正確的識別容器限制,3G)安全
1
2
3
4
5
6
7
8
9
10
11[root@xiaoke-test ~]# docker run -m 4GB --rm adoptopenjdk/openjdk10-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "10.0.2-adoptopenjdk" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2-adoptopenjdk+13)
Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 10 Linux amd64-64-Bit Compressed References 20180813_102 (JIT enabled, AOT enabled)
OpenJ9 - 24e53631
OMR - fad6bf6e
JCL - 7db90eda56 based on jdk-10.0.2+13) -
openjdk11-openj9(正確的識別容器限制,3G)安全
1
2
3
4
5
6
7
8
9
10
11[root@xiaoke-test ~]# docker run -m 4GB --rm adoptopenjdk/openjdk11-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.1+13)
Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.11.0, JRE 11 Linux amd64-64-Bit Compressed References 20181020_70 (JIT enabled, AOT enabled)
OpenJ9 - 090ff9dc
OMR - ea548a66
JCL - f62696f378 based on jdk-11.0.1+13)
分析
分析之前我們先了解這么一個情況:
1 |
JavaMemory (MaxRAM) = 元數據+線程+代碼緩存+OffHeap+Heap... |
一般我們都只配置Heap即使用-Xmx來指定JVM可使用的最大堆。而JVM默認會使用它獲取到的最大內存的1/4作為堆的原因也是如此。
安全性(即不會超過容器限制被容器kill)
OpenJdk
OpenJdk8-12,都能保證這個安全性的特點(8和9需要特殊參數,-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap)。
OpenJ9
2.IbmOpenJ9所有的版本都能識別到容器限制。
資源利用率
OpenJdk
自動識別到容器限制后,OpenJdk把最大堆設置為了大概容器內存的1/4,對內存的浪費不可謂不大。
當然可以配合另一個JVM參數來配置最大堆。-XX:MaxRAMFraction=int
。下面是我整理的一個常見內存設置的表格,
從中我們可以看到似乎JVM默認的最大堆的取值為MaxRAMFraction=4
,隨着內存的增加,堆的閑置空間越來越大,在16G容器內存時,java堆只有不到4G。
MaxRAMFraction取值 | 堆占比 | 容器內存=1G | 容器內存=2G | 容器內存=4G | 容器內存=8G | 容器內存=16G |
---|---|---|---|---|---|---|
1 | ≈90% | 910.50M | 1.78G | 3.56G | 7.11G | 14.22G |
2 | ≈50% | 455.50M | 910.50M | 1.78G | 3.56G | 7.11G |
3 | ≈33% | 304.00M | 608.00M | 1.19G | 2.37G | 4.74G |
4 | ≈25% | 228.00M | 455.50M | 910.50M | 1.78G | 3.56G |
OpenJ9
關於OpenJ9的的詳細介紹你可以從這里了解更多。
對於內存利用率OpenJ9的策略是優於OpenJdk的。以下是OpenJ9的策略表格
容器內存<size> |
最大Java堆大小 |
---|---|
小於1 GB | 50%<size> |
1 GB - 2 GB | <size> - 512 MB |
大於2 GB | 大於2 GB |
結論
- 注意:這里我們說的是容器內存限制,和物理機內存不同,
自動檔
- 如果你想要的是,不顯示的指定-Xmx,讓Java進程自動的發現容器限制。
1.如果你想要的是jvm進程在容器中安全穩定的運行,不被容器kiil,並且你的JDK版本小於10(大於等於JDK10的版本不需要設置,參考前面的測試)
你需要額外設置JVM參數-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
,即可保證你的Java進程不會因為內存問題被容器Kill。
當然這個方式使用起來簡單,可靠,缺點也很明顯,資源利用率過低(參考前面的表格MaxRAMFraction=4)。
2.如果想在基礎上我還想提高一些內存資源利用率,並且容器內存為1 GB - 4 GB,我建議你設置-XX:MaxRAMFraction=2
,在大於8G的可以嘗試設置-XX:MaxRAMFraction=1
(參考上表格)。
手動擋
- 如果你想要的是手動擋的體驗,更加進一步的利用內存資源,那么你可能需要回到手動配置時代-Xmx。
- 手動擋部分,請可以完全忽略上面我的BB。
1.上面的我們說到了自動擋的配置,用起來很簡單很舒服,自動發現容器限制,無需擔心和思考去配置-Xmx。
2.比如你有內存1G那么我建議你的-Xmx750M,2G建議配置-Xmx1700M,4G建議配置-Xmx3500-3700M,8G建議設置-Xmx7500-7600M,
總之就是至少保留300M以上的內存留給JVM的其他內存。如果堆特別大,可以預留到1G甚至2G。
3.手動擋用起來就沒有那么舒服了,當然資源利用率相對而言就更高了