在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作爲容器引擎。
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](http://javakk.com/tag/outofmemoryerror "查看更多關於 OutOfMemoryError 的文章")
錯誤是不同的。它表示最大堆大小不足以在內存中容納所有活動對象。如果是這種情況,則需要通過 -Xmx
增加最大堆大小,或者如果使用 UseCGroupMemoryLimitForHeap
,則通過容器的內存限制增加最大堆大小。