在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要使用多少可用内存作为最大堆大小。
通过-Xmx
或UseCGroupMemoryLimitForHeap
动态设置一个考虑可用内存限制的最大堆大小是很重要的,因为它有助于在内存使用接近其限制时通知JVM,以便释放空间。如果最大堆大小不正确(超过可用内存限制),JVM可能会盲目地达到该限制而不尝试释放内存,进程将被OOMKilled。
这个java.lang.OutOfMemoryError
错误是不同的。它表示最大堆大小不足以在内存中容纳所有活动对象。如果是这种情况,则需要通过-Xmx
增加最大堆大小,或者如果使用UseCGroupMemoryLimitForHeap
,则通过容器的内存限制增加最大堆大小。