Skip to content

Docker 生产故障排查 Runbook:从第一条命令到根因

“服务访问不了”不是根因,它只是用户看到的结果。容器可能根本没有创建、创建后无法启动、启动后立即退出、正在反复重启、进程还在但应用不健康、端口映射错误、Docker DNS 失败、挂载覆盖了配置、cgroup 内存超限,甚至 Docker daemon 或宿主机本身已经异常。

本页不是命令速查,而是一份可以按顺序执行的生产 Runbook。每一步都说明:为什么查、从哪里取证、输出怎么看、下一步走哪条分支,以及哪些操作会破坏现场。

学习目标

学完后,应能:

  1. 在五分钟内判断故障属于宿主机、Docker daemon、镜像、容器生命周期、应用、网络、存储还是下游依赖。
  2. 根据“运行中、已退出/重启中、已删除/宿主机失联”选择不同证据源。
  3. 正确解释 Running、Healthy、Restarting、ExitCode、OOMKilled 和容器 PID。
  4. 从外到内排查端口访问失败,从 DNS 到 TCP 再到应用协议定位依赖连接失败。
  5. 区分 Java Heap OOM、容器总内存 OOM、宿主机 OOM、主动 SIGKILL 和停止超时。
  6. 定位 CPU 高、内存增长、线程/PID耗尽、磁盘打满、日志暴涨和 IO 慢。
  7. 在不泄露密码、Token、证书和业务数据的前提下保存事故证据。
  8. 明确什么时候可以重启止损,什么时候必须先 Dump、抓包或保存日志。
  9. 写出根因、触发条件、扩大因素和长期修复,而不是把“重启恢复”当作结论。

一、排障前先建立三个原则

1.1 先止损,但止损不等于先重启

生产故障首先判断业务影响:

  • 是否正在影响支付、订单、采集、库存等核心链路。
  • 是否只有一个实例异常,其他实例能否承接流量。
  • 是否还能灰度摘除异常实例。
  • 是否有数据持续写错、重复消费或丢失风险。

如果存在多个实例,可以先从负载均衡摘除故障实例,再保留它取证。如果只有一个实例且业务完全中断,可能必须先恢复服务,但至少应尽快保存低成本证据:状态、inspect、日志、events、资源快照和镜像身份。

mermaid
flowchart TD
    A["收到Docker服务故障告警"] --> B["确认业务影响和故障范围"]
    B --> C{"是否有其他健康实例承接"}
    C -- "有" --> D["摘流故障实例并保留现场"]
    C -- "没有" --> E{"是否持续产生错误数据"}
    E -- "是" --> F["停止写入或隔离流量"]
    E -- "否" --> G["保存最小证据后恢复服务"]
    D --> H["进入根因排查"]
    F --> H
    G --> H

1.2 先保存事实,再提出假设

错误做法:

text
看到Exit 137

直接认定Java堆泄漏

增大Xmx并重启

正确做法:

text
看到Exit 137

确认OOMKilled、events、内核日志和监控

区分容器总内存超限、主动kill、停止超时

若为内存问题再区分Heap、Direct、线程栈和native

排障结论必须由证据支持。日志中的最后一行不一定是根因,可能只是进程被杀前最后来得及打印的信息。

1.3 一次只改变一个关键变量

同时执行这些操作会让原因无法复现:

  • 升级镜像。
  • 修改环境变量。
  • 增大内存。
  • 更换网络。
  • 删除 Volume。
  • 清理 Docker 数据。

即使服务恢复,也无法知道哪个变化真正有效。应记录每次操作的时间、执行人、命令、对象、前后状态和结果。

二、先判断现场属于哪一种状态

2.1 容器仍在运行

可获取:

  • 当前 PID 和进程树。
  • 实时 CPU、内存、PIDs、网络和块 IO。
  • 应用线程、Heap、Native Memory 等运行时信息。
  • 容器内监听端口、DNS、路由和挂载视图。
  • 抓包、线程 Dump、Heap Dump、JFR 等动态证据。

风险:不当 exec、Dump 或抓包也会消耗资源,严重故障下可能加重问题。

2.2 容器已经退出或正在Restarting

仍可能获取:

  • docker inspect 中的上一次 State。
  • ExitCode、OOMKilled、FinishedAt、Error。
  • 容器日志驱动保留的 stdout/stderr。
  • Docker events。
  • 容器可写层中的文件,只要容器对象尚未删除。
  • 已挂载 Volume 中的持久文件。

无法直接依赖:

  • 当前 docker stats
  • 当前进程和线程。
  • 进程内尚未落盘的证据。

反复重启时,可先在业务允许且已确认影响后停止自动重启,防止旧日志被滚动覆盖和依赖被持续冲击。但停止操作会改变状态,必须先保存现有 inspect、日志与 events。

2.3 容器已经删除或宿主机失联

docker inspect 容器名 已无法工作。证据主要来自:

  • 集中日志平台。
  • Prometheus、云监控、APM 和链路追踪。
  • Docker daemon 日志与宿主机 journal。
  • Linux 内核 OOM 日志。
  • CI/CD 发布记录和镜像仓库 Digest。
  • 审计日志、堡垒机命令记录和 Docker events 的外部采集。
  • 持久 Volume、对象存储和数据库日志。
  • 云平台主机事件、磁盘和网络事件。

这说明生产必须在事故发生前建设外部可观测性。容器删除后再临时想找历史 CPU、线程和 stdout,通常已经来不及。

三、按层定位,不要把所有问题都叫“Docker问题”

mermaid
flowchart TD
    A["用户请求失败"] --> B["入口与宿主机网络"]
    B --> C["Docker daemon与容器对象"]
    C --> D["端口发布和容器网络"]
    D --> E["容器主进程与应用监听"]
    E --> F["应用配置和运行时资源"]
    F --> G["数据库、Redis、MQ等依赖"]
    G --> H["业务数据和协议语义"]
层次典型现象主要证据
宿主机整台机器失联、磁盘满、内核OOM主机监控、journal、dmesg、云事件
Docker daemon所有docker命令失败、daemon无响应docker info、服务状态、daemon日志
镜像与创建pull失败、架构错误、Entrypoint不存在pull/build日志、image inspect、State.Error
容器生命周期Created、Exited、Restartingps -a、State、events、logs
网络DNS失败、refused、timeoutnetwork inspect、端口、监听、抓包
存储Permission denied、数据消失、只读文件系统Mounts、UID/GID、磁盘、内核日志
应用/JVMOOM、Full GC、线程池耗尽应用指标、GC日志、Dump、Arthas/JFR
下游依赖DB、Redis、MQ不可用依赖监控、连接日志、网络和账号权限

四、前五分钟最小取证清单

假设容器名为 order-api

4.1 记录时间、主机和Docker上下文

powershell
Get-Date -Format o
hostname
docker context show
docker version
docker info

为什么先看 context:Docker CLI 可能连接本机、远程 daemon、Docker Desktop 或其他 context。查错环境会出现“明明容器不存在”或误操作其他主机。

Linux:

bash
date --iso-8601=seconds
hostname
docker context show
docker version
docker info

4.2 保存容器列表和完整状态

bash
docker ps -a --no-trunc --filter "name=order-api"
docker inspect order-api

快速提取 State:

bash
docker inspect \
  --format 'status={{.State.Status}} running={{.State.Running}} restarting={{.State.Restarting}} pid={{.State.Pid}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} error={{json .State.Error}} health={{json .State.Health}}' \
  order-api

4.3 保存日志与事件时间线

bash
docker logs --timestamps --since 30m order-api
docker events --since 30m --filter container=order-api

docker events 是持续流。生产取证可以指定 --until,或由监控系统长期采集。不要让交互命令无限占用终端而漏掉其他检查。

4.4 保存镜像、命令、端口、网络、挂载和资源限制

bash
docker inspect \
  --format 'image={{.Image}} path={{json .Path}} args={{json .Args}} restart={{json .HostConfig.RestartPolicy}} resources={memory:{{.HostConfig.Memory}},nanoCpus:{{.HostConfig.NanoCpus}},pids:{{.HostConfig.PidsLimit}}} ports={{json .NetworkSettings.Ports}} networks={{json .NetworkSettings.Networks}} mounts={{json .Mounts}}' \
  order-api

4.5 容器运行时保存资源和进程快照

bash
docker stats --no-stream order-api
docker top order-api
docker port order-api

这只是当前快照,不等于历史趋势。还要查看告警前后的监控曲线。

4.6 脱敏要求

完整 inspect 可能包含:

  • 数据库密码。
  • Token。
  • 云访问密钥。
  • 内部域名和 IP。
  • 标签中的业务数据。
  • 挂载的宿主机敏感路径。

原始证据应进入受控事故目录,分享给无关人员前脱敏。不要直接把 docker inspect 全量输出粘贴到公开群、外部工单或公共仓库。

五、第一棵决策树:Docker命令能否正常工作

mermaid
flowchart TD
    A["执行docker version"] --> B{"Client能否连接Server"}
    B -- "不能" --> C["确认Docker context"]
    C --> D["检查daemon或Docker Desktop状态"]
    D --> E["检查socket权限与磁盘"]
    B -- "能" --> F["执行docker ps -a"]
    F --> G{"目标容器是否存在"}
    G -- "不存在" --> H["查项目名、主机、Compose、发布记录"]
    G -- "存在" --> I["按容器状态继续"]

5.1 Cannot connect to the Docker daemon

先看当前 context:

bash
docker context ls
docker context show

Linux 使用 systemd 时:

bash
systemctl status docker --no-pager
journalctl -u docker --since "30 minutes ago"

检查:

  • Docker 服务是否停止或卡死。
  • /var/run/docker.sock 是否存在。
  • 当前用户是否有权限访问 socket。
  • 根分区或 Docker Root Dir 是否已满。
  • daemon 配置 JSON 是否语法错误。
  • 证书、远程 daemon 网络或 context 是否失效。

不要为了绕过权限直接长期使用高权限或开放 Docker socket。Docker socket 通常接近宿主机管理员权限,应修正用户组、sudo 策略和访问控制。

5.2 Docker Desktop场景

Windows/macOS 下 Docker Engine 通常运行在 Docker Desktop 管理的 Linux VM 或 WSL2 环境中。需要区分:

  • Windows 主机文件系统。
  • WSL2 发行版。
  • Docker Desktop VM。
  • Linux 容器内部。

“D盘有空间”不代表 Docker Desktop 虚拟磁盘有空间;Windows 路径存在也不代表已允许文件共享。先看 Docker Desktop 状态、context、WSL2 状态和 Docker 数据位置。

六、第二棵决策树:容器处于什么状态

mermaid
flowchart TD
    A["docker ps -a定位目标容器"] --> B{"State.Status"}
    B -- "created" --> C["创建成功但进程未启动"]
    B -- "running" --> D{"Health是否异常"}
    B -- "restarting" --> E["抓取每次退出原因和重启策略"]
    B -- "exited" --> F["检查ExitCode、OOMKilled、日志和events"]
    B -- "paused" --> G["确认是谁暂停以及业务影响"]
    D -- "healthy或无health" --> H["检查监听、网络、资源和业务"]
    D -- "unhealthy" --> I["检查health命令和应用状态"]

6.1 Created

表示容器对象已经创建,但主进程未处于运行状态。可能原因:

  • 只执行了 docker createdocker compose create
  • 启动阶段遇到挂载、设备、端口或 runtime 错误。
  • Docker daemon 尚未真正启动它。

检查:

bash
docker inspect --format '{{json .State}}' order-api
docker start -a order-api
docker events --since 30m --filter container=order-api

在生产执行 start -a 前要确认是否会重复执行初始化或写业务数据。对无副作用的失败容器,附着启动输出有助于看到 daemon 返回的直接错误。

6.2 Running

只证明容器主进程还存在,不证明:

  • 应用已经启动完成。
  • 端口正在监听。
  • 健康检查通过。
  • 线程池和连接池有容量。
  • 下游依赖可用。
  • 业务请求成功。

继续检查健康、监听、错误率、P99 和业务指标。

6.3 Restarting

说明主进程反复退出,重启策略又将其启动。保存:

bash
docker inspect --format 'restartCount={{.RestartCount}} state={{json .State}} policy={{json .HostConfig.RestartPolicy}}' order-api
docker logs --timestamps --tail 500 order-api
docker events --since 30m --filter container=order-api

不要只盯日志最后一轮。需要把每次启动时间、退出时间、退出码和日志边界对应起来。

6.4 Exited

重点看:

bash
docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}' order-api

退出码只是线索:

常见值常见含义不能直接下的结论
0主进程正常退出不代表服务按预期长期运行
1应用通用失败不能只靠1判断具体原因
126命令找到但不可执行还要查权限、挂载、noexec、格式
127命令或解释器找不到还要查PATH、脚本shebang和基础镜像
137通常是SIGKILL不等于一定Java Heap OOM
139常见SIGSEGV可能涉及JVM、JNI、本地库或硬件
143常见SIGTERM可能是正常stop或平台终止

七、容器创建或启动失败

7.1 镜像不存在或拉取失败

bash
docker pull registry.example.com/order/order-api:1.8.3
docker image inspect registry.example.com/order/order-api:1.8.3

分类判断:

错误方向
manifest unknownTag或仓库路径错误,镜像尚未推送
unauthorized登录、Token、仓库权限或凭据过期
DNS错误宿主机到Registry的DNS链路
TLS证书错误CA、证书域名、系统时间或代理中间证书
timeout网络、代理、防火墙、Registry负载
no matching manifestCPU架构或OS平台不匹配

查看镜像平台:

bash
docker image inspect --format '{{.Os}}/{{.Architecture}} {{json .RepoDigests}}' order-api:1.0

7.2 exec format error

常见原因:

  • arm64 镜像运行在 amd64 主机,且没有正确仿真。
  • 入口脚本缺少合法 shebang。
  • 脚本使用 Windows CRLF,解释器路径变成带 \r 的无效路径。
  • 把非可执行文件当程序运行。

检查镜像架构、Entrypoint、脚本首行和文件格式。不要把它简单归因于“Docker坏了”。

7.3 permission denied

可能发生在:

  • Entrypoint 没有执行位。
  • 非 root 用户无权读取 Jar 或配置。
  • Bind Mount 覆盖了镜像内原本有权限的文件。
  • 宿主机目录 UID/GID 不匹配。
  • SELinux 标签拒绝。
  • 挂载点带 noexec

检查最终用户和挂载:

bash
docker image inspect --format 'user={{.Config.User}} entry={{json .Config.Entrypoint}}' order-api:1.0
docker inspect --format '{{json .Mounts}}' order-api

不要把永久切换为 root 当作修复。应修正 Dockerfile COPY --chown、固定 UID/GID、宿主机目录权限、SELinux 标签或挂载策略。

7.4 port is already allocated

宿主机端口已被其他进程或容器占用:

bash
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose ps

Windows:

powershell
Get-NetTCPConnection -LocalPort 18080 -ErrorAction SilentlyContinue

Linux:

bash
ss -lntp | grep ':18080'

需要判断:

  • 是旧容器未删除。
  • 是另一套 Compose 项目占用。
  • 是宿主机进程占用。
  • 数据库是否根本无需发布到宿主机。

7.5 挂载源不存在或类型错误

常见:

  • 期望挂文件,宿主机路径却不存在,被创建成目录。
  • Windows 路径没有共享给 Docker Desktop。
  • 相对路径基准与预期不同。
  • 文件覆盖目录或目录覆盖文件。
  • 挂载遮住 Entrypoint 或应用文件。

使用:

bash
docker inspect --format '{{json .Mounts}}' order-api
docker compose config

确认最终 Source、Destination、Type、RW 和 Propagation。

八、容器启动后立即退出或反复重启

8.1 先判断主进程为什么退出

bash
docker inspect --format 'path={{json .Path}} args={{json .Args}} state={{json .State}}' order-api
docker logs --timestamps --tail 500 order-api

常见类别:

  • Jar 不存在或路径错误。
  • Java class 文件版本高于运行 JRE。
  • 配置语法错误。
  • 必填环境变量缺失。
  • 数据库连接失败且应用选择启动失败退出。
  • 非 root 用户无权限。
  • 主进程实际是一个立即结束的 Shell 脚本。
  • 后台启动应用后脚本退出,容器随 PID 1 退出。

容器生命周期由主进程决定。如果入口脚本执行:

sh
java -jar /app/app.jar &

脚本可能立即结束,Docker认为容器结束,即使子进程短暂存在。通常应:

sh
exec java -jar /app/app.jar

8.2 配置缺失还是依赖未就绪

配置缺失常在每次启动的同一位置稳定失败;依赖未就绪可能随时间、网络和依赖启动顺序变化。

检查:

bash
docker inspect --format '{{json .Config.Env}}' order-api
docker compose config
docker compose logs --tail 200 mysql
docker compose logs --tail 200 redis

环境变量输出可能含密码,查看和分享时脱敏。

8.3 重启策略会掩盖首次错误

restart: always 会让容器不断重启,后续日志可能淹没第一次失败信息。事故前应配置日志轮转和集中采集;事故中应保存全量时间戳日志,再决定是否临时停止重启。

九、Running但外部访问不了:从外到内排查

不要一上来进入容器。按请求经过的路径逐层验证。

mermaid
flowchart TD
    A["客户端访问宿主机端口"] --> B["DNS和路由到宿主机"]
    B --> C["宿主机防火墙或云安全组"]
    C --> D["Docker端口发布规则"]
    D --> E["容器IP与容器端口"]
    E --> F["应用监听地址和端口"]
    F --> G["应用路由、认证和业务处理"]

9.1 确认用户访问的地址

记录:

  • 域名解析到哪个 IP。
  • 用户访问 HTTP 还是 HTTPS。
  • 宿主机端口是什么。
  • 是否经过 Nginx、云负载均衡或防火墙。
  • 问题是 Connection refused、Timeout、TLS 错误还是 HTTP 4xx/5xx。

这四类错误不在同一层:

现象常见层次
DNS解析失败域名和DNS
Connection refused目标可达但没有监听或被明确拒绝
Timeout路由、防火墙、丢包、服务卡住
TLS错误证书、协议、SNI、系统时间
HTTP 404/401/500请求已到HTTP应用层

9.2 检查发布端口

bash
docker port order-api
docker inspect --format '{{json .NetworkSettings.Ports}}' order-api
docker inspect --format '{{json .HostConfig.PortBindings}}' order-api

需要区分:

  • Dockerfile EXPOSE 8080 只是元数据。
  • -p 18080:8080 才发布宿主机端口。
  • 127.0.0.1:18080:8080 只绑定宿主机回环地址。
  • 容器端口必须与应用真实监听端口一致。

9.3 宿主机本地验证

bash
curl -v --max-time 5 http://127.0.0.1:18080/actuator/health

如果宿主机本地成功、远程失败,重点检查宿主机防火墙、云安全组、负载均衡和入口路由,而不是容器内 Spring Controller。

9.4 容器内监听验证

如果镜像有工具:

bash
docker exec order-api sh -c 'ss -lntp || netstat -lntp'

检查应用是监听:

text
0.0.0.0:8080

还是:

text
127.0.0.1:8080

只监听容器 127.0.0.1 时,从容器 eth0/端口发布路径访问可能失败。容器服务通常监听 0.0.0.0,安全边界由网络、端口发布、防火墙和认证共同控制。

9.5 极简镜像没有排障工具怎么办

没有 shcurlss 不代表容器坏了。可选择:

  • 使用 docker top 从宿主机看进程。
  • 使用同网络的受控诊断容器。
  • 进入容器 Network Namespace 抓包或检查 socket。
  • 使用应用已有 Actuator、JMX、JFR、APM。
  • 构建独立 debug 镜像,而不是向生产容器临时安装大量工具。

十、容器间或依赖连接失败:DNS、TCP、TLS、协议四步法

假设 order-api 连接 mysql:3306

10.1 第一步:确认配置目标

bash
docker inspect --format '{{json .Config.Env}}' order-api
docker compose config

最常见错误是写成:

text
jdbc:mysql://localhost:3306/order_db

在 order-api 容器内,localhost 是 order-api 自己,不是 MySQL 容器。Compose 中通常使用:

text
jdbc:mysql://mysql:3306/order_db

10.2 第二步:确认共同网络和DNS

bash
docker inspect --format '{{json .NetworkSettings.Networks}}' order-api
docker inspect --format '{{json .NetworkSettings.Networks}}' mysql
docker network inspect <网络>

有工具时:

bash
docker exec order-api getent hosts mysql

DNS 失败可能是:

  • 两个容器不在同一网络。
  • 服务名写错。
  • 网络别名只存在于另一个网络。
  • 容器使用错误 DNS 配置。
  • 容器重建过程中客户端长期缓存旧 IP。

10.3 第三步:验证TCP

bash
docker exec order-api nc -vz -w 3 mysql 3306

若镜像没有 nc,使用受控诊断容器或从 Network Namespace 验证。

判读:

  • refused:目标 IP 可达,但端口无监听或主动拒绝。
  • timeout:路由、防火墙、NetworkPolicy类控制、丢包或目标卡住。
  • success:TCP 已建立,继续查 TLS、认证和协议。

10.4 第四步:验证应用协议

TCP 成功不代表数据库登录成功。继续查:

  • MySQL 用户名、密码和 host 权限。
  • 数据库是否存在。
  • TLS 模式和证书。
  • 字符集、时区和驱动参数。
  • 连接池超时和旧连接。

网络层成功后出现 Access denied,应查数据库认证,不应继续修改 Docker bridge。

十一、配置修改后不生效

11.1 先分清配置来自哪里

可能来源:

  • 镜像内 application.yml。
  • Dockerfile ENV
  • Compose .env 插值。
  • service env_file
  • service environment
  • Bind Mount 配置文件。
  • Spring Boot 命令行参数。
  • 配置中心。

检查最终模型与容器:

bash
docker compose config
docker inspect --format '{{json .Config.Env}}' order-api
docker inspect --format '{{json .Mounts}}' order-api
docker inspect --format 'path={{json .Path}} args={{json .Args}}' order-api

11.2 restart为什么不一定生效

docker compose restart 重启现有容器,不会必然按新环境变量、端口、Volume 重新创建。修改 Compose 配置后通常执行:

bash
docker compose config
docker compose up -d order-api

然后 inspect 验证新容器实际配置。

11.3 挂载覆盖

镜像内 /app/config 原来有文件,Bind Mount 一个空宿主机目录到同一路径后,容器看到空目录。文件没有被删除,而是被 Mount 遮住。

bash
docker inspect --format '{{json .Mounts}}' order-api

确认 Source 中是否真的有预期文件、目标是否挂错、权限是否允许应用读取。

11.4 Spring Boot配置优先级

即使环境变量进入容器,也要确认变量名能映射到正确 Spring 属性,且没有被命令行参数、外部配置文件或配置中心更高优先级覆盖。Docker 只负责把值放入进程环境,不负责保证框架使用它。

十二、Unhealthy但容器仍在Running

查看健康历史:

bash
docker inspect --format '{{json .State.Health}}' order-api
docker inspect --format '{{json .Config.Healthcheck}}' order-api

逐项确认:

  1. 检查命令使用的 curlwget 是否存在。
  2. endpoint、端口和协议是否正确。
  3. start_period 是否覆盖应用启动时间。
  4. timeout 是否过短。
  5. 健康接口是否因为非核心依赖抖动失败。
  6. 应用是单纯探针失败,还是业务也已经失败。

手工在同一环境执行健康命令:

bash
docker exec order-api sh -c '<健康检查中的实际命令>'

若镜像无 Shell,应按 Healthcheck 的 exec 数组直接执行对应程序,或使用外部探针验证。

健康检查失败不会自动说明 Docker 网络有问题。也可能是应用线程池耗尽、Full GC、认证要求改变或探针工具本身不存在。

十三、CPU持续过高

13.1 先判断容器CPU还是宿主机整体CPU

bash
docker stats --no-stream order-api
docker top order-api

同时看宿主机:

bash
uptime
top

容器 CPU 百分比可能按多核累计,数值超过 100% 不一定异常;必须结合 CPU 限额、核数、历史基线和请求量。

13.2 看是否被CPU限流

CPU 使用率不高但接口变慢,可能是 cgroup quota 太低导致 throttling。查看容器限制:

bash
docker inspect --format 'nanoCpus={{.HostConfig.NanoCpus}} cpuQuota={{.HostConfig.CpuQuota}} cpuPeriod={{.HostConfig.CpuPeriod}} cpuset={{.HostConfig.CpusetCpus}}' order-api

再读取对应 cgroup 统计或监控中的 throttled periods。cgroup v1/v2 路径不同,应先确认主机 cgroup 模式。

13.3 Java容器进一步定位

建立宿主机线程、容器 PID 与 Java 线程之间的关系。可用:

bash
docker top order-api
docker exec order-api jcmd 1 Thread.print

如果 JDK 镜像没有 jcmd,可使用匹配版本的诊断方式、Arthas、JFR 或宿主机 namespace 工具。不要把不匹配版本的 jstack 随意附着到生产 JVM。

常见 CPU 原因:

  • 死循环或热循环。
  • JSON/正则表达式计算。
  • 大量序列化和压缩。
  • GC 频繁。
  • 锁竞争和自旋。
  • 流量突增。
  • 重试风暴。
  • TLS 加解密。

需要把 CPU 曲线、QPS、错误率、GC、线程 Dump 和发布事件对齐,而不是看到 Java 进程高就直接扩容。

十四、内存增长、OOM与Exit 137

14.1 先确认是哪一级内存问题

mermaid
flowchart TD
    A["容器退出或内存告警"] --> B["检查State.OOMKilled与ExitCode"]
    B --> C["检查容器memory limit和历史working set"]
    C --> D["检查宿主机内核OOM日志"]
    D --> E{"是否Java进程"}
    E -- "是" --> F["区分Heap、Metaspace、Direct、线程栈和native"]
    E -- "否" --> G["按对应运行时分析"]

第一轮:

bash
docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}' order-api
docker stats --no-stream order-api
docker events --since 1h --filter container=order-api

Linux 宿主机:

bash
journalctl -k --since "1 hour ago" | grep -Ei 'oom|out of memory|killed process'

14.2 Exit 137为什么不等于OOM

137 通常表示进程收到 SIGKILL,来源可能包括:

  • cgroup 内存超限触发 OOM Killer。
  • 宿主机整体内存压力。
  • 人工 docker kill
  • 编排或脚本主动 kill。
  • docker stop 超时后强杀。

所以必须联合 OOMKilled、events、内核日志和操作记录。

14.3 Java Heap OOM与容器OOM区别

Java Heap OOM 通常会在应用日志中出现:

text
java.lang.OutOfMemoryError: Java heap space

如果 JVM 有机会处理,还可能生成 Heap Dump。容器总内存 OOM 可能由:

  • Heap。
  • Metaspace。
  • Direct Buffer。
  • 线程栈。
  • Code Cache。
  • GC/JIT native memory。
  • JNI、本地库。
  • 子进程。

共同触发。Linux 可能直接杀 JVM,应用没有机会打印 Java OOM。

14.4 JDK 8容器感知

不同 JDK 8 更新版本和发行版对 cgroup 的支持不同。老版本可能按宿主机资源估算 Heap 或并行线程数。排查必须记录完整版本:

bash
docker exec order-api java -version
docker exec order-api java -XX:+PrintFlagsFinal -version

不能只写“项目使用JDK 8”。

14.5 现场还在时先取什么

按风险和现场承载能力选择:

  • GC 日志。
  • jcmd 1 VM.flags
  • jcmd 1 GC.heap_info
  • jcmd 1 VM.native_memory summary,前提是启动时开启 NMT。
  • 线程 Dump。
  • Class Histogram。
  • Heap Dump。
  • JFR 或 Arthas 观测。

Heap Dump 可能很大,会产生 STW、IO 和磁盘压力。先确认磁盘空间、业务影响和安全存储位置。完整 Java OOM 流程见 JVM OOM生产级排查

十五、PIDs或线程耗尽

容器 PIDS 不只是 Java 进程数量,Linux 线程也通常计入任务数量。

bash
docker stats --no-stream order-api
docker inspect --format 'pidsLimit={{.HostConfig.PidsLimit}}' order-api
docker top order-api

典型现象:

  • unable to create new native thread
  • 无法 fork/exec。
  • 健康检查进程无法创建。
  • 应用仍在但无法处理新请求。

原因可能是:

  • 线程池无界创建线程。
  • 每请求创建线程。
  • 线程泄漏。
  • pids_limit 过低。
  • 宿主机 PID 资源耗尽。
  • 每线程栈过大,内存先耗尽。

需要结合线程数量趋势、线程 Dump、线程池配置、-Xss 和容器内存分析,不能只调大 PIDsLimit。

十六、磁盘占满和Docker空间异常

16.1 先区分宿主机文件系统和Docker对象

bash
df -h
df -i
docker info --format '{{.DockerRootDir}}'
docker system df -v

df -i 用来检查 inode。大量小文件可能在容量尚未用完时耗尽 inode,表现为无法创建文件。

16.2 常见占用来源

来源特点检查方向
容器json日志持续增长logging driver、日志轮转
容器可写层应用把文件写进rootfsdocker ps --size、Mounts
Volume数据库、上传、缓存volume inspect、业务目录
镜像层多版本镜像未治理docker system df -v
Build CacheCI/本机构建缓存builder使用和保留策略
Core/Heap Dump单文件巨大Dump目录和事故记录
deleted-open文件文件删了但进程仍持有FDlsof +L1

16.3 日志驱动和日志轮转

检查:

bash
docker inspect --format '{{json .HostConfig.LogConfig}}' order-api
docker inspect --format '{{.LogPath}}' order-api

应用输出 stdout/stderr 后,由日志驱动存储。json-file 若不限制大小,可能写满 Docker Root Dir。Compose 示例:

yaml
logging:
  driver: json-file
  options:
    max-size: "20m"
    max-file: "5"

16.4 为什么删日志后空间可能没有释放

Linux 进程如果仍持有已删除文件的文件描述符,目录项消失但磁盘块要等 FD 关闭才释放:

bash
lsof +L1

不能直接在 Docker 管理目录中随意删除底层文件,可能破坏 Docker 元数据。应先确认文件归属、日志驱动和进程,再通过受支持的轮转或服务流程处理。

16.5 清理命令为什么危险

docker system prunedocker volume prune 等会改变全局对象,可能删除仍有价值的缓存、停止容器或未被当前容器引用的业务卷。生产执行前必须:

  1. 先用 docker system df -v 明确占用来源。
  2. 列出目标对象和所属业务。
  3. 确认备份和恢复。
  4. 选择精确删除,而不是盲目全局清理。
  5. 记录操作审计。

十七、磁盘IO高或服务突然变慢

docker stats 的 BLOCK I/O 是累计量,不能只凭总数判断当前 IO 饱和。还需看宿主机:

bash
iostat -xz 1
pidstat -d 1

常见原因:

  • 数据库高写入。
  • Redis AOF 重写。
  • 大量同步日志。
  • Heap Dump、Core Dump。
  • OverlayFS 上频繁写大文件。
  • Docker Desktop 跨宿主机文件系统 Bind Mount 性能差。
  • 宿主机磁盘本身延迟高。

检查写入应该进入 Volume、Bind Mount 还是容器可写层。数据库和高频持久写不应长期依赖 OverlayFS 可写层。

十八、数据或配置文件“消失”、只读或权限异常

18.1 看最终Mounts,而不是只看启动命令

bash
docker inspect --format '{{json .Mounts}}' order-api
docker compose config

回答:

  • Type 是 volume、bind 还是 tmpfs。
  • Source 在哪里。
  • Destination 是否正确。
  • RW 是 true 还是 false。
  • 是否挂到了新的项目卷。
  • 是否有多个挂载路径重叠。

18.2 UID/GID

非 root 容器写入宿主机目录时,内核比较的是数字 UID/GID,不是用户名字符串。容器中的 app 和宿主机中的 app 名字相同,ID 也可能不同。

有 Shell 时:

bash
docker exec order-api sh -c 'id; ls -ln /app /app/data'

宿主机也查看数字 ID,再修正目录所有权、组权限或部署 UID,不要使用全局 chmod 777

18.3 只读文件系统

检查:

bash
docker inspect --format 'readonlyRootfs={{.HostConfig.ReadonlyRootfs}} mounts={{json .Mounts}}' order-api

若开启只读 rootfs,应用必须把临时目录、日志、上传和 Dump 明确写到可写挂载。安全加固不是只加一个 read_only: true,还要梳理应用写路径。

18.4 数据库版本与数据目录

更换 MySQL/PostgreSQL 镜像版本后直接挂旧数据目录,可能出现数据格式不兼容。不能通过删除卷“修好启动”;应查升级路径、数据库日志、备份和恢复方案。

十九、Docker Compose专项排查

第一轮:

bash
docker compose config
docker compose ps -a
docker compose images
docker compose top
docker compose logs --timestamps --tail 300

检查:

  • 当前目录和 Compose 文件是否正确。
  • 项目名是否变化。
  • Profile 是否启用。
  • 多个 -f 文件是否按预期合并。
  • 环境变量插值是否正确。
  • service 的容器是否被重建。
  • 是否出现 orphan 容器。
  • Volume 是否因项目名变化而新建。

19.1 修改配置后只restart

restart 不会按新模型重建容器。应:

bash
docker compose config
docker compose up -d <service>
docker inspect <新容>

19.2 depends_on误区

短语法只保证启动顺序,不保证数据库已经可以连接。使用 healthcheck 和 service_healthy 可以改善初始顺序,但应用仍必须具备运行期重试和恢复能力。

19.3 项目名导致两套资源

bash
docker compose ls
docker ps --filter label=com.docker.compose.project=<项目>
docker volume ls
docker network ls

从资源标签确认归属,不要只凭名称猜测。

二十、Docker daemon或宿主机异常

如果同一主机多个无关容器同时异常,优先考虑共享故障域:

  • 宿主机 CPU、内存、磁盘、inode。
  • Docker daemon。
  • DNS、网卡、iptables/nftables。
  • Docker Root Dir。
  • 内核升级或重启。
  • 云主机迁移、磁盘故障和网络事件。

Linux:

bash
uptime
free -h
df -h
df -i
systemctl status docker --no-pager
journalctl -u docker --since "1 hour ago"
journalctl -k --since "1 hour ago"

不要在未保存状态时随意重启 Docker daemon,因为它可能影响主机上所有容器,并改变事件与运行状态。需要先确认业务影响、重启策略和恢复顺序。

二十一、商业案例一:订单服务发布后访问不了

现象:发布新镜像后,Nginx 返回 502,容器显示 Running。

21.1 错误做法

text
看到502

重启Nginx

重启order-api

扩大内存

没有任何证据说明 502 与 Nginx 进程、容器内存有关。

21.2 正确过程

  1. 从 Nginx error log 确认 upstream 连接的是哪个地址和端口。
  2. docker ps -a 确认 order-api 状态和端口映射。
  3. docker inspect 确认新容器实际镜像 ID、Entrypoint、端口和网络。
  4. 宿主机请求发布端口。
  5. 检查容器内应用监听地址。
  6. 对比新旧版本配置。

取证后发现:Spring Boot 新配置为:

properties
server.address=127.0.0.1
server.port=8080

应用只监听容器 loopback,Docker 端口发布流量到容器 eth0 地址时无法连接。

修复:删除错误绑定或监听 0.0.0.0,重新构建/配置并发布。长期措施:在流水线部署后从真实入口执行健康验证,而不只在容器内 curl localhost。

二十二、商业案例二:扩容后Java容器仍然Exit 137

现象:容器限制从 1 GiB 扩到 2 GiB 后短暂恢复,数小时后仍退出 137。

22.1 取证

bash
docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}' order-api
docker events --since 6h --filter container=order-api
journalctl -k --since "6 hours ago" | grep -Ei 'oom|killed process'

确认 OOMKilled=true,监控显示 RSS 持续增长,但 Java Heap 使用稳定。

进一步检查:

  • Direct Buffer 指标。
  • 线程数量。
  • NMT。
  • Netty allocator 指标。
  • Native Memory 和 /proc/<pid>/smaps

发现请求超时分支未释放 Direct Buffer。扩大内存只是延长耗尽时间。

长期修复:修复引用释放、增加 Direct Memory 与线程监控、设置合理上限和压测回归。不能把“扩到4 GiB”作为根因修复。

二十三、商业案例三:MySQL数据看起来消失

现象:执行 Compose 后,MySQL 变成了空库。

排查:

bash
docker compose ls
docker compose config
docker volume ls
docker inspect --format '{{json .Mounts}}' <mysql容>

发现部署脚本把项目名从 order-prod 改成 order-platform,新容器挂载 order-platform_mysql-data,旧数据仍在 order-prod_mysql-data

处理前先停止写入,确认旧卷身份、数据库一致性和备份,再按变更流程重新挂载。不能随意把两个数据目录合并,也不能在运行中直接复制数据库文件。

长期措施:生产使用显式 external volume 名称、变更前检查最终 Compose 模型,并建立数据库逻辑/物理备份与恢复演练。

二十四、商业案例四:磁盘清理后仍然100%

现象:删除了一个超大应用日志,df -h 仍显示磁盘满。

排查:

bash
lsof +L1

发现 Java 进程仍持有已删除日志 FD。目录项已删除,但磁盘块不会在 FD 关闭前释放。

处理:评估后让日志框架重新打开文件或优雅重启对应实例,而不是重启整台主机。长期改为 stdout/stderr 集中采集和日志轮转,并对 Docker Root Dir、Volume、inode 设置告警。

二十五、根因报告不能写“重启后恢复”

一份合格复盘至少包括:

项目要回答的问题
影响哪些用户、接口、数据和时间段受影响
时间线告警、发布、故障、止损、恢复分别何时发生
直接原因哪个技术条件直接导致失败
根因为什么系统允许该条件出现
触发因素发布、流量、数据、依赖还是主机事件
扩大因素为什么没有快速自愈或隔离
发现问题为什么监控没有更早发现
恢复动作做了什么,为什么有效
长期修复代码、配置、容量、流程和监控怎么改
验证如何证明修复有效且不会再次发生

例如:

text
错误结论:容器内存不足,重启后恢复。

合格结论:order-api在文件上传失败分支未关闭Direct Buffer,
导致容器RSS以每小时约180MiB增长;当总内存超过2GiB cgroup限制时,
内核OOM Killer终止JVM,State.OOMKilled=true并产生Exit 137。
重启只清空进程内存,不能消除泄漏。已修复释放逻辑、增加失败分支测试、
Direct Memory告警和6小时稳定性压测。

二十六、面试标准回答

Docker容器访问不了怎么排查

先确认影响范围和入口错误类型,再用docker ps -a判断容器状态,用inspect确认镜像、State、端口、网络、挂载和资源。若容器未运行,结合ExitCode、OOMKilled、logs和events查启动失败;若Running,从宿主机端口、Docker发布规则、容器IP、应用监听地址逐层验证。容器间调用再按服务名DNS、共同网络、TCP、TLS和应用协议排查,最后结合防火墙、下游依赖和业务指标,避免一开始就重启破坏现场。

容器反复Restarting怎么排查

保存RestartCount、每次StartedAt/FinishedAt、ExitCode、OOMKilled、重启策略、带时间戳日志和events,确认主进程为什么退出。常见是Entrypoint、配置、权限、依赖、OOM或脚本后台启动后PID 1退出。重启策略只是重复启动,不能修复根因;必要时在保留证据和评估业务后暂停重启,避免日志覆盖和依赖冲击。

Exit 137一定是OOM吗

不一定。137通常表示SIGKILL,可能是cgroup OOM、宿主机OOM、人工docker kill或stop超时强杀。应结合State.OOMKilled、Docker events、宿主机内核日志、资源监控和操作记录判断。即使确认容器OOM,也要区分Java Heap、Direct Memory、Metaspace、线程栈、native memory和子进程。

Running为什么不等于服务正常

Running只表示容器主进程存在。应用可能仍在启动、只监听127.0.0.1、Full GC、线程池耗尽、健康检查失败或下游不可用。还要检查Health、监听端口、真实入口请求、错误率、P99和业务成功率。

Docker磁盘满怎么排查

先用df -hdf -i区分容量与inode,再确认Docker Root Dir,并用docker system df -v区分镜像、容器可写层、Volume和构建缓存;检查容器日志驱动、LogPath、Dump和deleted-open文件。不能直接全局prune,应先确认对象归属、备份和恢复,再精确处理并补日志轮转和容量告警。

配置修改后为什么restart不生效

restart只重启已有容器,不会必然根据新Compose环境、端口或挂载重建。先用docker compose config确认插值和合并后的模型,再docker compose up -d service触发必要重建,最后用inspect核对实际Env、Mounts、Entrypoint和端口;还要检查应用框架配置优先级。

二十七、学习验收

学完后,应能在实验环境完成:

  1. 制造一个错误 Entrypoint,依据 State.Error 和退出信息定位。
  2. 制造端口映射错误,按宿主机到应用监听路径定位。
  3. 在 Compose 中错误使用 localhost,再改为服务名并解释 DNS 原理。
  4. 修改环境变量只 restart,证明其不生效,再用 up 重建并 inspect。
  5. 设置过低内存限制,区分 Java 日志与 State.OOMKilled。
  6. 制造日志增长,使用 Docker LogConfig 和磁盘指标定位。
  7. 挂载空目录遮住镜像配置,使用 Mounts 解释文件“消失”。
  8. 改变 Compose 项目名,识别两套 Volume 并安全恢复。
  9. 对运行中和已退出容器分别列出可获得与已丢失的证据。
  10. 写一份包含时间线、直接原因、根因、扩大因素和验证方法的复盘。

关联知识点