本文参考《Docker 健康检查的目的、影响和处理》的选题,并根据当前 Docker 官方文档重写。其中一个关键修正是:普通 Docker Engine 将容器标记为 unhealthy 后,不会自动把它移出网络,也不会因此触发 restart policy。

容器的主进程还活着,不代表服务还能工作。线程死锁、连接池耗尽、事件循环卡死,都可能让 PID 1 继续运行,却无法处理请求。

Docker HEALTHCHECK 正是为这种差异准备的:它给正常运行状态之外再加一个 startinghealthyunhealthy 的健康状态。不过它首先是检测和信号机制,不是完整的故障恢复系统。

running、healthy 和 restarted 是三件事

需要先把三个机制分开:

机制判断什么默认会做什么
容器状态PID 1 是否仍在运行进程退出后容器停止
健康检查自定义命令是否连续成功更新 health status,发出 health_status event
restart policy容器是否停止或异常退出按策略重新启动已停止的容器

restart: alwaysrestart: 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、请求错误和反复重启。

参考资料