我的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作爲容器引擎。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章