Java 8 终于支持 Docker!

Java 8曾经与Docker无法很好地兼容性,现在问题已消失。

请注意:我在本文中使用采用GNU GPL v2许可证的OpenJDK官方docker映像。在Oracle Java SE中,这里描述的docker支持功能在更新191中引入。Oracle在2019年4月更改了Java 8更新的许可证,自Java SE 8 Update 211以来商业使用不再免费。

你是否遇到过在docker中运行的基于JVM的应用程序出现“随机”故障?或者也许是一些奇怪的死机?两者都可能是Java 8(仍广泛使用的)中糟糕的docker支持引起的。

Docker使用控制组(cgroups)来限制资源。在容器中运行应用程序时限制内存和CPU绝对是个好主意――它可以阻止应用程序占用整个可用内存及/或CPU,这会导致在同一个系统上运行的其他容器毫无反应。限制资源可提高应用程序的可靠性和稳定性。它还允许为硬件容量作好规划。在Kubernetes或DC/OS之类的编排系统上运行容器时尤为重要。

问题

JVM可以“看到”系统上的整个内存和可用的所有CPU核心,并确保与资源一致。它默认情况下将最大堆大小(heap size)设置为系统内存的1/4,并将某些线程池大小(比如针对GC)设置为物理核心数量。不妨举例说明。

我们将运行一个简单的应用程序,它消耗尽可能多的内存(可在该网站上找到):

img

我们在拥有64GB内存的系统上运行,所以不妨检查默认的最大堆大小:

如上所述,它是物理内存的1/4即16GB。如果我们使用docker cgroups限制内存,会发生什么?不妨检查一下:

img

JVM进程被杀死了。由于它是一个子进程――容器本身幸存下来,但通常当java是容器(PID 1)内的唯一进程时,容器会崩溃。

不妨深入看看系统日志:

img

img

像这样的故障调试起来可能很难――应用程序日志中没有任何内容。在AWS ECS之类的托管系统上尤其困难重重。

CPU怎么样?不妨再次检查,运行一个显示可用处理器数量的小程序:

不妨在一个cpu编号设置为1的docker容器中运行它:

不好,这个系统上的确有12个CPU。因此,即使可用处理器的数量限制为1,JVM也会尝试使用12――比如说,GC线程数量由该公式设置:

在拥有N个硬件线程(N大于8)的机器上,并行收集器使用N的固定分数作为垃圾收集器线程的数量。如果N的值很大,该分数约5/8。如果N的值低于8,使用的数字是N。

在我们的情况下:

解决方案

OK,我们现在意识到了这个问题。有解决方案吗?幸运的是,有!

新的Java版本(10及以上版本)已经内置了docker支持功能。但有时升级不是办法,比如说如果应用程序与新JVM不兼容就不行。

好消息:Docker支持还被向后移植到Java 8。不妨检查标记为8u212的最新openjdk映像。我们将内存限制为1G,并使用1个CPU:docker run -ti --cpus 1 -m 1G openjdk:8u212-jdk。

内存:

它是256M,正好是已分配内存的1/4。

CPU:

正如我们想要的那样。

此外,还有几个新的设置:

它们允许微调堆大小――这些设置的含义在StackOverflow的这个优秀答案中已得到了解释。请注意:他们设置的是百分比,而不是固定值。正因为如此,改变Docker内存设置不会破坏任何东西。

如果由于某种原因不想要看到新的JVM行为,可以使用-XX:-UseContainerSupport来关闭。

总结

为基于JVM的应用程序设置正确的堆大小极其重要。如果使用最新的Java 8版本,你可以依赖安全(但非常保守)的默认设置。不需要在docker入口点中使用任何变通办法,也不需要再将Xmx设置为固定值。

使用JVM愉快!