容器死了之后,先看什么
一句话总结
容器诊断应按日志 → 退出码 → 状态字段的顺序进行。若这三项仍无法解决,再进入容器内部。
为什么需要了解这一点
“容器启动不了”这句话,实际上至少把四种完全不同的情况混在了一起:镜像不存在、命令不存在、应用因无法读取配置而自行退出,或应用被内核终止。这四种情况的处理方法完全不同,好在可以通过成本很低的信号加以区分。
退出码最先缩小调查范围
仅凭一个退出码,就能把调查范围缩小一半。规则很简单:大于 128 表示进程因信号终止,减去 128 就是信号编号。
| 代码 | 含义 | 首先查看哪里 |
|---|---|---|
| 0 | 正常退出 | 应用完成工作后退出;如果它是服务器,这也不正常 |
| 1 | 应用返回的一般错误 | 日志最后一行 |
| 125 | Docker 自身错误 | 命令选项错误 |
| 126 | 无法执行命令 | 没有执行权限 |
| 127 | 找不到命令 | ENTRYPOINT 路径错误,或镜像中没有 Shell |
| 137 | SIGKILL (128+9) | 必须区分是否由 OOM 导致 |
| 139 | SIGSEGV (128+11) | 原生库崩溃 |
| 143 | SIGTERM (128+15) | 收到正常终止请求;未必是事故 |
137 最容易令人混淆。它可能是内核 OOM Killer 所为,也可能是有人执行了 docker kill。区分依据是 State.OOMKilled 这一行。
docker inspect dk-err --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
137 true
若为 true,应提高内存限制或降低应用内存使用量;若为 false,则应调查是谁、为什么终止了进程。这是两个完全不同的调查方向。
如何阅读日志
容器的标准输出与标准错误会被运行时截获并保存。因此,即使容器已经退出,也能通过 docker logs 查看最后时刻。两个流显示时混在一起,但实际上相互独立,可以用 Shell 重定向分别提取。
docker logs dk-err 2>/dev/null # 표준 출력만
docker logs dk-err 2>&1 1>/dev/null # 표준 오류만
docker logs --tail 50 -t dk-err # 마지막 50줄에 시각을 붙여서
docker logs --since 10m dk-err # 최근 10분만
附加时间(-t)很重要。许多应用自己的日志没有时间;如果没有时间,就无法判断“这个错误发生在进程退出前一刻,还是很久以前”。
docker inspect 会在一个 JSON 中保存全部状态。实际诊断中常用的字段并不多。
| 字段 | 能说明什么 |
|---|---|
State.Status |
created / running / exited / paused |
State.ExitCode |
是应用返回,还是被信号终止 |
State.Error |
运行时根本无法启动进程的原因 |
State.OOMKilled |
137 是否由 OOM 导致 |
State.StartedAt / FinishedAt |
运行几秒后退出——立即退出通常是配置问题 |
RestartCount |
是否在悄悄重复重启 |
如果 StartedAt 与 FinishedAt 相差不到一秒,应按配置文件、环境变量、端口冲突的顺序检查,而不是先怀疑应用业务逻辑。
实际工作中的表现
最常见的陷阱是日志为空。原因有三种。
- 应用把日志写进文件。 容器中的原则是将日志写到标准输出而非文件;违反这一原则会使整套诊断工具失效。临时可以用
docker exec进入后读取文件,但容器一旦退出,连这种方法也无法使用。 - 日志困在缓冲区里。 Python 发现标准输出不是终端时,会采用块缓冲。若不足 4KB 就退出,缓冲中的日志会全部消失。可通过
PYTHONUNBUFFERED=1或python -u关闭。Node 默认不缓冲,Java 的System.out通常按行处理,所以一般没有问题。 - 使用了其他日志驱动。 若
--log-driver设置为none或外部收集器,docker logs将无法显示任何内容。
第二个陷阱是镜像中没有 Shell。distroless 或 scratch 镜像不包含 sh,因此 docker exec -it ... sh 不会工作。看到 OCI runtime exec failed: exec: "sh": executable file not found 后,不应判断“容器异常”;这是正常现象。
此时可以把含有工具的容器连接到相同命名空间进行检查。
docker run -it --rm --pid container:myapp --network container:myapp nicolaka/netshoot
通过 --pid 共享进程,通过 --network 共享网络,因此 ps 与 ss 会直接显示目标容器中的状态。文件系统并不共享,应通过 /proc/1/root/ 查看。
第三个陷阱是不断重启。如果 RestartCount 持续增加,docker logs 只显示当前实例的日志。真正需要的是上一个、刚刚退出的实例日志,但 Docker 没有像 Kubernetes --previous 那样的选项。应暂时把 --restart 策略改为 no,让它只启动一次后再调查。
下个练习将做什么
练习从日志尾部截取内容、分离两个流,并把已退出容器的退出码与日志放在一起确定原因。还会设置较低的内存限制,亲自制造退出码 137,并确认 OOMKilled 显示为 true。