Docker 生产故障排查 Runbook:从第一条命令到根因
“服务访问不了”不是根因,它只是用户看到的结果。容器可能根本没有创建、创建后无法启动、启动后立即退出、正在反复重启、进程还在但应用不健康、端口映射错误、Docker DNS 失败、挂载覆盖了配置、cgroup 内存超限,甚至 Docker daemon 或宿主机本身已经异常。
本页不是命令速查,而是一份可以按顺序执行的生产 Runbook。每一步都说明:为什么查、从哪里取证、输出怎么看、下一步走哪条分支,以及哪些操作会破坏现场。
学习目标
学完后,应能:
- 在五分钟内判断故障属于宿主机、Docker daemon、镜像、容器生命周期、应用、网络、存储还是下游依赖。
- 根据“运行中、已退出/重启中、已删除/宿主机失联”选择不同证据源。
- 正确解释 Running、Healthy、Restarting、ExitCode、OOMKilled 和容器 PID。
- 从外到内排查端口访问失败,从 DNS 到 TCP 再到应用协议定位依赖连接失败。
- 区分 Java Heap OOM、容器总内存 OOM、宿主机 OOM、主动 SIGKILL 和停止超时。
- 定位 CPU 高、内存增长、线程/PID耗尽、磁盘打满、日志暴涨和 IO 慢。
- 在不泄露密码、Token、证书和业务数据的前提下保存事故证据。
- 明确什么时候可以重启止损,什么时候必须先 Dump、抓包或保存日志。
- 写出根因、触发条件、扩大因素和长期修复,而不是把“重启恢复”当作结论。
一、排障前先建立三个原则
1.1 先止损,但止损不等于先重启
生产故障首先判断业务影响:
- 是否正在影响支付、订单、采集、库存等核心链路。
- 是否只有一个实例异常,其他实例能否承接流量。
- 是否还能灰度摘除异常实例。
- 是否有数据持续写错、重复消费或丢失风险。
如果存在多个实例,可以先从负载均衡摘除故障实例,再保留它取证。如果只有一个实例且业务完全中断,可能必须先恢复服务,但至少应尽快保存低成本证据:状态、inspect、日志、events、资源快照和镜像身份。
flowchart TD
A["收到Docker服务故障告警"] --> B["确认业务影响和故障范围"]
B --> C{"是否有其他健康实例承接"}
C -- "有" --> D["摘流故障实例并保留现场"]
C -- "没有" --> E{"是否持续产生错误数据"}
E -- "是" --> F["停止写入或隔离流量"]
E -- "否" --> G["保存最小证据后恢复服务"]
D --> H["进入根因排查"]
F --> H
G --> H1.2 先保存事实,再提出假设
错误做法:
看到Exit 137
↓
直接认定Java堆泄漏
↓
增大Xmx并重启正确做法:
看到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问题”
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、Restarting | ps -a、State、events、logs |
| 网络 | DNS失败、refused、timeout | network inspect、端口、监听、抓包 |
| 存储 | Permission denied、数据消失、只读文件系统 | Mounts、UID/GID、磁盘、内核日志 |
| 应用/JVM | OOM、Full GC、线程池耗尽 | 应用指标、GC日志、Dump、Arthas/JFR |
| 下游依赖 | DB、Redis、MQ不可用 | 依赖监控、连接日志、网络和账号权限 |
四、前五分钟最小取证清单
假设容器名为 order-api。
4.1 记录时间、主机和Docker上下文
Get-Date -Format o
hostname
docker context show
docker version
docker info为什么先看 context:Docker CLI 可能连接本机、远程 daemon、Docker Desktop 或其他 context。查错环境会出现“明明容器不存在”或误操作其他主机。
Linux:
date --iso-8601=seconds
hostname
docker context show
docker version
docker info4.2 保存容器列表和完整状态
docker ps -a --no-trunc --filter "name=order-api"
docker inspect order-api快速提取 State:
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-api4.3 保存日志与事件时间线
docker logs --timestamps --since 30m order-api
docker events --since 30m --filter container=order-apidocker events 是持续流。生产取证可以指定 --until,或由监控系统长期采集。不要让交互命令无限占用终端而漏掉其他检查。
4.4 保存镜像、命令、端口、网络、挂载和资源限制
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-api4.5 容器运行时保存资源和进程快照
docker stats --no-stream order-api
docker top order-api
docker port order-api这只是当前快照,不等于历史趋势。还要查看告警前后的监控曲线。
4.6 脱敏要求
完整 inspect 可能包含:
- 数据库密码。
- Token。
- 云访问密钥。
- 内部域名和 IP。
- 标签中的业务数据。
- 挂载的宿主机敏感路径。
原始证据应进入受控事故目录,分享给无关人员前脱敏。不要直接把 docker inspect 全量输出粘贴到公开群、外部工单或公共仓库。
五、第一棵决策树:Docker命令能否正常工作
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:
docker context ls
docker context showLinux 使用 systemd 时:
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 数据位置。
六、第二棵决策树:容器处于什么状态
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 create或docker compose create。 - 启动阶段遇到挂载、设备、端口或 runtime 错误。
- Docker daemon 尚未真正启动它。
检查:
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
说明主进程反复退出,重启策略又将其启动。保存:
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
重点看:
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 镜像不存在或拉取失败
docker pull registry.example.com/order/order-api:1.8.3
docker image inspect registry.example.com/order/order-api:1.8.3分类判断:
| 错误 | 方向 |
|---|---|
manifest unknown | Tag或仓库路径错误,镜像尚未推送 |
unauthorized | 登录、Token、仓库权限或凭据过期 |
| DNS错误 | 宿主机到Registry的DNS链路 |
| TLS证书错误 | CA、证书域名、系统时间或代理中间证书 |
| timeout | 网络、代理、防火墙、Registry负载 |
| no matching manifest | CPU架构或OS平台不匹配 |
查看镜像平台:
docker image inspect --format '{{.Os}}/{{.Architecture}} {{json .RepoDigests}}' order-api:1.07.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。
检查最终用户和挂载:
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
宿主机端口已被其他进程或容器占用:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose psWindows:
Get-NetTCPConnection -LocalPort 18080 -ErrorAction SilentlyContinueLinux:
ss -lntp | grep ':18080'需要判断:
- 是旧容器未删除。
- 是另一套 Compose 项目占用。
- 是宿主机进程占用。
- 数据库是否根本无需发布到宿主机。
7.5 挂载源不存在或类型错误
常见:
- 期望挂文件,宿主机路径却不存在,被创建成目录。
- Windows 路径没有共享给 Docker Desktop。
- 相对路径基准与预期不同。
- 文件覆盖目录或目录覆盖文件。
- 挂载遮住 Entrypoint 或应用文件。
使用:
docker inspect --format '{{json .Mounts}}' order-api
docker compose config确认最终 Source、Destination、Type、RW 和 Propagation。
八、容器启动后立即退出或反复重启
8.1 先判断主进程为什么退出
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 退出。
容器生命周期由主进程决定。如果入口脚本执行:
java -jar /app/app.jar &脚本可能立即结束,Docker认为容器结束,即使子进程短暂存在。通常应:
exec java -jar /app/app.jar8.2 配置缺失还是依赖未就绪
配置缺失常在每次启动的同一位置稳定失败;依赖未就绪可能随时间、网络和依赖启动顺序变化。
检查:
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但外部访问不了:从外到内排查
不要一上来进入容器。按请求经过的路径逐层验证。
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 检查发布端口
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 宿主机本地验证
curl -v --max-time 5 http://127.0.0.1:18080/actuator/health如果宿主机本地成功、远程失败,重点检查宿主机防火墙、云安全组、负载均衡和入口路由,而不是容器内 Spring Controller。
9.4 容器内监听验证
如果镜像有工具:
docker exec order-api sh -c 'ss -lntp || netstat -lntp'检查应用是监听:
0.0.0.0:8080还是:
127.0.0.1:8080只监听容器 127.0.0.1 时,从容器 eth0/端口发布路径访问可能失败。容器服务通常监听 0.0.0.0,安全边界由网络、端口发布、防火墙和认证共同控制。
9.5 极简镜像没有排障工具怎么办
没有 sh、curl、ss 不代表容器坏了。可选择:
- 使用
docker top从宿主机看进程。 - 使用同网络的受控诊断容器。
- 进入容器 Network Namespace 抓包或检查 socket。
- 使用应用已有 Actuator、JMX、JFR、APM。
- 构建独立 debug 镜像,而不是向生产容器临时安装大量工具。
十、容器间或依赖连接失败:DNS、TCP、TLS、协议四步法
假设 order-api 连接 mysql:3306。
10.1 第一步:确认配置目标
docker inspect --format '{{json .Config.Env}}' order-api
docker compose config最常见错误是写成:
jdbc:mysql://localhost:3306/order_db在 order-api 容器内,localhost 是 order-api 自己,不是 MySQL 容器。Compose 中通常使用:
jdbc:mysql://mysql:3306/order_db10.2 第二步:确认共同网络和DNS
docker inspect --format '{{json .NetworkSettings.Networks}}' order-api
docker inspect --format '{{json .NetworkSettings.Networks}}' mysql
docker network inspect <网络名>有工具时:
docker exec order-api getent hosts mysqlDNS 失败可能是:
- 两个容器不在同一网络。
- 服务名写错。
- 网络别名只存在于另一个网络。
- 容器使用错误 DNS 配置。
- 容器重建过程中客户端长期缓存旧 IP。
10.3 第三步:验证TCP
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 命令行参数。
- 配置中心。
检查最终模型与容器:
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-api11.2 restart为什么不一定生效
docker compose restart 重启现有容器,不会必然按新环境变量、端口、Volume 重新创建。修改 Compose 配置后通常执行:
docker compose config
docker compose up -d order-api然后 inspect 验证新容器实际配置。
11.3 挂载覆盖
镜像内 /app/config 原来有文件,Bind Mount 一个空宿主机目录到同一路径后,容器看到空目录。文件没有被删除,而是被 Mount 遮住。
docker inspect --format '{{json .Mounts}}' order-api确认 Source 中是否真的有预期文件、目标是否挂错、权限是否允许应用读取。
11.4 Spring Boot配置优先级
即使环境变量进入容器,也要确认变量名能映射到正确 Spring 属性,且没有被命令行参数、外部配置文件或配置中心更高优先级覆盖。Docker 只负责把值放入进程环境,不负责保证框架使用它。
十二、Unhealthy但容器仍在Running
查看健康历史:
docker inspect --format '{{json .State.Health}}' order-api
docker inspect --format '{{json .Config.Healthcheck}}' order-api逐项确认:
- 检查命令使用的
curl、wget是否存在。 - endpoint、端口和协议是否正确。
start_period是否覆盖应用启动时间。- timeout 是否过短。
- 健康接口是否因为非核心依赖抖动失败。
- 应用是单纯探针失败,还是业务也已经失败。
手工在同一环境执行健康命令:
docker exec order-api sh -c '<健康检查中的实际命令>'若镜像无 Shell,应按 Healthcheck 的 exec 数组直接执行对应程序,或使用外部探针验证。
健康检查失败不会自动说明 Docker 网络有问题。也可能是应用线程池耗尽、Full GC、认证要求改变或探针工具本身不存在。
十三、CPU持续过高
13.1 先判断容器CPU还是宿主机整体CPU
docker stats --no-stream order-api
docker top order-api同时看宿主机:
uptime
top容器 CPU 百分比可能按多核累计,数值超过 100% 不一定异常;必须结合 CPU 限额、核数、历史基线和请求量。
13.2 看是否被CPU限流
CPU 使用率不高但接口变慢,可能是 cgroup quota 太低导致 throttling。查看容器限制:
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 线程之间的关系。可用:
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 先确认是哪一级内存问题
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["按对应运行时分析"]第一轮:
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-apiLinux 宿主机:
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 通常会在应用日志中出现:
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 或并行线程数。排查必须记录完整版本:
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 线程也通常计入任务数量。
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对象
df -h
df -i
docker info --format '{{.DockerRootDir}}'
docker system df -vdf -i 用来检查 inode。大量小文件可能在容量尚未用完时耗尽 inode,表现为无法创建文件。
16.2 常见占用来源
| 来源 | 特点 | 检查方向 |
|---|---|---|
| 容器json日志 | 持续增长 | logging driver、日志轮转 |
| 容器可写层 | 应用把文件写进rootfs | docker ps --size、Mounts |
| Volume | 数据库、上传、缓存 | volume inspect、业务目录 |
| 镜像层 | 多版本镜像未治理 | docker system df -v |
| Build Cache | CI/本机构建缓存 | builder使用和保留策略 |
| Core/Heap Dump | 单文件巨大 | Dump目录和事故记录 |
| deleted-open文件 | 文件删了但进程仍持有FD | lsof +L1 |
16.3 日志驱动和日志轮转
检查:
docker inspect --format '{{json .HostConfig.LogConfig}}' order-api
docker inspect --format '{{.LogPath}}' order-api应用输出 stdout/stderr 后,由日志驱动存储。json-file 若不限制大小,可能写满 Docker Root Dir。Compose 示例:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"16.4 为什么删日志后空间可能没有释放
Linux 进程如果仍持有已删除文件的文件描述符,目录项消失但磁盘块要等 FD 关闭才释放:
lsof +L1不能直接在 Docker 管理目录中随意删除底层文件,可能破坏 Docker 元数据。应先确认文件归属、日志驱动和进程,再通过受支持的轮转或服务流程处理。
16.5 清理命令为什么危险
docker system prune、docker volume prune 等会改变全局对象,可能删除仍有价值的缓存、停止容器或未被当前容器引用的业务卷。生产执行前必须:
- 先用
docker system df -v明确占用来源。 - 列出目标对象和所属业务。
- 确认备份和恢复。
- 选择精确删除,而不是盲目全局清理。
- 记录操作审计。
十七、磁盘IO高或服务突然变慢
docker stats 的 BLOCK I/O 是累计量,不能只凭总数判断当前 IO 饱和。还需看宿主机:
iostat -xz 1
pidstat -d 1常见原因:
- 数据库高写入。
- Redis AOF 重写。
- 大量同步日志。
- Heap Dump、Core Dump。
- OverlayFS 上频繁写大文件。
- Docker Desktop 跨宿主机文件系统 Bind Mount 性能差。
- 宿主机磁盘本身延迟高。
检查写入应该进入 Volume、Bind Mount 还是容器可写层。数据库和高频持久写不应长期依赖 OverlayFS 可写层。
十八、数据或配置文件“消失”、只读或权限异常
18.1 看最终Mounts,而不是只看启动命令
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 时:
docker exec order-api sh -c 'id; ls -ln /app /app/data'宿主机也查看数字 ID,再修正目录所有权、组权限或部署 UID,不要使用全局 chmod 777。
18.3 只读文件系统
检查:
docker inspect --format 'readonlyRootfs={{.HostConfig.ReadonlyRootfs}} mounts={{json .Mounts}}' order-api若开启只读 rootfs,应用必须把临时目录、日志、上传和 Dump 明确写到可写挂载。安全加固不是只加一个 read_only: true,还要梳理应用写路径。
18.4 数据库版本与数据目录
更换 MySQL/PostgreSQL 镜像版本后直接挂旧数据目录,可能出现数据格式不兼容。不能通过删除卷“修好启动”;应查升级路径、数据库日志、备份和恢复方案。
十九、Docker Compose专项排查
第一轮:
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 不会按新模型重建容器。应:
docker compose config
docker compose up -d <service>
docker inspect <新容器>19.2 depends_on误区
短语法只保证启动顺序,不保证数据库已经可以连接。使用 healthcheck 和 service_healthy 可以改善初始顺序,但应用仍必须具备运行期重试和恢复能力。
19.3 项目名导致两套资源
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:
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 错误做法
看到502
↓
重启Nginx
↓
重启order-api
↓
扩大内存没有任何证据说明 502 与 Nginx 进程、容器内存有关。
21.2 正确过程
- 从 Nginx error log 确认 upstream 连接的是哪个地址和端口。
docker ps -a确认 order-api 状态和端口映射。docker inspect确认新容器实际镜像 ID、Entrypoint、端口和网络。- 宿主机请求发布端口。
- 检查容器内应用监听地址。
- 对比新旧版本配置。
取证后发现:Spring Boot 新配置为:
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 取证
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 变成了空库。
排查:
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 仍显示磁盘满。
排查:
lsof +L1发现 Java 进程仍持有已删除日志 FD。目录项已删除,但磁盘块不会在 FD 关闭前释放。
处理:评估后让日志框架重新打开文件或优雅重启对应实例,而不是重启整台主机。长期改为 stdout/stderr 集中采集和日志轮转,并对 Docker Root Dir、Volume、inode 设置告警。
二十五、根因报告不能写“重启后恢复”
一份合格复盘至少包括:
| 项目 | 要回答的问题 |
|---|---|
| 影响 | 哪些用户、接口、数据和时间段受影响 |
| 时间线 | 告警、发布、故障、止损、恢复分别何时发生 |
| 直接原因 | 哪个技术条件直接导致失败 |
| 根因 | 为什么系统允许该条件出现 |
| 触发因素 | 发布、流量、数据、依赖还是主机事件 |
| 扩大因素 | 为什么没有快速自愈或隔离 |
| 发现问题 | 为什么监控没有更早发现 |
| 恢复动作 | 做了什么,为什么有效 |
| 长期修复 | 代码、配置、容量、流程和监控怎么改 |
| 验证 | 如何证明修复有效且不会再次发生 |
例如:
错误结论:容器内存不足,重启后恢复。
合格结论: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 -h和df -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和端口;还要检查应用框架配置优先级。
二十七、学习验收
学完后,应能在实验环境完成:
- 制造一个错误 Entrypoint,依据 State.Error 和退出信息定位。
- 制造端口映射错误,按宿主机到应用监听路径定位。
- 在 Compose 中错误使用 localhost,再改为服务名并解释 DNS 原理。
- 修改环境变量只 restart,证明其不生效,再用 up 重建并 inspect。
- 设置过低内存限制,区分 Java 日志与 State.OOMKilled。
- 制造日志增长,使用 Docker LogConfig 和磁盘指标定位。
- 挂载空目录遮住镜像配置,使用 Mounts 解释文件“消失”。
- 改变 Compose 项目名,识别两套 Volume 并安全恢复。
- 对运行中和已退出容器分别列出可获得与已丢失的证据。
- 写一份包含时间线、直接原因、根因、扩大因素和验证方法的复盘。
