本文参考《Docker 健康检查的目的、影响和处理》的选题,并根据当前 Docker 官方文档重写。其中一个关键修正是:普通 Docker Engine 将容器标记为
unhealthy后,不会自动把它移出网络,也不会因此触发 restart policy。
容器的主进程还活着,不代表服务还能工作。线程死锁、连接池耗尽、事件循环卡死,都可能让 PID 1 继续运行,却无法处理请求。
Docker HEALTHCHECK 正是为这种差异准备的:它给正常运行状态之外再加一个 starting、healthy 或 unhealthy 的健康状态。不过它首先是检测和信号机制,不是完整的故障恢复系统。
running、healthy 和 restarted 是三件事
需要先把三个机制分开:
| 机制 | 判断什么 | 默认会做什么 |
|---|---|---|
| 容器状态 | PID 1 是否仍在运行 | 进程退出后容器停止 |
| 健康检查 | 自定义命令是否连续成功 | 更新 health status,发出 health_status event |
| restart policy | 容器是否停止或异常退出 | 按策略重新启动已停止的容器 |
restart: always 或 restart: unless-stopped 关注的是“容器停止”,不是“健康状态变为 unhealthy”。如果进程卡死但没有退出,restart policy 不会被触发。
同样,Docker Engine 的普通网络不会因为某个容器 unhealthy 就自动修改 DNS 解析或隔离流量。是否摘除实例、重建任务,要看上层编排器或负载均衡器是否消费这个健康信号。
一个好的健康检查应该检查什么
健康命令应回答一个窄问题:这个容器此刻是否具备继续承担其核心职责的最低条件?
推荐特征:
- 检查本机回环地址,不绕公网域名和外部负载均衡;
- 快速、确定、资源消耗小;
- 有严格 timeout;
- 允许启动预热期;
- 连续失败多次才转为 unhealthy,避免单次抖动;
- 输出简短但可诊断,不包含密钥和个人信息。
不要把所有下游依赖都塞进一个探针。数据库短暂抖动时,如果几十个应用容器同时变 unhealthy 并被重启,可能把局部故障放大成重启风暴。应用应区分:自身已经不可恢复,还是某个下游暂时不可用但可以退避重试。
Dockerfile 中的配置
最可靠的方式是在镜像中提供一个小型检查程序或脚本,避免运行时才发现镜像里没有 curl:
COPY --chmod=755 healthcheck /app/healthcheck
HEALTHCHECK \
--interval=30s \
--timeout=3s \
--start-period=30s \
--retries=3 \
CMD ["/app/healthcheck"]
命令退出码的含义是:
0:检查成功;1:检查失败;2:Docker 保留,不应使用。
start-period 给应用预热时间;期间失败不会计入连续失败次数。如果检查在预热期内成功,Docker 会认为应用已经启动,之后的失败会正常计数。
如果使用 HTTP 探针,一个简单脚本可以只访问本机 readiness 端点:
#!/bin/sh
set -eu
wget -q --spider --timeout=2 \
http://127.0.0.1:8080/health/ready
基础镜像没有 wget 时,应改用镜像内确定存在的工具或应用自带检查命令,而不是为了健康检查临时安装一整套调试工具。
Compose 的 service_healthy 只解决启动顺序
Compose 默认只等待依赖容器进入 running,不等待数据库真正可接受连接。可以用 condition: service_healthy 延迟依赖服务启动:
services:
app:
build: .
restart: unless-stopped
depends_on:
db:
condition: service_healthy
db:
image: postgres:18
restart: unless-stopped
healthcheck:
test:
- CMD-SHELL
- pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"
interval: 10s
timeout: 5s
start_period: 30s
retries: 5
这能让 app 在首次创建时等待 db healthy,但不能代替运行期间的重连、熔断和恢复逻辑。数据库在两小时后重启时,应用仍应自己处理连接失效。
unhealthy 之后应该怎样恢复
不同故障需要不同动作:
应用内部不可恢复
如果应用确认自己进入无法恢复的状态,最清晰的做法通常是记录必要信息后让主进程退出。容器停止后,restart policy 或编排器才能按明确规则重启它。
不要让健康检查脚本直接 kill -9 主进程。探针应负责判断,应用或编排层负责生命周期,职责混在一起会让故障原因和退出路径难以追踪。
下游依赖暂时故障
应用应退避重试、快速失败或降级,而不是立即自杀。如果所有依赖方都因数据库短暂不可用而同时重启,只会增加恢复压力。
需要自动摘流和重建
单机 Compose 不提供完整的副本摘流与重建能力。真正需要多副本自愈时,应让支持健康调度的编排平台负责,并配置最大重试、退避和告警。
外部 autoheal 容器虽能监听 health status 并调用 Docker API 重启容器,但要谨慎使用:挂载 /var/run/docker.sock 通常意味着获得接近宿主机 root 的控制能力,而且无上限重启会掩盖根因。至少应限制作用范围、增加退避与重启上限,并把连续重启升级为告警。
先把诊断信息拿出来
查看当前健康状态和最近几次检查输出:
docker inspect \
--format '{{json .State.Health}}' \
my-container
监听健康状态变化:
docker events \
--filter container=my-container \
--filter event=health_status
Docker 会保存探针的少量标准输出和错误输出,适合放错误码、耗时和失败环节,不适合输出完整响应、token 或连接串。
告警要覆盖“状态”和“反复恢复”
只告警 unhealthy 还不够。一个每隔几分钟就被自动重启、随后短暂恢复的容器,表面可用,实际已经处于故障循环。
建议至少采集:
- 容器健康状态及持续时间;
- 最近检查的退出码与耗时;
- 单位时间内的重启次数;
- 应用请求错误率和延迟;
- 下游连接失败率;
- OOMKilled、退出码和宿主机资源压力。
恢复通知也应聚合,避免 unhealthy/healthy 抖动造成告警风暴。连续重启达到阈值后,应停止盲目自愈并要求人工介入。
上线前检查清单
- 明确区分 running、healthy 与 restart policy;
- 不假设 unhealthy 会被普通 Docker 网络自动摘除;
- 探针只检查最低服务能力,执行快速且有 timeout;
- 设置合理的 start period 和连续失败次数;
- Compose 依赖启动使用
service_healthy,应用仍实现运行期重连; - 不可恢复故障由应用明确退出,暂时依赖故障不会触发重启风暴;
- 外部自愈工具限制 Docker socket 权限、范围、频率和重试上限;
- 告警同时关注 unhealthy、请求错误和反复重启。