前提說明:
爲公司新的架構的技術選型爲,springCloud 架構搭建微服務,在ECS以docker形式,部署每個微服務。併爲每個docker容器,設置內存限制。在服務部署上線後,發現經常有微服務,莫名的停止。日誌上卻沒有任何error錯誤。很讓人捉急。
具體配置如下:
1、docker 容器的創建:
docker run -dit \
-m 640M --memory-swap -1 \
--net docker-network-dev \
--ip 192.168.0.100 \
--restart=always \
--privileged=true \
--name=keda6-dev-information-main \
--hostname=slave_informationr_main \
-v /home/docker/springCloud/project/keda6-information-main/:/var/local/project/ \
-v /home/springCloud/project/keda6-information-main/:/home/springCloud/ \
-v /etc/localtime:/etc/localtime \
-e TZ='Asia/Shanghai' \
-e LANG="en_US.UTF-8" \
-p 30009:30009 \
-p 20209:20209 \
-p 20009:9820 \
-p 2199:22 \
seowen/jdk8u241-project:latest \
/usr/sbin/init
爲容器配置 640M的內存限制,但不限制 虛擬內存的使用。
2、jar 啓動命令如下(start.sh):
#!/bin/bash
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
echo "Asia/Shanghai" > /etc/timezone
sh stop.sh
#sh jmx.sh
JARR=$(ls -lt /var/local/project/ | grep 'keda-' | head -n 1 |awk '{print $9}')
nohup java \
-Dcom.sun.management.jmxremote \
-Djava.rmi.server.hostname=192.168.1.126 \
-Dcom.sun.management.jmxremote.port=30009 \
-Dcom.sun.management.jmxremote.rmi.port=30009 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.access.file=/usr/local/jmx/jmxremote.access \
-Dcom.sun.management.jmxremote.password.file=/usr/local/jmx/jmxremote.password \
-Dcom.sun.management.jmxremote.ssl=false \
-Xms512m -Xmx512m -jar $JARR \
--server.port=9820 \
--eureka.instance.non-secure-port=20009 \
--management.server.port=20209 \
--eureka.instance.ip-address=192.168.1.126 \
--eureka.instance.hostname=192.168.1.126 --spring.profiles.active=dev &
重點如下:
-Xms512m -Xmx512m -jar $JARR
設置 jvm 的最大內存和初始內存均爲512M,項目能順利啓動。 但是,經常不知所云的停止。 第一能想到的是,內存溢出,造成服務停止,但是查看日誌,又沒有任何相關信息。 無奈,只能先解決當前的問題,即,讓服務停止後,能自動重啓。 儘量保證服務的不可用時長,最小化。 因此,寫了個腳本,每隔1秒,檢查一次服務線程。如果停止了,就重新啓動,如下:
always.sh
#!/bin/bash
while :
do
PID=$(ps -ef | grep keda | grep -v grep | awk '{ print $2 }')
if [ -z "$PID" ]
then
sh start.sh
fi
sleep 1
done &
雖然,能將服務的停用時間,縮短到1秒內(不含啓動時間)。【但不能根本找出問題,就無法根本解決,還得繼續尋找問題的根源】。
我們知道,Docker使用控制組(cgroups)來限制資源。在容器中運行應用程序時限制內存和CPU絕對是個好主意――它可以阻止應用程序佔用整個可用內存及/或CPU,這會導致在同一個系統上運行的其他容器毫無反應。限制資源可提高應用程序的可靠性和穩定性。它還允許爲硬件容量作好規劃。在Kubernetes或DC/OS之類的編排系統上運行容器時尤爲重要。
首先Docker容器本質是是宿主機上的一個進程,它與宿主機共享一個/proc目錄,也就是說我們在容器內看到的/proc/meminfo,/proc/cpuinfo 與直接在宿主機上看到的一致,如下。
Host
容器
那麼Java是如何獲取到Host的內存信息的呢?沒錯就是通過/proc/meminfo來獲取到的。
默認情況下,JVM的Max Heap Size是系統內存的1/4,假如我們系統是8G,那麼JVM將的默認Heap≈2G。
Docker通過CGroups完成的是對內存的限制,而/proc目錄是已只讀形式掛載到容器中的,由於默認情況下Java 壓根就看不見CGroups的限制的內存大小,而默認使用/proc/meminfo中的信息作爲內存信息進行啓動, 這種不兼容情況會導致,如果容器分配的內存小於JVM的內存,JVM進程會被直接殺死。
劃重點:如果容器分配的內存小於JVM的內存,JVM進程會被直接殺死。
我們來回顧一下,上面創建容器的限制內存上限爲640M。 java項目設置的JVM 堆內存爲最大爲512M,注意是堆內存 。我們知道,JVM 除了堆區,還有非堆區(Metaspace等本地內存區)以及還有其他內存使用。 而 640-512 剩下128M。 因此,在這種情況下,如果我們的程序,出現了大量的內存溢出,GC還沒來得及回收的情況下,就已經因爲內存達到容器限制,而線程被docker直接殺死了。 那也就不會存在,報錯信息的出現。 而是項目直接down掉了。
到此,問題的大概,已經明白的差不多了。那就開始解決問題吧:
1、能不能不讓docker容器,殺死我的項目。就算內存爆了 ,也不直接kill。 開始查找相關資料,幸運找到:
docker內存限制相關的參數
執行docker run
命令時能使用的和內存限制相關的所有選項如下。
選項 | 描述 |
---|---|
-m ,--memory |
內存限制,格式是數字加單位,單位可以爲 b,k,m,g。最小爲 4M |
--memory-swap |
內存+交換分區大小總限制。格式同上。必須必-m 設置的大 |
--memory-reservation |
內存的軟性限制。格式同上 |
--oom-kill-disable |
是否阻止 OOM killer 殺死容器,默認沒設置 |
--oom-score-adj |
容器被 OOM killer 殺死的優先級,範圍是[-1000, 1000],默認爲 0 |
--memory-swappiness |
用於設置容器的虛擬內存控制行爲。值爲 0~100 之間的整數 |
--kernel-memory |
核心內存限制。格式同上,最小爲 4M |
OOM killer
默認情況下,在出現 out-of-memory(OOM) 錯誤時,系統會殺死容器內的進程來獲取更多空閒內存。這個殺死進程來節省內存的進程,我們姑且叫它 OOM killer。我們可以通過設置--oom-kill-disable
選項來禁止 OOM killer 殺死容器內進程。但請確保只有在使用了-m/--memory
選項時才使用--oom-kill-disable
禁用 OOM killer。如果沒有設置-m
選項,卻禁用了 OOM-killer,可能會造成出現 out-of-memory 錯誤時,系統通過殺死宿主機進程或獲取更改內存。
下面的例子限制了容器的內存爲 100M 並禁止了 OOM killer:
$ docker run -it -m 100M --oom-kill-disable ubuntu:16.04 /bin/bash
是正確的使用方法。
而下面這個容器沒設置內存限制,卻禁用了 OOM killer 是非常危險的:
$ docker run -it --oom-kill-disable ubuntu:16.04 /bin/bash
容器沒用內存限制,可能或導致系統無內存可用,並嘗試時殺死系統進程來獲取更多可用內存。
一般一個容器只有一個進程,這個唯一進程被殺死,容器也就被殺死了。我們可以通過--oom-score-adj
選項來設置在系統內存不夠時,容器被殺死的優先級。負值更加不可能被殺死,而正值更有可能被殺死。
接下來,刪除容器。 加上這個 --oom-kill-disable 重新運行容器。
docker run -dit \
-m 640M --memory-swap -1 \
--oom-kill-disable \
--net docker-network-dev \
--ip 192.168.0.100 \
--restart=always \
--privileged=true \
--name=keda6-dev-information-main \
--hostname=slave_informationr_main \
-v /home/docker/springCloud/project/keda6-information-main/:/var/local/project/ \
-v /home/springCloud/project/keda6-information-main/:/home/springCloud/ \
-v /etc/localtime:/etc/localtime \
-e TZ='Asia/Shanghai' \
-e LANG="en_US.UTF-8" \
-p 30009:30009 \
-p 20209:20209 \
-p 20009:9820 \
-p 2199:22 \
seowen/jdk8u241-project:latest \
/usr/sbin/init
然後新的問題出現了。
重啓容器後, 項目雖然不會被kill掉, 但因爲內存爆滿,達到容器限制,卻沒有出現 out-of-memory(OOM)錯誤,而是請求,一直卡在那裏。 原因,前面也說過了。 就是 容器剩餘的 內存,沒有來得及GC線程啓用,就瞬間被 堆佔用了。
解決方法:
減少 jvm 堆內存,騰出更多空閒內存,或 增加容器的 內存限制。 (現在能體會到JVM 最大堆默認爲 系統的 1/4的原因了)
再次重啓容器, 通過 java jvisualvm工具,可以看到 堆內存已經佔滿了,並且日誌中,也出現了 java.lang.OutOfMemoryError: Java heap space 異常信息了。
至此,問題是基本解決了。 但又有一個需要優化的地方。 那就是 在 啓動命令中, 我是設置了 -Xms -Xmx 的固定值。 原因,不用多說, 因爲jvm 默認對容器的資源限制是無感的。 而是以宿主機的資源爲主。那就會因爲 JVM內存達到容器限制,而被kill掉。
但這意味着需要控制內存兩次,一次在Docker中,一次在JVM中。每當想要做出改變時,必須做兩次,不理想。
能不能,有個辦法,可以讓JVM 感知 docker 容器的資源限制。 這樣,我可以不設置 JVM 堆的固定配置。還是開始找資料,發現 在JDK 8u131及以後版本開始支持了Docker的cpu和memory限制。
很幸運的是,本項目使用的是 JDK8u241版本。
memory limit
在java8u131+及java9,需要加上-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
才能使得Xmx感知docker的memory limit。
查看參數默認值
java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal
部分輸出
bool UseCGroupMemoryLimitForHeap = false {experimental} {default}
可以看到在java9,UseCGroupMemoryLimitForHeap參數還是實驗性的,默認關閉。
其他參數:
- -XX:MinRAMPercentage
- -XX:MaxRAMPercentage
- -XX:InitialRAMPercentage
這三個參數是JDK8U191爲適配Docker容器新增的幾個參數(單位 百分比),類比Xmx、Xms。
舉例說明:假如docker容器內存限制是6G,那麼:
-XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=75 -XX:MinRAMPercentage=75
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
等價於:
-Xmx4608m -Xms4608m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
這裏提供如下建議:
- 除非想爲 Java 進程壓榨額外內存,否則不要修改這些參數。在大部分情況下默認值 25% 對於內存管理來說是比較安全的。這個設置對內存來說可能並不是最有效的,但是內存是相對廉價的,同時相比於 JVM 進程在未知情況下被 OOM-kill,還是謹慎一些比較好。
- 如果非要調試這些參數,還是保守點爲妙。50% 通常是個安全值,可以避免(大部分)問題。當然,這還是主要取決於容器內存大小。我不推薦設置成 75%,除非容器至少有 768MB 內存(最好是 1GB),同時需要對應用程序的實際內存使用非常瞭解。
- 如果容器內除了 Java 進程之外還有其他進程,那麼在調整這些值的時候需要額外的注意。容器內存由其中所有進程共享,因此在這種情況下,瞭解整個容器內存使用會更加複雜。
- 設置成超過 90% 可能是自找麻煩。
最終啓動命令如下:
nohup java \
-Dcom.sun.management.jmxremote \
-Djava.rmi.server.hostname=192.168.1.126 \
-Dcom.sun.management.jmxremote.port=30009 \
-Dcom.sun.management.jmxremote.rmi.port=30009 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.access.file=/usr/local/jmx/jmxremote.access \
-Dcom.sun.management.jmxremote.password.file=/usr/local/jmx/jmxremote.password \
-Dcom.sun.management.jmxremote.ssl=false \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMPercentage=50.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:MinRAMPercentage=50.0 \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintFlagsFinal \
-XshowSettings:vm \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/springCloud/heapLogs \
-jar $JARR --server.port=9820 \
--eureka.instance.non-secure-port=20009 \
--management.server.port=20209 \
--eureka.instance.ip-address=192.168.1.126 \
--eureka.instance.hostname=192.168.1.126 \
--spring.profiles.active=dev &
注意:複製的時候,可能有空格格式問題,造成一些其他的報錯信息。
建議:
如果需要排查問題時,最好在 JVM 參數中加上
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution
讓 GC log 更加詳細,方便定位問題。