Java容器环境支持
本文简要记录了本人前段时间KubeCon Meetup分享的一部分,主要介绍三个方面:
第一,历史版本Java在Docker等容器环境(后面就简单以Docker为例)暴露出的局限性。
第二,Java新版本和未来版本对容器环境的支持。
第三,工程实践的一些选择。
首先,我们来看看Java在容器环境为什么和有哪些局限性。从技术角度看,Docker并不是虚拟机层面的完全虚拟化技术,而更是一种轻量级的隔离技术。Docker隔离基于namespace和cgroup,在Linux内核之上实现了有限的隔离和虚拟化。本文不是Docker文章,如果想了解更多细节请参考相关技术文档,我们还是关注Java本身。
有人曾经说,“幸运的是Docker没有完全隐藏硬件等底层信息,但是不幸的也是Docker没有隐藏硬件等底层信息”,对于Java平台来说,这些未隐藏的区别带来了很多新问题,主要体现在两方面:
第一,容器环境对于计算资源的管理方式是全新的,而不是像虚拟机一样相对透明的。 Cgroup作为相对比较新的技术,历史版本的Java平台显然并不能自然地理解相应的资源限制。
第二,namespace对于容器内的应用细节增加了一些微妙的差异,比如jcmd, jstack等工具可能会依赖于/proc/
/下面提供的部分信息,但是docker环境改变或者增加了一些新的东西,某些信息可能通过新的途径暴露出来。
在这篇文章中 “Java in side docker what youmust know to not fail”,对此进行了更加详细的介绍。
https://dzone.com/articles/java-inside-docker-what-you-must-know-to-not-fail
用户可能在生产环境中,比如,下面Docker命令:
$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m150M rafabene/java-container:openjdk
利用‘-m’限定了容器环境内存大小,但是测试中的Java版本显然不能正确处理这个限制。同理,如果我们设置了CPU个数或者时间片的限制,历史版本的Java也是不能正常识别的。
这种“沟通障碍”会从不同层面影响Java应用:
第一,Java可能会试图分配大于限制的内存或者争抢过多CPU。
第二,不能友好的处理OutOfMemoryError。有的工程师为保证服务的可用性,可能会依赖于“-XX:OnOutOfMemoryError”,采取一些补救措施,比如重启服务等。但是由于已经过度提交内存,这种基于fork的实现很可能是不能良好工作的。
第三,即使是未发生OOM,也可能会影响Java的表现。即使未明确指定,Java启动时会根据系统指标,选择一些已经在通用场景中广泛验证的初始值。具体请参考:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html
但是由于容器环境的差异,Java的判断可能是基于错误信息的做出的。这也许就解释了,为什么Docker在Swap使用上似乎特别“欺负”Java。
针对这种情况,Java 9中引入了一些实验性的参数,以方便Docker和Java“沟通”,针对内存限制,可以使用下面的参数设置:
-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap
注意,这两个参数是顺序敏感的,并且只支持Linux环境。而对于CPU核心数限定,Java被修正可以正确理解设置。
幸运的是,这个修改已经被移植到JDK 8u131,笔者推荐直接获取Oracle官方镜像:
https://store.docker.com/images/oracle-serverjre-8
注意,获取这个景象需要登录Docker,接受使用条款,下面是简单的安装命令:
docker pullstore/oracle/serverjre:8
在镜像中,Java被安装在/usr/java/latest,欢迎大家下载、使用并提供反馈。
在后续开发中的版本,Java容器(Docker)支持已经比较完善,默认Java就会自适应各种资源限制和实现差异。前面提到的实验性参数“UseCGroupMemoryLimitForHeap”已经被deprecated。
如果发现实践问题,也可以使用“-XX:-UseContainerSupport”将Java行为回退到容器支持以前。与此同时新增了参数“-XX:ActiveProcessorCount=N ”用以指定CPU core数目。特别要注意的是,这个参数可以使用在任何容器和非容器环境。
前面介绍了相关支持,很有可能用户尚且不能升级到8u131,这种情况下,推荐显示的去设定各种Java启动参数。在Rafael的例子中,分别介绍了具体设置方法,如Docker:
docker run -d --name mycontainer8g -p 8080:8080 -m 800M -e JAVA_OPTIONS='-Xmx300m' rafabene/java-container:openjdk-env
或者Kubernetes:
kubectl run mycontainer --image=rafabene/java-container:openjdk-env --limits='memory=800Mi' --env="JAVA_OPTIONS='-Xmx300m'"
这里也有一些可能的麻烦事儿。比如,Xmx参数可能符合大多数应用的需求,但是HEAP毕竟不是Java进程内存的全部,全面了解Java的各种内存使用是需要比较多的专业知识和技能的,还有一个参数可以有帮助,如设置MaxRAM:
-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes`
如果应用需要显式设置更多Java启动参数,情况就更加复杂了,可以试试其他工具,如CloudFoundary提供的Cloud Foundry JVM Memory Calculator
https://github.com/cloudfoundry/java-buildpack-memory-calculator
好了,祝大家圣诞快乐,最近工作较忙,时间仓促,希望能对大家有些帮助。
听说不写JVM源代码的文章都是很Low的,笔者比较懒,同时,个人认为文档和spec是比较好的沟通方式,依赖于实现细节可能会产生类似使用内部API的问题,后续修改可能会影响应用中基于行为的假设。如果确实认为源码有必要,请提供反馈,欢迎指正。
参考:
Rafael的文章: https://dzone.com/articles/java-inside-docker-what-you-must-know-to-not-fail
CGroup实验性增强: https://bugs.openjdk.java.net/browse/JDK-8170888
进一步改进https://bugs.openjdk.java.net/browse/JDK-8146115
领取专属 10元无门槛券
私享最新 技术干货