
在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 ,則通過容器的記憶體限制增加最大堆大小。