Docker容器异常退出码和排查¶
在运行docker容器时,可能会看到docker容器异常退出,例如,在 Alpine Linux软件开发环境构建 中,我执行:
docker run -itd --hostname x-node --name x-node -p 3000:3000 alpine-node:latest
结果发现容器没有启动:
$ docker ps --all CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 31639111bd58 alpine-node:latest "node app.js" 18 hours ago Exited (1) 18 hours ago x-node
最佳实践¶
查找容器以及容器退出码¶
-
列出所有退出的容器:
docker ps --filter "status=exited"
-
可以通过容器名字来过滤:
docker ps -a | grep node
例如输出:
db28254c826d alpine-node "/bin/sh -s" 3 hours ago Exited (130) 3 hours ago competent_grothendieck
-
通过以下命令可以直接获取容器的退出码:
docker inspect <container-id> --format='{{.State.ExitCode}}'
上述案例就是:
docker inspect db28254c826d --format='{{.State.ExitCode}}'
显示就是退出码:
130
常见退出码¶
退出码 | 含义 |
---|---|
0 | 一个归属的前台进程退出(通常是执行完成) |
1 | 由于应用程序错误导致的失败 |
137 | 表示容器接收到 |
139 | 表示容器接收到 |
143 | 表示容器接收到 |
docker logs¶
-
首先检查docker容器日志:
docker logs x-node
docker logs
命令可以看到异常如下:
/home/node/code/app.js:3 # const hostname = '127.0.0.1'; ^ SyntaxError: Invalid or unexpected token at Object.compileFunction (node:vm:352:18) at wrapSafe (node:internal/modules/cjs/loader:1032:15) ...
显然 app.js
语法错误 - js 注释应该使用 //
覆盖镜像命令¶
由于Dockerfile修改需要重新build,对于调试不是很方便。 docker
提供了直接覆盖 CMD
和 ENTRYPOINT
的方法,也就是直接在 docker run
命令中传递运行参数:
docker run -it --entrypoint /bin/bash $IMAGE_NAME -s
上述命令会使容器运行 /bin/bash
,并获得 -s
作为 CMD
,可以覆盖镜像中指令。这样就可以获得一个交互的bash环境: 实际上就是登陆到容器系统中,这样方便在容器中执行命令,检查输出,并查看问题。
举例,我使用如下命令,使用 alpine-node
镜像运行一个交互shell的容器(但是不执行Dockerfile最后的 node app.js
命令):
docker run -it --entrypoint /bin/sh alpine-node -s
此时就会看到进入容器的应用目录:
/home/node/code #
可以检查容器,手工运行命令:
/home/node/code # ls app.js /home/node/code # df -h Filesystem Size Used Available Use% Mounted on overlay 31.2G 2.3G 27.4G 8% / tmpfs 64.0M 0 64.0M 0% /dev tmpfs 924.3M 0 924.3M 0% /sys/fs/cgroup shm 64.0M 0 64.0M 0% /dev/shm /dev/sda2 31.2G 2.3G 27.4G 8% /etc/resolv.conf /dev/sda2 31.2G 2.3G 27.4G 8% /etc/hostname /dev/sda2 31.2G 2.3G 27.4G 8% /etc/hosts devtmpfs 10.0M 0 10.0M 0% /dev/null devtmpfs 10.0M 0 10.0M 0% /dev/random devtmpfs 10.0M 0 10.0M 0% /dev/full devtmpfs 10.0M 0 10.0M 0% /dev/tty devtmpfs 10.0M 0 10.0M 0% /dev/zero devtmpfs 10.0M 0 10.0M 0% /dev/urandom devtmpfs 10.0M 0 10.0M 0% /proc/keys devtmpfs 10.0M 0 10.0M 0% /proc/latency_stats devtmpfs 10.0M 0 10.0M 0% /proc/timer_list tmpfs 924.3M 0 924.3M 0% /sys/firmware /home/node/code # node app.js Server running at http://${hostname}:${port}/
总之,可以完整实现交互验证
参考¶
排查Docker容器问题的5个简便方法¶
和 Kubernetes排查 类似,实际上Docker的排查不外乎检查日志、状态以及通过巧妙方法重试和排查日志,特别是Docker出现crash的时候。
容器日志¶
如果 docker run
运行失败,你可以通过检查容器日志找到蛛丝马迹:
docker logs <container_id>
上述命令会获得容器初始化的完整 STDOUT
和 STDERR
,从而找到线索
容器状态¶
如果你需要不断检查容器是否正常运行,通常可以检查状态,例如扫描大量的容器来获得系统运行健康度:
docker stats <container_id>
复制容器文件¶
有时候从容器内部获取文件,例如 coredump 文件,进行检查能够帮助我们排查问题:
docker cp <container_id>:/path/to/useful/file /local-path
启动容器bash¶
通常我们会在容器中直接运行服务,但是有时候容器可能crash,你需要调试的话,可以先运行容器的shell,通过shell进入容器去执行服务,排查日志:
docker exec -it <container_id> /bin/bash
快照和运行¶
如果不能启动容器,有一个技巧可以帮助排查问题,即将容器快照保存下来,立即以这个快照来运行shell进行排查:
docker commit <container_id> my-broken-container && docker run -it my-broken-container /bin/bash
保存关闭容器的当前状态作为镜像,然后启动基于该镜像的shell,可以避免启动问题,并且进入容器排查
参考¶
从进程pid反推获得该进程所属容器¶
在生产环境中排查系统异常,经常会遇到某个进程异常,如D状态。此时我们需要找出这个进程所属哪个容器,以便找到对应的应用服务器进行排查。
通过procfs获取容器信息¶
通常我们使用 systemd-cgls
可以比较容易通过树形结构找出某个进程以及对应进程的容器,不过,在脚本中,其实还有一个办法,就是直接从 procfs
找出进程的 cgroup
来确定所属容器:
cat /proc/<process-pid>/cgroup
此时会看到所属cgroup的字符串,这个字符串就对应容器id,可以进一步获取容器名字:
docker inspect --format '{{.Name}}' "${containerId}" | sed 's/^\///'
反复查找ppid直到找到容器名¶
另一个思路是不断查找进程的父id,直到匹配上容器名特征,此时就能顺藤摸瓜找出容器:
获取docker的容器¶
#!/bin/bash cpid=$1 while true; do ppid=$(ps -o ppid= -p $cpid) pname=$(ps -o comm= -p $ppid) if [ "$pname" == "docker" ]; then echo "$cpid parent $ppid ($pname)" break else echo "$cpid parent $ppid ($pname)" cpid=$ppid fi done docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}' | grep $cpid
类似思路,如果不是运行docker,而是使用 containerd运行时(runtime) ,则脚本修订:
获取containerd的容器¶
#!/bin/bash cpid=$1 while true; do ppid=$(ps -o ppid= -p $cpid) pname=$(ps -o comm= -p $ppid) if [ "$pname" == "containerd-shim" ]; then echo "$cpid parent $ppid ($pname)" break else echo "$cpid parent $ppid ($pname)" cpid=$ppid fi done crictl ps -q | xargs crictl inspect --output go-template --template '{{.info.pid}}, {{.status.metadata.name}}' | grep $cpid