重磅:Oracle在2019年4月修改了Java 8更新的许可政策,自Java SE 8更新211后的商业使用不再免费。
你是否曾经经历过在docker中运行基于JVM的应用程序时出现“随机”故障?或者一些奇怪的死机?两者都有可能是由于Java 8(它仍然被广泛使用)中的糟糕的Docker支持引起。
JVM可以“看到”系统上所有可用的内存和CPU内核,并保持与这些资源的一致。在默认情况下,JVM会将max heap size(最大堆大小)设置为系统内存的1/4,并将一些线程池个数(比如说垃圾回收(GC))设置为与物理CPU内核的数量一致。我们一起来看看下面的例子。
我们将运行一个简单的应用程序,它将消耗尽可能多的内存(示例可以在这个站点上找到):
我们在内存为64GB的系统上运行它,让我们来检查一下默认的最大堆大小:
如前所述,它应该是系统物理内存的1/4 (16GB)。如果我们使用Docker cgroups限制内存,会发生什么呢?让我们检查一下:
这个JVM进程被终止了。因为它是一个子进程,所以容器本身幸存下来,但是通常当Java是容器内的唯一进程(用PID 1)时,容器也会崩溃。
让我们深入研究一下系统日志中有什么:
像这样的故障很难调试,因为应用程序日志中没有任何内容。在像AWS ECS这样的管理系统上,它可能尤其困难。
CPU怎么样?让我们运行一个显示可用处理器数量的小程序,来再一次看看会发生什么:
我们在一个CPU数量设置为1的Docker容器中运行这个小程序:
不好!这个系统上实际有12个CPU。因此,即使将可用处理器的数量限制为1个,JVM也将尝试使用12个。例如,垃圾回收(GC)线程数量是基于以下公式设置的:
在具有n个硬件线程并且n大于8的计算机上,并行回收器使用一个固定的分数来设定垃圾回收器的线程数。当n大于8时,这个分数约为5/8。当n小于8时,垃圾回收器的线程数为n。
在案例中:
好的,我们现在知道这个问题的存在了。那么有解决办案吗?幸运的是 - 有!
新的Java版本(10及以上)已经内置了Docker的支持功能。但有时升级并不能解决问题,比如说,如果应用程序与新的JVM不兼容就不行。
好消息是:对Docker的支持还被向后移植到Java 8。让我们运行下面人命令来检查标记为8u212的最新openjdk 镜像。我们将内存限制为1G,并使用1个CPU:
docker run -ti --cpus 1 -m 1G openjdk:8u212-jdk
内存:
root@843e552c2e49:/# java -XX:+PrintFlagsFinal -version | grep MaxHeap uintx MaxHeapFreeRatio = 70 {manageable} uintx MaxHeapSize := 268435456 {product} openjdk version "1.8.0_212" OpenJDK Runtime Environment (build 1.8.0_212-8u212-b01-1~deb9u1-b01) OpenJDK 64-Bit Server VM (build 25.212-b01, mixed mode) root@843e552c2e49:/#
它是256M, 正好是已分配内存的1/4。
CPU:
root@16f12923f731:/# java AvailableProcessors 1
正如我们想要的那样。
此外,还有一些新设置:
-XX:InitialRAMPercentage -XX:MaxRAMPercentage -XX:MinRAMPercentage
这些设置允许微调 heap size(堆大小)。这些设置的含义在StackOverflow的这个优秀答案中已经得到了解释。请注意,它们设置的是百分比,而不是固定值。多亏了它,更改Docker内存设置不会破坏任何东西。
如果出于某种原因不需要新JVM的特性,可以使用-xx:-useContainerSupport关闭它。