为公司新的架构的技术选型为,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版本。
在java8u131+及java9,需要加上-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
才能使得Xmx感知docker的memory limit。
查看参数默认值
java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal
部分输出
bool UseCGroupMemoryLimitForHeap = false {experimental} {default}
可以看到在java9,UseCGroupMemoryLimitForHeap参数还是实验性的,默认关闭。
其他参数:
这三个参数是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
这里提供如下建议:
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 更加详细,方便定位问题。