【SpringCloud分佈式框架搭建】一文讀懂,docker容器部署springCloud微服務莫名停止的原因

前提說明:

爲公司新的架構的技術選型爲,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選項來設置在系統內存不夠時,容器被殺死的優先級。負值更加不可能被殺死,而正值更有可能被殺死。

更多詳細:Docker容器CPU、memory資源限制

接下來,刪除容器。 加上這個 --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 更加詳細,方便定位問題。

 

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