36

容器中的JVM资源该如何被安全的限制?

 5 years ago
source link: http://dockone.io/article/8563?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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:

cat /proc/meminfo 

MemTotal:      197869260 kB

MemFree:         3698100 kB

MemAvailable:   62230260 kB

容器:

docker 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的物理机。

#free -g

               total        used        free      shared  buff/cache   available

Mem:            188         122           1           0          64          64

以下的测试中,我们将包含OpenJDK的hotspot虚拟机,IBM的OpenJ9虚拟机。

以下测试中,我们把正确识别到限制的JDK,称之为安全(即不会超出容器限制不会被kill),反之称之为危险。

测试用例1(OpenJDK)

这一组测试我们使用最新的OpenJDK8-12,给容器限制内存为4G,看JDK默认参数下的最大堆为多少?看看我们默认参数下多少版本的JDK是安全的

命令如下,如果你也想试试看,可以用一下命令。

docker 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) 危险。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:8-jre-slim java  -XshowSettings:vm  -version



VM settings:

Max. Heap Size (Estimated): 26.67G

Ergonomics Machine Class: server

Using VM: OpenJDK 64-Bit Server VM



openjdk version "1.8.0_181"

OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)

OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

OpenJDK8 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正确的识别容器限制,910.50M)安全。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:8-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm  -version

VM settings:

Max. Heap Size (Estimated): 910.50M

Ergonomics Machine Class: server

Using VM: OpenJDK 64-Bit Server VM



openjdk version "1.8.0_181"

OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)

OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

OpenJDK 9(并没有识别容器限制,26.67G)危险。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:9-jre-slim java  -XshowSettings:vm  -version

VM settings:

Max. Heap Size (Estimated): 29.97G

Using VM: OpenJDK 64-Bit Server VM



openjdk version "9.0.4"

OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)

OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)

OpenJDK 9 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap(正确的识别容器限制,1G)安全。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:9-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm  -version

VM settings:

Max. Heap Size (Estimated): 1.00G

Using VM: OpenJDK 64-Bit Server VM



openjdk version "9.0.4"

OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)

OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)

OpenJDK 10(正确的识别容器限制,1G)安全。

[root@xiaoke-test ~]# docker run -m 32GB --rm  openjdk:10-jre-slim java -XshowSettings:vm -XX:MaxRAMFraction=1  -version

VM settings:

Max. Heap Size (Estimated): 1.00G

Using VM: OpenJDK 64-Bit Server VM



openjdk version "10.0.2" 2018-07-17

OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)

OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)

OpenJDK 11(正确的识别容器限制,1G)安全。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:11-jre-slim java -XshowSettings:vm  -version

VM settings:

Max. Heap Size (Estimated): 1.00G

Using VM: OpenJDK 64-Bit Server VM



openjdk version "11.0.1" 2018-10-16

OpenJDK Runtime Environment (build 11.0.1+13-Debian-3)

OpenJDK 64-Bit Server VM (build 11.0.1+13-Debian-3, mixed mode, sharing)

OpenJDK 12(正确的识别容器限制,1G)安全。

[root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:12 java -XshowSettings:vm  -version

VM settings:

Max. Heap Size (Estimated): 1.00G

Using VM: OpenJDK 64-Bit Server VM



openjdk version "12-ea" 2019-03-19

OpenJDK Runtime Environment (build 12-ea+23)

OpenJDK 64-Bit Server VM (build 12-ea+23, mixed mode, sharing)

测试用例2(IBM OpenJ9)

docker run -m 4GB --rm  adoptopenjdk/openjdk8-openj9:alpine-slim  java -XshowSettings:vm  -version

docker run -m 4GB --rm  adoptopenjdk/openjdk9-openj9:alpine-slim  java -XshowSettings:vm  -version

docker run -m 4GB --rm  adoptopenjdk/openjdk10-openj9:alpine-slim  java -XshowSettings:vm  -version

docker run -m 4GB --rm  adoptopenjdk/openjdk11-openj9:alpine-slim  java -XshowSettings:vm  -version

OpenJDK8-OpenJ9(正确的识别容器限制,3G)安全。

[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)安全。

[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)安全。

[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)安全。

[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)

分析

分析之前我们先了解这么一个情况:

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进程自动的发现容器限制。

如果你想要的是JVM进程在容器中安全稳定的运行,不被容器kiil,并且你的JDK版本小于10(大于等于JDK10的版本不需要设置,参考前面的测试)。

你需要额外设置JVM参数-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,即可保证你的Java进程不会因为内存问题被容器Kill。

当然这个方式使用起来简单,可靠,缺点也很明显,资源利用率过低(参考前面的表格MaxRAMFraction=4)。

如果想在基础上我还想提高一些内存资源利用率,并且容器内存为1GB - 4GB,我建议你设置-XX:MaxRAMFraction=2,在大于8G的可以尝试设置-XX:MaxRAMFraction=1(参考上表格)。

手动挡

如果你想要的是手动挡的体验,更加进一步的利用内存资源,那么你可能需要回到手动配置时代-Xmx。

手动挡部分,请可以完全忽略上面我的BB。

上面我们说到了自动挡的配置,用起来很简单很舒服,自动发现容器限制,无需担心和思考去配置-Xmx。

比如你有内存1G那么我建议你的-Xmx750M,2G建议配置-Xmx1700M,4G建议配置-Xmx3500-3700M,8G建议设置-Xmx7500-7600M,总之就是至少保留300M以上的内存留给JVM的其他内存。如果堆特别大,可以预留到1G甚至2G。

手动挡用起来就没有那么舒服了,当然资源利用率相对而言就更高了。

原文链接: https://qingmu.io/2018/12/17/H ... iner/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK