【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%,除非容器至少有 512MB 内存(最好是 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 更加详细,方便定位问题。

发布了125 篇原创文章 · 获赞 43 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/weixin_42697074/article/details/105513414