你的java程序可能并没有正确的跑在容器中

现在越来越多的项目接入docker, 以及一些 swarm,kubernetes这样的编排平台。

java作为程序语言大户,自然少不了和docker对接,那么一个java应用如何快速的接入docker呢?

java应用docker化,如何docker化?

普通的jar包执行, 怎么docker化?

哎呀,吃着葡萄写着文档就是爽(正宗大圩葡萄,有意联系小编~!~)

首先jar要运行必须有jdk/jre环境, docker化的jdk环境官方就已经支持,所以想在docker中把jar调起来. 把jar打入image中,入口点配置为 java -jar xx.jar即可。

那么springboot这样的应用怎么docker话呢? 一模一样。我们可以构造如下dockerfile文件

途中 arya-service为我们的一个spring boot应用结构基本如下:

Startup.sh 内容很多都是项目独立配置但是基本如下:

当我们非docker打包运行的时候,一般是类似在机器上执行后台程序比如nohup将java进程后台运行;

所以startup.sh执行完就结束了,但是会fork一个后台进程来跑java程序。

而docker是需要一个常驻的前台进程作为主进程,所以startup.sh 并不适合做docker的入口成宿,我们需要对startup.sh做一个简单的改造。

改造为main.sh如下:

可以看到 我们在main.sh中做了一些环境变量的配置,同事我们执行了java -jar 但是并没有将其后台运行。

这样在dockerfile中,我们把main.sh作为 docker的 入口点(Entrypoint),docker入口点的作用是,当用镜像启动一个实例容器是,容器默认会执行入口点程序(Entrypoint),具有类似作用的还有dockerfile中的 cmd,CMD 和 Entrypoint在Dockerfile中同时作用,一般CMD会做为Entrypoint的参数,具体可看官方文档。

dockerfile搞定后,一个应用的docker化便完成了一小半,其次就要去验证dockerfile打出的镜像是否满足需要,以及排错,这一点我们不做过多介绍。

现在介绍个坑点:

很多开发者会(或者应该)知道,当我们为运行在Linux容器(Docker、rkt、runC、lxcfs等)中的Java程序去设置JVM的GC、堆大小和运行时编译器的参数时并没有得到预想的效果。当我们通过“java -jar mypplication-fat.jar”的方式而不设置任何参数来运行一个Java应用时,JVM会根据自身的许多参数进行调整,以便在执行环境中获得最优的性能。

我们倾向于认为容器可以像虚拟机一样可以完整的定义虚拟机的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。

我们需要知道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中发出的退出信号。

更多的内存是解决方案吗?

开发者如果不理解问题可能会认为运行环境中没有为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://X53X: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} 

还能再改进吗?

有什么办法规避和解决并自动的配置内存?(部分节选与网络)

大概有几种方案?

1: 在JDK9+中已经开始进行尝试在容器(i.e. Docker)环境中为JVM提供cgroup功能的内存限制。相关信息可以查看:http://hg.openjdk.java.net/jdk ... 0ea49

   jdk11 更是着重优化跟容器的适配性,是一个最为期待的版本。而据我了解,9,10的java对业务也有或多或少的变更代码风险,jdk11当然也会有这个风险,但是jdk11应该是未来的java主流和方向,个人建议不要过快的上jdk9 10,等11稳定了再考虑选型11

2; 那么对于已有的jdk8是不是没救了呢?

并不是!

仍然有3种解决方案:

第一种:  oepnjdk8u131以上版本,可以通过加参数感知容器的内存,见下图的dockerfile中的java参数;

第二种: 使用社区的 farbric8 jdk镜像,该社区镜像有实现自动计算容器真实限制的启动脚本;

第三种: 外部trick,使用lxcfs ,大概原理是模拟真实的容器限制的/proc文件系统使,java程序感知不到自己处于一个容器环境,虚拟的/proc系统可以获取真实的容器受限约束。(哎呀葡萄好甜!)

猜你喜欢

转载自blog.csdn.net/itest_2016/article/details/81293154
今日推荐