容器中的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

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

[[email protected] ~]# 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)安全

[[email protected] ~]# 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)危险

[[email protected] ~]# 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)安全

[[email protected] ~]# 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)安全


[[email protected] ~]# 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)安全

[[email protected] ~]# 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)安全

[[email protected] ~]# 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(IBMOPENJ9)

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

[[email protected] ~]# 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)安全

[[email protected] ~]# 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)安全


[[email protected] ~]# 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)安全

[[email protected] ~]# 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进程自动的发现容器限制。

1.如果你想要的是jvm进程在容器中安全稳定的运行,不被容器kill,并且你的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.手动挡用起来就没有那么舒服了,当然资源利用率相对而言就更高了。

原文地址:http://blog.51cto.com/13981400/2340199

时间: 2024-10-31 07:19:27

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

【原创】大叔经验分享(71)docker容器中使用jvm工具

java应用中经常需要用到jvm工具来进行一些操作,如果java应用部署在docker容器中,如何使用jvm工具? 首先要看使用的docker镜像, 比如常用的openjdk镜像分为jdk和jre,只有jdk版本才有jvm工具,所以可以直接使用jdk版本的openjdk: 比如常用的tomcat镜像则没有jdk和jre选择,默认使用都是jre,所以没有jvm工具,tomcat镜像中的jdk目录如下: # ls /usr/lib/jvm/java-1.8-openjdk bin jre lib #

通过JNDI从服务器容器中获取资源_Spring JNDI+Mysql+Tomcat

通过JNDI从服务器容器中获取DataSource资源 (由容器管理,不要关闭它,容器自己会处理)上一篇我们使用的是dbcp,这里使用JNDI: 使用JNDI连接数据: 在Spring注释 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="userna

EJB和Web容器中的资源或组件是如何查找的?

JavaEE中资源或组件是如何通过JNDI查找的一直都没完全弄清楚,这段时间花时间总算把它弄清楚了,总结如下: (a).JavaEE1.5以前: ServletA.java: InitialContext ic=new InitialContext(); DataSource ds=(DataSource)ic.lookup("java:comp/env/jdbc/FooDS"); web.xml: <resource-ref> <res-ref-name>jd

如何设置Docker容器中Java应用的内存限制

如果使用官方的Java镜像,或者基于Java镜像构建的Docker镜像,都可以通过传递 JAVA_OPTS 环境变量来轻松地设置JVM的内存参数.比如,对于官方Tomcat 镜像,我们可以执行下面命令来启动一个最大内存为512M的tomcat实例 docker run --rm -e JAVA_OPTS='-Xmx512m' tomcat:8 在日志中,我们可以清楚地发现设置已经生效 "Command line argument: -Xmx512m" 02-Apr-2016 12:46

Spring笔记——6.容器中bean的生命周期

spring可以管理单例bean的生命周期,知道何时被创建,核实初始化,何时销毁,也可以进行某些通用资源申请,销毁前资源回收.对于prototype,容器只负责创建,之后就撒手不管自生自灭.容器不知道一共创建了多少prototype,也不知道他们什么时候会被销毁,所以容器没法管理prototype. 管理bean的生命周期有如下两个时机: 注入依赖关系后 即将销毁bean前 依赖关系注入后的行为 通过设定init-method属性指定某个方法或者实现InitializingBean接口指定需要实

容器内部设置JVM的Heap大小

容器内部利用脚本来获取容器的CGroup资源限制,并通过设置JVM的Heap大小. Docker1.7开始将容器cgroup信息挂载到容器中,所以应用可以从 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件获取内存. CPU等设置,在容器的应用启动命令中根据Cgroup配置正确的资源设置 -Xmx, -XX:ParallelGCThreads等参数 Java应用在容器使用中一个常见Heap设置的问题.容器与虚拟机不同,其资源限制通过CGroup来实现

容器中Java 程序OOMKilled原因浅析

背景: 业务的容器化刚刚搞完,线上开始告警,容器重启,容器重启.describe pod 查看原因是OOMKilled 分析: OOMKilled 是pod 中的进程使用的内存超过了.spec.containers[*].resources.limits.memory中定义的内存限制,在超出限制后, kubernetes 会向容器中的进程(pid=1)发送kill -9 信号.kill -9 信号对于进程来说是不可捕捉的,进程无法在收到-9 信号后优雅的退出. 这对于业务来说是有损的.那么为啥进

Cocos2d-JS中使用CocosStudio资源——关卡选择界面

在本篇博客中,我们将通过一个在Cocos2d-JS中使用从CocosStudio导出的关卡选择界面资源的例子,来简要介绍以下内容:Button控件,Text控件,PageView控件,即翻页容器的使用. 一.CocosStudio准备 第一步:从官网下载相应的示例,LevelSelection.效果图如下: 第二步:修改其中的一些控件的名称,方便寻找获取,如: 由于官网给的例子中的控件名字有的较复杂,所以可以按照自己的想法进行修改. 第三步:发布资源.注意发布设置中应该将数据格式改为JSON格式

非阻塞算法在并发容器中的实现【转】

转自:https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/ 非阻塞算法在 Java 中的应用越来越广泛 , ConcurrentLinkedQueue 是 java. concurrent 包中基于非阻塞算法实现的并发容器的典范.通过本文,您将了解非阻塞算法的工作原理及其在 ConcurrentLinkedQueue 中的具体实现机制. 简介 非阻塞算法在更细粒度的层面协调争用,它比传统的锁有更高的并发性.随着非阻塞算法在 Jav