Skip to content

Latest commit

 

History

History
557 lines (407 loc) · 23.9 KB

4.errors-while-running.md

File metadata and controls

557 lines (407 loc) · 23.9 KB

4.运行中的问题

运行中的问题

pid 为1 真的好吗

第一种情况实际上 php 和 java 的容器在长期运行后经常会频繁 oom,主要原因是代码里涉及到 fork 啥的。传统 Linux 上,pid 为 1 的角色承担了孤儿和僵尸进程的回收,但是目前我们的业务进程都是 pid 为1的角色,没有处理掉孤儿进程,这里我们主进程用 bash 模仿个僵尸进程看看会不会被回收。

起一个容器 exec 运行sleep

docker run -d --name test centos:7 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 镜像就是

python 的话我们项目经常发现产生僵尸进程。排查僵尸进程的方法见: Linux 僵尸进程排查

有个别项目加了下面代码还是有,后面加了 tini 或者docker 启动的时候加 --init 的时候解决了

import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)

/etc/nsswitch.conf

alpine 之类的小的 rootfs 会没这个文件,导致很多依赖 hosts 的进程无法解析 hosts,你手动进去 ping 是能通的,如果是 k8s 因为走集群 dns,所以你域名解析啥的正常,但是如果你 docker 运行依赖 hosts 文件的话会不正常。所以如果使用 alpine 做基础镜像最好加一行:

RUN if [ ! -e /etc/nsswitch.conf ];then echo 'hosts: files dns' > /etc/nsswitch.conf; fi

快速检查容器是否有这个文件:

while read ctr;do
    echo -n "${ctr} "
    sudo docker exec $ctr sh -c 'if [ -f /etc/nsswitch.conf ];then echo ok;else echo no; fi'
done < <(docker ps -a --filter status=running --format '{{.Names}}')

exec -t 选项的坑

可能需要利用 exec 写一些 shell 脚本,如果你习惯性的 -ti 的话,会是 windows 的换行符。这会导致你从命令输出取值会多了 \r 导致奇怪的问题。

$ docker exec test printenv NGINX_VERSION | cat -A
1.19.8$
$ docker exec -t test printenv NGINX_VERSION | cat -A
1.19.8^M$
$ docker exec -ti test printenv NGINX_VERSION | cat -A
1.19.8^M$
$ docker exec -i test printenv NGINX_VERSION | cat -A
1.19.8$

alpine 的 hosts 单ip多域名的坑

和域名的个数没关系,和字符串长度有关系,只在 alpine 上有这个问题。底层原因不清楚,发现是因为 k8s 的 yaml 里我们屏蔽了多个域名发现不生效:

...
  spec:
    hostAliases:
      - ip: 127.0.0.1
        hostnames:
          - xxx
          - xxx2
          ...

如果使用 alpine 记得不要一个ip和多个域名在同一行 hosts 条目,下面是 www1www 对比

$ docker run --name test --rm  -d  nginx:alpine
$ ctr_hosts=`docker inspect test --format '{{.HostsPath}}'`
$ echo $(echo 127.0.0.1 $(seq -f "www1.zhangguanzhang%g.com" 20 | paste -d " " -s -)) \
    >> $ctr_hosts
$ docker exec -ti test ping -c 1 www1.zhangguanzhang20.com
ping: bad address 'www1.zhangguanzhang20.com'
$ docker exec -ti test cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.13.5
PRETTY_NAME="Alpine Linux v3.13"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

$ docker stop test
$ docker run --name test --rm  -d  nginx:alpine
$ ctr_hosts=`docker inspect test --format '{{.HostsPath}}'`
$ echo $(echo 127.0.0.1 $(seq -f "www.zhangguanzhang%g.com" 20 | paste -d " " -s -)) \
    >> $ctr_hosts
$ docker exec -ti test ping -c 1 www.zhangguanzhang20.com
PING www.zhangguanzhang20.com (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.057 ms

--- www.zhangguanzhang20.com ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.057/0.057/0.057 ms

filter name的坑

注意是模糊匹配,全名使用^/xxx$

$ docker run -d --rm --name test $IMG
5a880656b0dc55ed58ab8af0669eaf1ac63dae95df819f184a3ff12920d198bf
$ docker run -d --rm --name test-web $IMG
be2e0560a56fe735320179bf9eaa65cbf6ef1057d07265706a28c75c47939b12
$ docker run -d --rm --name t-test $IMG
45d0d7edd69da78833029299d4846dea7e4a2295b22f0ea512f6a6661cbea549
$ docker ps -a --filter=name=test --format {{.Names}}
t-test
test-web
test
$ docker ps -a --filter=name=^/test$ --format {{.Names}}
test

python或者类似其他的无法使用docker logs查看日志的问题

[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试试

参考:

alpine的locale

试试这个

gliderlabs/docker-alpine#144 (comment)

{% embed url="https://github.com/Auswaschbar/alpine-localized-docker/blob/master/Dockerfile" %}

[Errno 28] No space left on device

容器的 /dev/shm 是 64m,dockerd 的选项配置的

$ dockerd --help |& grep shm
      --default-shm-size bytes                  Default shm size for containers (default 64MiB)

有时候某些进程会并发读写这个导致报错这个。可以起容器的时候加选项,例如 --shm-size=256m

镜像的时区

环境变量 TZ 设置时区依赖于 tzdata这个包,或者可以挂载宿主机的/etc/localtime , 如果 alpine 像下面这样后时区还是无效,可以试试 -e TZ='CST-8'

ENV TZ Asia/Shanghai

RUN apk update \
    && apk add bash tzdata ... \
    && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo Asia/Shanghai > /etc/timezone \

不要删掉 tzdata,网上有些文章误人子弟,删了后可能时区是对的,但是如果声明 TZ 变量后时区就错了。另一方面就是源码了,例如 java 可以 -Duser.timezone=GMT+08, golang 则要代码里设置.

java项目的hostname

java 项目,例如 kafka 还是 zk 来着,以及可能你引入的包,启动的时候会解析 hostname,无法解析可能会无法启动或者启动非常慢,k8s 里会给 pod 里容器添加 hostname 的 /etc/hosts 解析。

但是(包括 docker ) host 网络下设置 hostname 不等于宿主机的 hostname 的时候,这个 host 网络的容器里 /etc/hosts 没有此刻 hostname 的记录,你需要下面这条加到你容器的启动脚本里以防万一:

grep -Eqw $HOSTNAME /etc/hosts || echo "127.0.0.1 $HOSTNAME" >> /etc/hosts

docker的重启策略

容器的 --restart 被设置为 always 的时候,如果你停止这个容器,在宿主机 reboot 后,这个容器会被拉起来。

如果你在做一些容灾关机开机测试,想停止一些容器减少副本的时候,需要操作的容器的重启策略设置为 unless-stopped

``

pm2 环境变量数量与 k8s

k8s 的 pod 默认开启 enableServiceLinks,开启下会把同一个 namespace 里所有的 service 和 service 的 port 按照一定规则生成环境变量传递给 pod 的容器,而 pm2 在启动时会导入系统中的环境变量,当环境变量数量过多时,就会报错[PM2][ERROR] Process failed to launch spawn E2BIG,内部测试了下 pm2 大概环境变量数量为大于等于 2050 的时候会报这个错误,数量不一定是这个数量。为了避免出现这样的问题,可以启动脚本里加下面的逻辑:

if [ -n "$KUBERNETES_SERVICE_PORT" ];then
    # XXX 是你们服务的特定关键词,防止你们的 env key也被过滤出来 unset 了
    for i in $(env |grep -Ev '^XXX' | grep -E '_SERVICE_|_PORT' | cut -f1 -d=); do unset $i; done
fi

也可以直接 enableServiceLinks: falsekubelet 的 getServiceEnvVarMap 方法设置容器的 env 源码 可以看到是始终传入 kube-apiserver 的一些 env 的,不用担心设置为 false 时候影响到 pod 内 k8s sdk 的功能

低版本内核下容器内部无法使用 unix socket 通信

低版本内核下容器内部无法使用 unix socket 通信

jdk无法识别cgroup限制

参考:

  1. https://qingmu.io/2018/12/17/How-to-securely-limit-JVM-resources-in-a-container/
  2. https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits

但是他博客可能需要梯子,这里我简单说下

首先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杀掉。

测试用例1(OPENJDK)

这一组测试我们使用最新的 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
  • OpenJDK8u212之前版本 (并没有识别容器限制,26.67G) 危险
$ docker run -m 4GB --rm  openjdk:8u181 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)
  • OpenJDK8u212之后版本 (正确的识别容器限制,910.50M)安全
$ docker run -m 4GB --rm  openjdk:8-jdk java -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_265"
OpenJDK Runtime Environment (build 1.8.0_265-b01)
OpenJDK 64-Bit Server VM (build 25.265-b01, 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)

测试用例2(IBMOPENJ9)

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作为堆的原因也是如此。

安全性(即不会超过容器限制被容器kill)

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主进程参数

注意: jdk在8u191-XX:MaxRAMFraction=1选项是一件非常危险的事情