f8a23406ab5544950599bec0ebc307b5.png

像我们这样在Docker中运行Java应用程序的人,可能已经遇到过JVM在容器中运行时无法准确检测可用内存的问题。jvm没有准确地检测Docker容器中可用的内存,而是查看机器的可用内存。这可能导致在容器内运行的Java应用程序在尝试使用超出Docker容器限制的内存量时被终止的情况。

jvm对可用内存的错误检测与Linux工具/lib有关,这些工具/lib是在cgroups存在之前创建的,用于返回系统资源信息(例如,/proc/meminfo,/proc/vmstat)。它们返回主机的资源信息(该主机是物理机还是虚拟机)。

让我们通过观察一个简单的Java应用程序在Docker容器中运行时如何分配一定百分比的内存来探索这个过程。我们将把应用程序部署为Kubernetes pod(使用Minikube)来说明Kubernetes上也存在这个问题,这并不奇怪,因为Kubernetes使用Docker作为容器引擎。

package com.banzaicloud;

import java.util.Vector;

public class MemoryConsumer {

private static float CAP = 0.8f; // 80%

private static int ONE_MB = 1024 * 1024;

private static Vector cache = new Vector();

public static void main(String[] args) {

Runtime rt = Runtime.getRuntime();

long maxMemBytes = rt.maxMemory();

long usedMemBytes = rt.totalMemory() - rt.freeMemory();

long freeMemBytes = rt.maxMemory() - usedMemBytes;

int allocBytes = Math.round(freeMemBytes * CAP);

System.out.println("Initial free memory: " + freeMemBytes/ONE_MB + "MB");

System.out.println("Max memory: " + maxMemBytes/ONE_MB + "MB");

System.out.println("Reserve: " + allocBytes/ONE_MB + "MB");

for (int i = 0; i < allocBytes / ONE_MB; i++){

cache.add(new byte[ONE_MB]);

}

usedMemBytes = rt.totalMemory() - rt.freeMemory();

freeMemBytes = rt.maxMemory() - usedMemBytes;

System.out.println("Free memory: " + freeMemBytes/ONE_MB + "MB");

}

}

我们使用Docker构建文件来创建包含jar的图像,jar是从上面的Java代码构建的。我们需要这个Docker映像,以便将应用程序部署为Kubernetes Pod。

Dockerfile文件

FROM openjdk:8-alpine

ADD memory_consumer.jar /opt/local/jars/memory_consumer.jar

CMD java $JVM_OPTS -cp /opt/local/jars/memory_consumer.jar com.banzaicloud.MemoryConsumer

docker build -t memory_consumer .

现在我们有了Docker映像,我们需要创建一个pod定义来将应用程序部署到kubernetes:

memory-consumer.yaml

apiVersion: v1

kind: Pod

metadata:

name: memory-consumer

spec:

containers:

- name: memory-consumer-container

image: memory_consumer

imagePullPolicy: Never

resources:

requests:

memory: "64Mi"

limits:

memory: "256Mi"

restartPolicy: Never

此pod定义确保将容器调度到至少有64MB可用内存的节点,并且不允许其使用超过256MB的内存。

$ kubectl create -f memory-consumer.yaml

pod "memory-consumer" created

输出:

$ kubectl logs memory-consumer

Initial free memory: 877MB

Max memory: 878MB

Reserve: 702MB

Killed

$ kubectl get po --show-all

NAME READY STATUS RESTARTS AGE

memory-consumer 0/1 OOMKilled 0 1m

在容器内运行的Java应用程序检测到877MB的可用内存,因此试图保留702MB的可用内存。因为我们之前将最大内存使用限制为256MB,所以容器被终止。

为了避免这种结果,我们需要指示JVM可以保留的最大内存量。我们通过-Xmx选项来实现。我们需要修改pod定义,将-Xmx设置通过JVM_OPTS env变量传递给容器中的Java应用程序。

memory-consumer.yaml

apiVersion: v1

kind: Pod

metadata:

name: memory-consumer

spec:

containers:

- name: memory-consumer-container

image: memory_consumer

imagePullPolicy: Never

resources:

requests:

memory: "64Mi"

limits:

memory: "256Mi"

env:

- name: JVM_OPTS

value: "-Xms64M -Xmx256M"

restartPolicy: Never

$ kubectl delete pod memory-consumer

pod "memory-consumer" deleted

$ kubectl get po --show-all

No resources found.

$ kubectl create -f memory_consumer.yaml

pod "memory-consumer" created

$ kubectl logs memory-consumer

Initial free memory: 227MB

Max memory: 228MB

Reserve: 181MB

Free memory: 50MB

$ kubectl get po --show-all

NAME READY STATUS RESTARTS AGE

memory-consumer 0/1 Completed 0 1m

这次应用程序运行成功;它检测到我们通过-Xmx256M传递的正确可用内存,因此没有达到pod定义中指定的内存限制内存“256Mi”。

虽然这个解决方案可行,但它需要在两个地方指定内存限制:一个是作为容器内存的限制:“256Mi”,另一个是在传递给-Xmx256M的选项中。如果JVM根据内存“256Mi”设置准确地检测到可用内存的最大数量,会更方便,不是吗?

好吧,Java9中有一个变化,使它具有Docker意识,它已经被后移植到Java8。

为了利用此功能,我们的pod定义必须如下所示:

memory-consumer.yaml

apiVersion: v1

kind: Pod

metadata:

name: memory-consumer

spec:

containers:

- name: memory-consumer-container

image: memory_consumer

imagePullPolicy: Never

resources:

requests:

memory: "64Mi"

limits:

memory: "256Mi"

env:

- name: JVM_OPTS

value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Xms64M"

restartPolicy: Never

$ kubectl delete pod memory-consumer

pod "memory-consumer" deleted

$ kubectl get pod --show-all

No resources found.

$ kubectl create -f memory_consumer.yaml

pod "memory-consumer" created

$ kubectl logs memory-consumer

Initial free memory: 227MB

Max memory: 228MB

Reserve: 181MB

Free memory: 54MB

$ kubectl get po --show-all

NAME READY STATUS RESTARTS AGE

memory-consumer 0/1 Completed 0 50s

请注意-XX:MaxRAMFraction=1,通过它我们可以告诉JVM要使用多少可用内存作为最大堆大小。

通过-Xmx或UseCGroupMemoryLimitForHeap动态设置一个考虑可用内存限制的最大堆大小是很重要的,因为它有助于在内存使用接近其限制时通知JVM,以便释放空间。如果最大堆大小不正确(超过可用内存限制),JVM可能会盲目地达到该限制而不尝试释放内存,进程将被oom kill。

这个内存溢出的错误是不同的。它表示最大堆大小不足以在内存中容纳所有活动对象。如果是这种情况,则需要通过-Xmx增加最大堆大小,或者如果使用UseCGroupMemoryLimitForHeap,则通过容器的内存限制增加最大堆大小。

在k8s上运行基于JVM的工作负载时,cgroups的使用是非常实际的。

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐