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

Java和Docker限制的那些东西

时间:2023-03-15 14:39:57 科技观察

Java和Docker不是天生的朋友。Docker可以设置内存和CPU限制,这是Java无法自动检测到的。使用Java的Xmx标志(繁琐/重复)或新的实验性JVM标志,我们可以解决这个问题。虚拟化中的不匹配Java和Docker的组合并不是完美的组合,而且最初离完美的组合还有很长的路要走。对于初学者来说,JVM的整体思想是一个虚拟机可以使程序独立于底层硬件。那么,将我们的Java应用程序打包到JVM中,然后塞进Docker容器中,能带来哪些好处呢?大多数时候,您只是在复制JVM和Linux容器,除了浪费更多内存之外没有任何好处。感觉很傻。但是,Docker可以将您的程序、设置、特定的JDK、Linux设置和应用程序服务器以及其他工具打包为一个东西。从DevOps/Cloud的角度来看,这样一个完整的容器,封装程度更高。问题1:内存如今,绝大多数生产应用程序仍在使用Java8(或更早版本),这可能会导致问题。Java8(更新131之前)不适用于Docker。问题在于,在您的机器上,JVM可用的内存和CPU数量并不是Docker允许您使用的可用内存和CPU数量。例如,如果您将Docker容器限制为仅使用100MB内存,但是,旧版本的Java无法识别此限制。Java看不到此限制。JVM会要求更多的内存,远远超过这个限制。如果使用过多内存,Docker将采取行动并终止容器内的进程!JAVA进程被杀了,显然,这不是我们想要的。要解决此问题,您需要为Java指定最大内存限制。在旧版本的Java中(8u131之前),您需要通过在容器中设置-Xmx来限制堆大小。这感觉不对,你不想定义这些限制两次,你也不想在你的容器中定义它们。幸运的是,我们现在有更好的方法来解决这个问题。在Java9(8u131+)之后,JVM增加了如下标志:-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap这些标志强制JVM检查Linuxcgroup配置,Docker通过cgroup实现最大内存设置。现在,如果您的应用程序达到Docker设置的限制(比如500MB),JVM可以看到这个限制。JVM将尝试GC操作。如果仍然超出内存限制,JVM将执行它应该执行的操作并抛出OutOfMemoryException。也就是说,JVM能够看到Docker的这些设置。在Java10之后(参考下面的测试),这些体验标志默认是开启的,也可以通过-XX:+UseContainerSupport开启(你可以通过设置-XX:-UseContainerSupport来关闭这些行为)。问题二:CPU第二个问题类似,不过是CPU相关的。简而言之,JVM将查看硬件并检测CPU的数量。它将优化您的运行时以使用这些CPU。但同样,这里还有另一个不匹配,Docker可能不允许您使用所有这些CPU。遗憾的是,这在Java8或Java9中没有得到解决,但在Java10中得到了解决。从Java10开始,可用CPU的计算将采用不同的(默认情况下)方法来解决这个问题(同样通过UseContainerSupport)。Java和Docker的内存处理测试作为一个有趣的练习,让我们验证和测试Docker如何使用几个不同的JVM版本/标志甚至不同的JVM处理内存不足。首先,我们创建一个测试应用程序,它只是“吃掉”内存而不释放它。javaimportjava.util.ArrayList;importjava.util.List;publicclassMemEat{publicstaticvoidmain(String[]args){Listl=newArrayList<>();while(true){byteb[]=newbyte[1048576];l.add(b);Runtimert=Runtime.getRuntime();System.out.println("freememory:"+rt.freeMemory());}}}我们可以启动Docker容器并运行应用程序,看看会发生什么。测试一:Java8u111首先,我们将从一个装有旧版本Java8(更新111)的容器开始。shelldockerrun-m100m-itjava:openjdk-8u111/bin/bash我们编译并运行MemEat.java文件:shelljavacMemEat.javajavaMemEat...freememory:67194416freememory:66145824freememory:65097232Killed正如预期的那样,Docker已经终止了我们的Java进程。不是我们想要的(!)。您还可以看到Java认为它仍有大量内存要分配的输出。我们可以通过使用-Xmx标志为Java提供最大内存来解决此问题:限制,进程正常停止,JVM了解它正在运行的限制。但是,问题是您现在将这些内存限制设置了两次,一次针对Docker,一次针对JVM。测试2:Java8u144如前所述,通过添加新标志来修复问题,JVM现在可以遵循Docker提供的设置。我们可以使用较新版本的JVM对其进行测试。shelldockerrun-m100m-itadoptopenjdk/openjdk8/bin/bash(在撰写本文时,此OpenJDKJava映像的版本为Java8u144)接下来,我们再次编译并运行MemEat.java文件,不带任何标志:shelljavacMemEat.javajavaMemEat...freememory:67194416freememory:66145824freememory:65097232Killed仍然有同样的问题。但是我们现在可以试试上面提到的实验标志:shelljavacMemEat.javajava-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeapMemEat...freememory:1679936freememory:2204208freememory:1155616freememory:1155616freememory:1155616freememory.javaspaceMeatEmain(Javafreememory).这次我们没有告诉JVM限制是什么,我们只是告诉JVM检查正确的限制设置!现在感觉好多了。测试三:Java10u23有人在评论和Reddit上提到,Java10通过将实验标志设为新的默认标志来修复所有问题。可以通过禁用此标志来关闭此行为:-XX:-UseContainerSupport。当我测试它时,它最初不起作用。在撰写本文时,AdoptAJDKOpenJDK10映像已与jdk-10+23一起打包。这个JVM显然仍然不理解UseContainerSupport标志,并且进程仍然被Docker杀死。shelljavacMemEat.javajavaMemEat...freememory:96262112freememory:94164960freememory:92067808freememory:89970656Killedjava-XX:+UseContainerSupportMemEatUnrecognizedVMoption'UseContainerSupport'Error:CouldnotcreatetheJavaVirtualMachine.Error:Afatalexceptionhasoccurred.Programwillexit.测试四:Java10u46(Nightly)我决定尝试AdoptAJDKOpenJDK10的最新nightly构造。它包含的版本是Java10+46,而不是Java10+23。shelldockerrun-m100m-itadoptopenjdk/openjdk10:nightly/bin/bash但是,这个ngithly构建有一个问题,导出的PATH指向旧的Java10+23目录而不是10+46,我们需要修复这个问题。shellexportPATH=$PATH:/opt/java/openjdk/jdk-10+46/bin/javacMemEat.javajavaMemEat...freememory:3566824freememory:2796008freememory:1480320Exceptioninthread“main”java.lang.OutOfMemoryError:Javaheapspaceat:javaMemEat.main(8)成功!在不提供任何标志的情况下,Java10仍然可以正确检测到Docker的内存限制。测试五:OpenJ9我最近也尝试了OpenJ9,这是一个免费的替代JVM,它已从IBMJ9开源,现在由Eclipse维护。请在我的下一篇博文(http://royvanrijn.com/blog/2018/05/openj9-jvm-shootout/)中阅读有关OpenJ9的更多信息。它运行速度快,内存管理得很好,性能很好,并且通常可以为我们的微服务节省高达30-50%的内存。这几乎将SpringBoot应用程序定义为“微型”,运行速度为100-200mb,而不是300mb+。我打算很快写一篇关于这个的文章。但令我感到惊讶的是,OpenJ9还没有类似于Java8/9/10+中cgroup内存限制的标志(向后移植)的选项。如果我们将之前的测试用例应用于最新的AdoptAJDKOpenJDK9+OpenJ9构建:shelldockerrun-m100m-itadoptopenjdk/openjdk9-openj9/bin/bash我们添加OpenJDK标志(OpenJ9将忽略的标志):shelljava-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeapMemEat...freememory:83988984freememory:82940400freememory:81891816KilledOops,JVM又被Docker杀死了。我真的希望很快将类似的选项添加到OpenJ9,因为我想在生产中运行它而不必两次指定最大内存。Eclipse/IBM正在努力解决这个问题,问题已经提出,甚至已经针对问题提交了PR。更新:(不推荐Hack)解决这个问题的一种有点丑陋/hacky的方法是使用以下标志组合:shelljava-Xmx`cat/sys/fs/cgroup/memory/memory.limit_in_bytes`MemEat...freememory:3171536freememory:2127048freememory:2397632freememory:1344952JVMDUMP039IProcessingdumpevent"systhrow",detail"java/lang/OutOfMemoryError"at2018/05/1514:04:26-pleasewait.JVMDUMP032IJVMrequestedSystemdumpusing'//core.20180515.140426.125.0001.dmp'inresponsetoaneventJVMDUMP010ISystemdumpwrittento//core.20180515.140426.125.0001.dmpJVMDUMP032IJVMrequestedHeapdumpusing'//heapdump.20180515.140426.125.0002.phd'inresponsetoaneventJVMDUMP010IHeapdumpwrittento//heapdump.20180515.140426.125.0002.phdJVMDUMP032IJVMrequestedJavadumpusing'//javacore.20180515.140426.125.0003.txt'inresponsetoaneventJVMDUMP010IJavadumpwrittento//javacore.20180515.140426.125.0003.txtJVMDUMP032IJVMrequestedSnapdumpusing'//Snap.20180515.140426.125.0004.trc'响应事件JVMDUMP010ISnapdumpwrittento//Snap.20180515.140426.125.0004.trcJVMDUMP013IProcesseddumpevent“systhrow”,详细信息“java/lang/OutOfMemoryError”。Exceptioninthread“main”java.lang.OutOfMemoryError:JavaheapspaceatMemEat.main(MemEat.java:8)在这种情况下,堆大小受分配给Docker实例内存,这适用于较旧的JVM和OpenJ9,这当然是错误的,因为容器本身和堆外JVM的其他部分也使用内存。但这似乎有效,显然Docker在这种情况下很宽容。也许一些bash大师会制作一个更好的版本,从其他进程中减去一些字节。无论如何,不??要这样做,它可能不会起作用。测试6:OpenJ9(夜间版)建议使用最新的夜间版OpenJ9。shelldockerrun-m100m-itadoptopenjdk/openjdk9-openj9:nightly/bin/bash最新的OpenJ9nightlybuild,它有两件事:另一个有问题的PATH参数需要首先修复JVM支持新标志UseContainerSupport(就像Java10一样)shellexportPATH=$PATH:/opt/java/openjdk/jdk-9.0.4+12/bin/javacMemEat.javajava-XX:+UseContainerSupportMemEat...freememory:5864464freememory:4815880freememory:3443712freememory:2391032JVMDUMP039I"deprocessingdumpevent",decessingdumpevent"lang/OutOfMemoryError"at2018/05/1521:32:07-pleasewait.JVMDUMP032IJVMrequestedSystemdumpusing'//core.20180515.213207.62.0001.dmp'inresponsetoaneventJVMDUMP010ISystemdumpwrittento//core.20180515.213207.62.0001.dmpJVMDUMP032IJVMrequestedHeapdumpusing'//heapdump.20180515.213207.62.0002.phd'inresponsetoaneventJVMDUMP010IHeapdumpwrittento//heapdump.20180515.213207.62.0002.phdJVMDUMP032IJVMrequestedJavadumpusing'//javacore.20180515.213207.62.0003.txt'响应事件JVMDUMP010IJavadumpto//javacore.20180515.213207.62.0003.txtJVMDUMP032IJVMrequestedSnapdumpusing'//Snap.20180515.213207.62.0004.trc'inresponsetoaneventJVMDUMP010ISnapdumpwrittento//Snap.20180515.213207.62.0004.trcJVMDUMP013IProcesseddumpevent"systhrow",detail"java/lang/OutOfMemoryError".Exceptioninthread"main"java.lang.OutOfMemoryError:JavaheapspaceTADAAA,正在修复中!奇怪的是,这个标志在OpenJ9中没有像在Java10中那样默认启用再次:确保你测试这是你想要在Docker容器中运行Java的东西。总结总结:注意资源限制的不匹配。测试你的内存设置和JVM标志,不要假设任何东西。如果您在Docker容器中运行Java,请确保您在JVM中设置了Docker内存限制和限制,或者您的JVM理解这些限制。如果您无法升级您的Java版本,请使用-Xmx设置您自己的限制。对于Java8和Java9,请更新到最新版本并使用:-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap对于Java10,确保它支持'UseContainerSupport'(更新到最新版本)。对于OpenJ9(我强烈推荐,可以有效减少生产环境中的内存占用),目前使用-Xmx设置限制,但很快就会有支持UseContainerSupport标志的版本。