为什么我的Java应用程序被OOMKilled了?

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

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

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

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

pod输出:

$ 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 环境变量传递给容器中的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要使用多少可用内存作为最大堆大小。

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

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

老K的Java博客
我还没有学会写个人说明!
上一篇

问了158个问题后 科学家首次发现做梦也能答对数学题

下一篇

(十) 数据库查询处理之排序(sorting)

你也可能喜欢

评论已经被关闭。

插入图片