当前位置: 首页 > 科技观察

在Docker中运行Java:防止失败你应该知道的

时间:2023-03-16 16:51:55 科技观察

如果你尝试在容器中运行Java程序,或者专注于Docker,你可能会遇到一些与JVM和堆大小相关的问题。本文将展示如何解决这些问题。许多开发者会(或应该)知道,当我们为运行在Linux容器(docker、rkt、runC、lxcfs等)中的Java程序设置JVMGC、堆大小和运行时编译器的参数时,并没有得到预期的效果。当我们在不设置任何参数的情况下通过“java-jarmypplication-fat.jar”运行一个Java应用程序时,JVM会根据很多参数进行自我调整,以在执行环境中获得最佳性能。此博客将以简单的方式向开发人员展示在Linux容器中运行Java应用程序时他们需要了解的内容。我们倾向于认为一个容器可以像虚拟机一样完全定义虚拟机的CPU数量和内存数量。容器更像是进程级的资源(CPU、内存、文件系统、网络等)隔离。这种隔离依赖于Linux内核中提供的cgroups功能。但是,一些可以从运行时环境收集信息的应用程序在cgroups功能出现之前就已经存在。在容器中执行命令“top”、“free”、“ps”,包括未优化的JVM是受高限制的Linux进程。我们来验证一下。问题为了演示遇到的问题,我使用命令“docker-machinecreate-dvirtualbox--virtualbox-memory'1024'docker1024”在虚拟机中创建一个1GB内存的Dockerdaemon,然后在3个Linux容器中执行命令“free-h”让它只有100MB的内存和Swap。结果显示所有容器的总内存为995MB。即使在Kubernetes/OpenShift集群中,结果也是相似的。我还在15G内存的集群中执行了一个命令,让KubernetesPod的内存限制为511MB(命令:“kubectlrunmycentos–image=centos-it–limits='memory=512Mi'”),总内存显示为14GB。要了解为什么会这样,您可以阅读这篇博文“Linux容器中的内存——或者为什么不在Linux容器中使用free和top?”我们需要知道Docker参数(-m、-memory和-memory-swap)和Kubernetes参数(-limits)会导致Linux内核在内存超过限制时杀死进程,但JVM并不知道其存在完全没有这个限制。当超过这个限制时,坏事就会发生。!为了模拟一个进程超过内存限制会被kill的场景,我们可以在容器中运行WildFly应用服务器,通过命令“dockerrun-it-namemywildfly-m=”对其进行内存限制50mjboss/wildfly”大小为50MB。当这个容器正在运行时,我们可以执行命令“dockerstats”来查看容器的限制。但过几秒后,容器Wildfly会中断,输出信息:***JBossASprocess(55)receivedKILLsignal***Youcanpassthecommand"dockerinspectmywildfly-f'{{json.State}}'"查看容器是否因为发生OOM(OutofMemory)而被杀死。容器中的“状态”记录为OOMKilled=true。这将如何影响Java应用程序在Docker主机中创建具有1GB内存的虚拟机(之前使用命令“docker-machinecreate-dvirtualbox--virtualbox-memory'1024'docker1024”创建),并限制一个容器内存为150M,似乎足以运行Dockerfile中设置的参数-XX:PrintFlagsFinal和-XX:PrintGCDetails的SpringBoot应用。这些参数允许我们读取JVM的初始化参数并获取垃圾收集(GC)操作的详细信息。试试:$dockerrun-it--rm--namemycontainer150-p8080:8080-m150Mrafabene/java-container:openjdk我还提供了一个访问接口“/api/memory/”来使用String对象加载JVM内存,模拟大量内存消耗,你可以调用它:$curlhttp://X41X:8080/api/memory该接口将返回以下信息“Allocatedmorethan80%(219.8MiB)ofthemaxallowedJVMmemorysize(241.7MiB)”这里我们至少有2个问题:为什么JVM允许最大内容为241.7MiB?如果容器限制内存为150MB,为什么Java允许分配内存到220MB?首先,我们应该重新理解JVMergonomicpage中描述的“最大堆大小”的定义,它会使用1/4的物理内存。JVM不知道它在容器中运行,因此它可以使用最大260MB的堆大小。通过在容器初始化的时候加入参数-XX:PrintFlagsFinal,我们可以查看这个参数的值。$dockerlogsmycontainer150|grep-iMaxHeapSizeuintxMaxHeapSize:=262144000{product}其次,我们应该明白,在docker命令行设置“-m150M”参数时,Dockerdaemon会限制RAM为150M,Swap为150M。从结果来看,一个进程可以分配300M的内存,这就解释了为什么我们的进程收不到Kernel的任何退出信号。可以在此处找到有关Docker命令中内存限制(--memory)和交换(--memory-swap)之间的区别的更多信息。更多内存是解决方案吗?不了解问题的开发人员可能会认为运行时没有足够的内存供JVM使用。通常的解决办法是为运行环境提供更多的内存,但实际上,这是一种错误的理解。假设我们将DockerMachine的内存从1GB增加到8GB(使用命令“docker-machinecreate-dvirtualbox–virtualbox-memory'8192'docker8192”),创建的容器从150M增加到800M:$dockerrun-it--namemycontainer-p8080:8080-m800Mrafabene/java-container:openjdk此时使用命令“curlhttp://X58X:8080/api/memory”无法返回结果,因为在JVM环境下计算的MaxHeapSize与8GB内存大小为2092957696(~2GB)。可以使用命令“dockerlogsmycontainer|grep-iMaxHeapSize”查看。应用程序将尝试分配超过1.6GB的内存。当超过容器的限制(800MBRAM和800MBSwap)时,进程将被杀死。显然在容器中运行程序时,增加内存和设置JVM参数并不是一个好办法。在容器中运行Java应用程序时,我们应该根据应用程序的需要和容器的限制来设置最大堆大小(参数:-Xmx)。解决办法是什么?对Dockerfile稍作修改以指定JVM的扩展环境变量。修改后的内容如下:CMDjava-XX:PrintFlagsFinal-XX:PrintGCDetails$JAVA_OPTIONS-jarjava-container.jar现在我们可以使用JAVA_OPTIONS环境变量来设置JVMHeap的大小。300MB似乎足够应用程序了。后面可以查看日志,Heap的值为314572800字节(300MBi)。在Docker下,可以使用“-e”参数设置环境变量进行切换。$dockerrun-d--namemycontainer8g-p8080:8080-m800M-eJAVA_OPTIONS='-Xmx300m'rafabene/java-container:openjdk-env$dockerlogsmycontainer8g|grep-iMaxHeapSizeuintxMaxHeapSize:=314572800{product}在Kubernetes中,你可以使用“--env=[key=value]”设置要切换的环境变量:$kubectlrunmycontainer--image=rafabene/java-container:openjdk-env--limits='memory=800Mi'--env="JAVA_OPTIONS='-Xmx300m'"$kubectlgetpodsNAMEREADYSTATUSRESTARTSAGEmycontainer-2141389741-b1u0o1/1Running06s$kubectllogsmycontainer-2141389741-b1u0o|grepMaxHeapSizeuintxMaxHeapSize:=314572800{product}可以改进吗?有没有什么办法让Fabric自动计算Heaplimit的容器呢,容器是根据你的soap8自动计算的它可以实现。镜像fabric8/java-jboss-openjdk8-jdk使用脚本计算容器的内存限制,使用50%的内存作为上限。即可以写入50%的内存。您还可以使用此图像启用/禁用调试、诊断和更多功能。让我们看一下SpringBoot应用程序的Dockerfile:FROMfabric8/java-jboss-openjdk8-jdk:1.2.3ENVJAVA_APP_JARjava-container.jarENVAB_OFFtrueEXPOSE8080ADDtarget/$JAVA_APP_JAR/deployments/就是这样!现在,不管容器的内存限制如何,我们的Java应用程序的Heap大小都会在容器中自动调整,而不是根据宿主机来设置。总结到目前为止,JavaJVM并不知道它正在容器中运行——某些资源在内存和CPU使用方面受到限制。因此,你不能让JVM设置自己的最优最大Heap值。一种解决方案是使用Fabric8作为基础镜像,它可以实现应用程序运行在一个受限的容器中,并且可以自动调整最大堆的值而无需你做任何事情。JDK9中已尝试为容器(即Docker)环境中的JVM提供支持cgroup的内存限制。