Introduction
你是否曾经经历过在Docker中运行基于JVM的应用程序时出现“随机”故障?或者一些奇怪的死机?两者都有可能是由于Java 8中的糟糕的Docker支持引起。
Docker使用控制组(cgroups)来限制对资源的使用。在容器中运行应用程序时限制其对内存和CPU的使用绝对是一个好主意,它可以防止应用程序占用全部可用的内存和/或CPU,因而导致在同一系统上运行的其他容器无法响应。限制资源的使用可以提高应用程序的可靠性和稳定性。它还为硬件容量的规划提供了依据。在像诸如Kubernetes或DC/OS这样的编排系统上运行容器时,这一点尤为重要。
JVM 可以“看到”系统上所有可用的内存和 CPU 内核,并保持与这些资源的一致。在默认情况下,JVM 会将 max heap size 设置为系统内存的 1/4,并将一些线程池个数(比如GC)设置为与物理 CPU 内核的数量一致。
本文中使用的是遵循GNU GPL v2 许可授权的OpenJDK官方Docker镜像。这里描述的对Docker的支持在Oracle Java SE 开发工具包(JDK)版本8的更新191中被引入。Oracle在2019年4月修改了Java 8更新的许可政策,自Java SE 8更新211后的商业使用不再免费。
Practice
我们一起来看看下面的例子:
1 | import java.util.Vector; |
We run it on a system with 64GB of memory, so let’s check the default maximum heap size:
1 | docker run -ti openjdk:8u181-jdk |
As said — it’s 1/4 of physical memory — 16GB. What will happen if we limit the memory using docker cgroups? Let’s check:
1 | docker run -ti -m 512M openjdk:8u181-jdk |
The JVM process was killed. Since it was a child process — the container itself survived, but normally when java is the only process inside a container (with PID 1) the container will crash.
Let’s look into system logs:
1 | dcos-agent-1 kernel: java invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=0 |
Failures like these can be very difficult to debug — there is nothing in the application logs. It can be especially difficult on managed systems like AWS ECS.
And how about CPUs? Let’s check it again running a small program which displays the number of available processors:
1 | public class AvailableProcessors { |
Let’s run it in a docker container with cpu number set to 1:
1 | $ docker run -ti --cpus 1 openjdk:8u181-jdk |
Not good — there are 12 CPUs on this system indeed. So even the number of available processors is limited to 1, the JVM will try to use 12.
for example the GC threads number is set by this formula: On a machine with N hardware threads where N is greater than 8, the parallel collector uses a fixed fraction of N as the number of garbage collector threads. The fraction is approximately 5/8 for large values of N. At values of N below 8, the number used is N.
In our case:
1 | java -XX:+PrintFlagsFinal -version | grep ParallelGCThreads |
Solution
The new Java version (10 and above) docker support is already built-in. But sometimes upgrading is not an option — for example if the application is incompatible with the new JVM.
The good news: Docker support was also backported to Java 8. Let’s check the newest openjdk image tagged as 8u212.
We’ll limit the memory to 1G and use 1 CPU: docker run -ti –cpus 1 -m 1G openjdk:8u212-jdk
The memory:
1 | java -XX:+PrintFlagsFinal -version | grep MaxHeap |
Moreover, there are some new settings:
- -XX:InitialRAMPercentage
- -XX:MaxRAMPercentage
- -XX:MinRAMPercentage
If for some reason the new JVM behaviour is not desired it can be switched off using -XX:-UseContainerSupport.
On Kuberntes
imits.cpu <==> –cpu-quota # docker inspect中的CpuQuota值
requests.cpu <==> –cpu-shares # docker inspect中的CpuShares值
Reference
- https://blog.softwaremill.com/docker-support-in-new-java-8-finally-fd595df0ca54
- https://www.chainnews.com/articles/631552101870.htm
- https://stackoverflow.com/questions/54292282/clarification-of-meaning-new-jvm-memory-parameters-initialrampercentage-and-minr/54297753#54297753
- https://www.cnblogs.com/yehaifeng/p/9596399.html