运行中的问题分为两个,或者说都是OOM的原因
- pid 为1 真的好吗
- jdk无法识别cgroup限制
第一种情况实际上php和java的容器在长期运行后经常会频繁oom,主要原因是代码里涉及到fork啥的。传统Linux上,pid为1的角色承担了孤儿和僵尸进程的回收,但是目前我们的业务进程都是pid为1的角色,没有处理掉孤儿进程,这里我们主进程用bash模仿个僵尸进程看看会不会被回收。
起一个容器exec运行sleep
docker run -d --name test centos bash -c 'sleep 1000'
docker exec -ti test bash # 运行一个bash操作容器
[root@134b96f29c73 /]# bash -c 'sleep 2000'
再开一个终端操作按照如下图操作
得到一个僵尸进程。解决这个的办法就是pid为1的跑一个支持信号转发且支持回收孤儿僵尸进程的进程就行了,为此有人开发出了tini项目,感兴趣可以github上搜下下,现在tini已经内置在docker里了。
使用tini可以在docker run的时候添加选项--init即可,底层我猜测是复制docker-init到容器的/dev/init路径里然后启动entrypoint cmd,大家可以在run的时候测试下上面的步骤会发现根本不会有僵尸进程遗留。
这里不多说,如果是想默认使用tini可以把tini构建到镜像里(例如k8s目前不支持docker run 的--init,所以需要把tini做到镜像里),参照jenkins官方镜像dockerfile和tini的github地址文档 https://github.com/krallin/tini
如果是基于alpine,tini是依赖glibc的可能还会command not found,可以尝试它的静态版本
ENV TINI_VERSION=v0.18.0 \
TINI_DOWNLOAD_URL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static
RUN curl -sSL ${TINI_DOWNLOAD_URL} > /usr/bin/tini \
&& chmod +x /usr/bin/tini
...
ENTRYPOINT ["tini","--"]
CMD ["/your/program","-and","-its","args"]
类似tini的还有dumb-init,例如后续的k8s的ingress nginx镜像就是
[root@centos7 temp]# cat test.py
import time
while(1):
print("hello")
time.sleep(3)
上面这样的一个脚本,间隔3秒输出,按理来说我们可以run后前台输出信息,或者-d后我们可以使用docker logs查看到,但是实际我们用docker跑起来测试下
[root@centos7 temp]# docker run --rm -v $PWD/test.py:/root/test.py python:3.6-alpine python /root/test.py
^CTraceback (most recent call last):
File "/root/test.py", line 4, in <module>
time.sleep(3)
KeyboardInterrupt
hello
hello
我们不带-d选项或者-d后无法使用docker logs查看到,上面的是我ctrl+c后才出来的。这个问题原因是缓存。
我们可以通过环境变量PYTHONUNBUFFERED=0
来关闭掉输出的缓存,或者主进程python -u xxx.py
[root@centos7 temp]# docker run --rm -e PYTHONUNBUFFERED=0 -v $PWD/test.py:/root/test.py python:3.6-alpine python /root/test.py
hello
hello
hello
^CTraceback (most recent call last):
File "/root/test.py", line 4, in <module>
time.sleep(3)
KeyboardInterrupt
主要原因是缓存,其他类似的可能也是tty选项需要带上-t选项,或者打印指定到系统的output的fd试试
参考:
- https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker/29745541
- https://docs.python.org/2/using/cmdline.html#cmdoption-u
试试这个
gliderlabs/docker-alpine#144 (comment)
https://github.com/Auswaschbar/alpine-localized-docker/blob/master/Dockerfile
参考https://qingmu.io/2018/12/17/How-to-securely-limit-JVM-resources-in-a-container/
但是他博客可能需要梯子,这里我简单说下
首先Docker容器本质是是宿主机上的一个进程,它与宿主机共享一个/proc目录,也就是说我们在容器内看到的/proc/meminfo
,/proc/cpuinfo
与直接在宿主机上看到的一致,如下
$ head -n3 /proc/meminfo
MemTotal: 197869260 kB
MemFree: 3698100 kB
MemAvailable: 62230260 kB
$ docker run -m 500m --rm alpine head -n3 /proc/meminfo # 带上内存限制选项无法识别
MemTotal: 197869260 kB
MemFree: 3698100 kB
MemAvailable: 62230260 kB
jvm也是读取/proc目录,会导致无法识别cgroup限制。默认情况下,JVM的Max Heap Size是系统内存的1/4,假如我们系统是8G,那么JVM将的默认Heap≈2G。
Docker通过CGroups完成的是对内存的限制,而/proc目录是已只读形式挂载到容器中的,由于默认情况下Java
压根就看不见CGroups的限制的内存大小,而默认使用/proc/meminfo
中的信息作为内存信息进行启动,
这种不兼容情况会导致,如果容器分配的内存小于JVM的内存,JVM进程申请超过限制的内存会被docker认为oom杀掉。
这一组测试我们使用最新的openjdk8-12,给容器限制内存为4G,看JDK默认参数下的最大堆为多少?看看我们默认参数下多少版本的JDK是安全的。
测试命令为如下:
docker run -m 4GB --rm openjdk:8-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:9-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:10-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:11-jre-slim java -XshowSettings:vm -version
docker run -m 4GB --rm openjdk:12 java -XshowSettings:vm -version
- OpenJDK8(并没有识别容器限制,26.67G) 危险
$ docker run -m 4GB --rm openjdk:8-jre-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 26.67G
Ergonomics Machine Class: server
Using VM: OpenJDK 64-Bit Server VM
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
- OpenJDK8 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正确的识别容器限制,910.50M)安全
$ docker run -m 4GB --rm openjdk:8-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 910.50M
Ergonomics Machine Class: server
Using VM: OpenJDK 64-Bit Server VM
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
- OpenJDK 9(并没有识别容器限制,26.67G)危险
$ docker run -m 4GB --rm openjdk:9-jre-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 29.97G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "9.0.4"
OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
- OpenJDK 9 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正确的识别容器限制,1G)安全
$ docker run -m 4GB --rm openjdk:9-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 1.00G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "9.0.4"
OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
- OpenJDK 10(正确的识别容器限制,1G)安全
$ docker run -m 32GB --rm openjdk:10-jre-slim java -XshowSettings:vm -XX:MaxRAMFraction=1 -version
VM settings:
Max. Heap Size (Estimated): 1.00G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)
- OpenJDK 11(正确的识别容器限制,1G)安全
$ docker run -m 4GB --rm openjdk:11-jre-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 1.00G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment (build 11.0.1+13-Debian-3)
OpenJDK 64-Bit Server VM (build 11.0.1+13-Debian-3, mixed mode, sharing)
- OpenJDK 12(正确的识别容器限制,1G)安全
$ docker run -m 4GB --rm openjdk:12 java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 1.00G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "12-ea" 2019-03-19
OpenJDK Runtime Environment (build 12-ea+23)
OpenJDK 64-Bit Server VM (build 12-ea+23, mixed mode, sharing)
docker run -m 4GB --rm adoptopenjdk/openjdk8-openj9:alpine-slim java -XshowSettings:vm -version
docker run -m 4GB --rm adoptopenjdk/openjdk9-openj9:alpine-slim java -XshowSettings:vm -version
docker run -m 4GB --rm adoptopenjdk/openjdk10-openj9:alpine-slim java -XshowSettings:vm -version
docker run -m 4GB --rm adoptopenjdk/openjdk11-openj9:alpine-slim java -XshowSettings:vm -version
- openjdk8-openj9 (正确的识别容器限制,3G)安全
$ docker run -m 4GB --rm adoptopenjdk/openjdk8-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Ergonomics Machine Class: server
Using VM: Eclipse OpenJ9 VM
openjdk version "1.8.0_192"
OpenJDK Runtime Environment (build 1.8.0_192-b12_openj9)
Eclipse OpenJ9 VM (build openj9-0.11.0, JRE 1.8.0 Linux amd64-64-Bit Compressed References 20181107_95 (JIT enabled, AOT enabled)
OpenJ9 - 090ff9dcd
OMR - ea548a66
JCL - b5a3affe73 based on jdk8u192-b12)
- openjdk9-openj9 (正确的识别容器限制,3G)安全
$ docker run -m 4GB --rm adoptopenjdk/openjdk9-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "9.0.4-adoptopenjdk"
OpenJDK Runtime Environment (build 9.0.4-adoptopenjdk+12)
Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 9 Linux amd64-64-Bit Compressed References 20180814_248 (JIT enabled, AOT enabled)
OpenJ9 - 24e53631
OMR - fad6bf6e
JCL - feec4d2ae based on jdk-9.0.4+12)
- openjdk10-openj9 (正确的识别容器限制,3G)安全
$ docker run -m 4GB --rm adoptopenjdk/openjdk10-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "10.0.2-adoptopenjdk" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2-adoptopenjdk+13)
Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 10 Linux amd64-64-Bit Compressed References 20180813_102 (JIT enabled, AOT enabled)
OpenJ9 - 24e53631
OMR - fad6bf6e
JCL - 7db90eda56 based on jdk-10.0.2+13)
- openjdk11-openj9(正确的识别容器限制,3G)安全
$ docker run -m 4GB --rm adoptopenjdk/openjdk11-openj9:alpine-slim java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.00G
Using VM: Eclipse OpenJ9 VM
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.1+13)
Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.11.0, JRE 11 Linux amd64-64-Bit Compressed References 20181020_70 (JIT enabled, AOT enabled)
OpenJ9 - 090ff9dc
OMR - ea548a66
JCL - f62696f378 based on jdk-11.0.1+13)
分析之前我们先了解这么一个情况:
JavaMemory (MaxRAM) = 元数据+线程+代码缓存+OffHeap+Heap...
一般我们都只配置Heap即使用-Xmx来指定JVM可使用的最大堆。而JVM默认会使用它获取到的最大内存的1/4作为堆的原因也是如此。
OpenJdk
OpenJdk8-12,都能保证这个安全性的特点(8和9需要特殊参数,-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap)。
OpenJ9
2.IbmOpenJ9所有的版本都能识别到容器限制。
OpenJdk
自动识别到容器限制后,OpenJdk把最大堆设置为了大概容器内存的1/4,对内存的浪费不可谓不大。
当然可以配合另一个JVM参数来配置最大堆。-XX:MaxRAMFraction=int
。下面是我整理的一个常见内存设置的表格, 从中我们可以看到似乎JVM默认的最大堆的取值为MaxRAMFraction=4
,随着内存的增加,堆的闲置空间越来越大,在16G容器内存时,java堆只有不到4G。
MaxRAMFraction取值 | 堆占比 | 容器内存=1G | 容器内存=2G | 容器内存=4G | 容器内存=8G | 容器内存=16G |
---|---|---|---|---|---|---|
1 | ≈90% | 910.50M | 1.78G | 3.56G | 7.11G | 14.22G |
2 | ≈50% | 455.50M | 910.50M | 1.78G | 3.56G | 7.11G |
3 | ≈33% | 304.00M | 608.00M | 1.19G | 2.37G | 4.74G |
4 | ≈25% | 228.00M | 455.50M | 910.50M | 1.78G | 3.56G |
OpenJ9
关于OpenJ9的的详细介绍你可以从这里了解更多。 对于内存利用率OpenJ9的策略是优于OpenJdk的。以下是OpenJ9的策略表格
容器内存<size> |
最大Java堆大小 |
---|---|
小于1 GB | 50%<size> |
1 GB - 2 GB | <size> - 512 MB |
大于2 GB | 大于2 GB |
- 注意:这里我们说的是容器内存限制,和物理机内存不同,
- 如果你想要的是,不显示的指定-Xmx,让Java进程自动的发现容器限制。
1.如果你想要的是jvm进程在容器中安全稳定的运行,不被容器kiil,并且你的JDK版本小于10(大于等于JDK10的版本不需要设置,参考前面的测试) 你需要额外设置JVM参数-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
,即可保证你的Java进程不会因为内存问题被容器Kill。 当然这个方式使用起来简单,可靠,缺点也很明显,资源利用率过低(参考前面的表格MaxRAMFraction=4)。
2.如果想在基础上我还想提高一些内存资源利用率,并且容器内存为1 GB - 4 GB,我建议你设置-XX:MaxRAMFraction=2
,在大于8G的可以尝试设置-XX:MaxRAMFraction=1
(参考上表格)。
-
如果你想要的是手动挡的体验,更加进一步的利用内存资源,那么你可能需要回到手动配置时代-Xmx。
-
手动挡部分,请可以完全忽略上面我的BB。
1.上面的我们说到了自动挡的配置,用起来很简单很舒服,自动发现容器限制,无需担心和思考去配置-Xmx。
2.比如你有内存1G那么我建议你的-Xmx750M,2G建议配置-Xmx1700M,4G建议配置-Xmx3500-3700M,8G建议设置-Xmx7500-7600M,
总之就是至少保留300M以上的内存留给JVM的其他内存。如果堆特别大,可以预留到1G甚至2G。
3.手动挡用起来就没有那么舒服了,当然资源利用率相对而言就更高了。
最后说点我自己的看法,可以entrypoint去判断然后case几种情况拼接下最后的java主进程参数