目录

Docker-04 重启策略与健康检查:让服务在正确的时候自动恢复

本篇较长,按需跳转: 四种策略 · 按事件比较 · on-failure 判断 · PID 1 · docker restart 命令 · Compose 三种 restart · 策略与健康检查 · HEALTHCHECK 深入 · depends_on 启动顺序 · 信号与优雅退出 · OOM · 如何选择 · 实验 · 排障清单

1. 重启策略:容器退出后到底会发生什么

Docker restart policy 是 daemon 针对容器主进程退出所执行的生命周期策略。它解决的是单机上"进程退出后是否重新启动",不等于健康检查、故障转移、高可用或滚动发布。

创建容器时设置:

docker run -d --name api \
  --restart unless-stopped \
  example/api:1.0.0

也可以更新已有容器,不需要重新创建:

docker update --restart always api
docker inspect api --format '{{.HostConfig.RestartPolicy.Name}} {{.HostConfig.RestartPolicy.MaximumRetryCount}}'

1.1 四种 restart policy

策略 触发条件 daemon 重启后 典型用途
no 从不自动重启,默认值 不启动 一次性任务、由外部系统调度的容器
on-failure[:N] 主进程以非 0 状态退出 不因为 daemon 重启而启动 有限次数重试的批处理、临时任务
always 不论退出码都重启 会重新启动;之前手动停止的容器也会启动 必须随 daemon 恢复的常驻服务
unless-stopped 不论退出码都重启 会启动,但之前被手动停止的容器保持停止 最常见的单机常驻服务

N 是最大重试次数:

docker run --restart on-failure:5 example/job:1.0.0

没有写 Non-failure 会持续重试非 0 退出。它只关心退出状态,不知道业务操作是否安全;会产生重复写入的任务必须自己保证幂等。

2. 按事件比较策略

下面的"自动重启"指 restart policy 的正常行为;显式执行 docker start 永远可以启动未删除的容器。

事件 no on-failure always unless-stopped
主进程 exit 0
主进程 exit 1
进程因 OOM/SIGKILL 以非 0 退出
执行 docker stop 保持停止 保持停止 当下保持停止 保持停止
手动停止后重启 dockerd 保持停止 保持停止 重新启动 保持停止
宿主机重启,关机前容器在运行 保持停止 通常不因 daemon 启动恢复 启动 启动
healthcheck 变成 unhealthy
容器被删除 不存在,无法重启 不存在 不存在 不存在

这里最容易答错的是三点:

  1. always 不是"执行 docker stop 后马上和你对抗"。显式停止会抑制当前 daemon 生命周期中的自动重启;daemon 再启动时,always 会恢复它。
  2. unless-stopped 会记住显式停止状态,所以 daemon 或宿主机重启后仍保持停止。
  3. unhealthy 只是健康状态,不会触发 Docker Engine restart policy。需要自动替换不健康实例,应由编排平台、外部 watchdog 或应用故障退出策略负责。

不同 Engine 版本、live-restore 和 daemon 关闭路径会影响边缘时序。生产环境应该在目标版本上做故障演练,而不是只依赖表格推断。

3. on-failure 如何判断失败

restart manager 只观察容器主进程的退出码:

docker run --name job-ok --restart on-failure:3 \
  alpine:3.20 sh -c 'sleep 11; exit 0'

docker run --name job-fail --restart on-failure:3 \
  alpine:3.20 sh -c 'date; sleep 11; exit 1'

检查结果:

docker inspect job-fail --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} restarts={{.RestartCount}}'
docker logs --timestamps job-fail

Docker 会对连续崩溃使用逐步增加的重启等待,避免毫无间隔地占满 CPU 和日志;容器稳定运行一段时间后,退避状态会重置。具体等待上限属于 Engine 实现细节,不要让业务逻辑依赖精确毫秒数。

Docker 文档还把"容器已成功启动"定义为至少稳定运行约 10 秒,目的是避免一个从未正常启动的容器被无限快速拉起。实验中使用 sleep 11,可以避免不同版本边缘行为干扰观察。

4. restart policy 只关注 PID 1

如果容器内的 worker 子进程崩溃,但 PID 1 的 shell 或 supervisor 仍然存活,Docker 认为容器仍在运行,不会触发重启:

CMD myserver & tail -f /dev/null

更好的方式是让服务直接成为 PID 1:

ENTRYPOINT ["/usr/local/bin/myserver"]

确实需要多个子进程时,使用能正确转发信号、回收子进程并在关键进程失败时退出的 init/process supervisor,同时明确谁决定容器退出状态。

5. docker restart 命令不是 restart policy

名称相似,但两者完全不同:

docker restart -t 30 api

docker update --restart unless-stopped api

docker restart 对运行中的容器执行 stop/start:先发送容器配置的停止信号,等待 timeout,再在需要时 SIGKILL,然后重新启动。它不会改变 HostConfig.RestartPolicy

查看停止信号和超时:

docker inspect api --format 'signal={{.Config.StopSignal}} timeout={{.Config.StopTimeout}}'

镜像可用 STOPSIGNAL 指定默认信号;容器创建时也可用 --stop-signal--stop-timeout 覆盖。应用应在 timeout 内完成摘流、在途请求处理、连接关闭和日志刷新。

6. Compose 中的三种"restart"不要混淆

容器 restart policy

普通 docker compose up 使用服务级 restart

services:
  api:
    image: example/api:1.0.0
    restart: unless-stopped

  worker:
    image: example/worker:1.0.0
    restart: "on-failure:5"

最终仍写进 Docker 容器的 HostConfig.RestartPolicy。验证:

docker compose up -d
docker compose ps -q api | xargs docker inspect \
  --format '{{json .HostConfig.RestartPolicy}}'

depends_on.restart

services:
  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true

这里的 restart: true 不是容器退出策略。它表示 Compose 显式更新/重启依赖服务 db 时,也重启依赖它的 api,让 API 重新建连接;它不表示数据库崩溃一次,Compose 就永久驻留并替你重启 API。

deploy.restart_policy

services:
  worker:
    image: example/worker:1.0.0
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

这是 Compose Deploy Specification/编排层策略,语义包括 conditiondelaymax_attemptswindow。它与普通容器级 restart 不是同一个字段;实际是否以及如何生效取决于部署目标和 Compose 实现。使用前必须执行 docker compose config,并在目标平台验证。

7. restart policy 与健康检查

下面的服务即使持续 unhealthy,PID 1 没退出就不会因 restart: always 自动重启:

services:
  api:
    image: example/api:1.0.0
    restart: always
    healthcheck:
      test: ["CMD", "/app/healthcheck"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s

健康检查的主要消费者是人、监控和编排平台。常见设计有两种:

  1. 单机 Docker:应用检测到不可恢复状态后主动退出,由 restart policy 拉起;可恢复故障只报 unhealthy/指标并继续重试。
  2. 编排平台:应用保持运行并报告 readiness/liveness,由平台摘流、重启或替换实例。

不要让探针因为一个短暂的下游超时就杀掉整个服务,否则故障期间所有副本可能同步重启,形成重启风暴。

8. HEALTHCHECK 深入:Dockerfile vs Compose

8.1 Dockerfile HEALTHCHECK

Dockerfile 中用 HEALTHCHECK 指令为镜像内嵌健康检查:

HEALTHCHECK --interval=15s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -fsS http://localhost:8080/healthz || exit 1

参数含义:

参数 默认值 作用
--interval 30s 两次探测之间的间隔
--timeout 30s 单次探测超时,超时视为失败
--start-period 0s 容器启动后的宽限期,期间失败不计入 retries
--start-interval 5s start_period 内的探测间隔(Engine 25+)
--retries 3 连续失败多少次后标记为 unhealthy

探测命令的退出码决定结果:0 表示健康,1 表示不健康,其他值保留。

HEALTHCHECK NONE 可以在子镜像中取消父镜像的健康检查。一个 Dockerfile 只保留最后一条 HEALTHCHECK

8.2 Compose healthcheck

Compose 中直接在服务级别声明,会覆盖镜像内嵌的 HEALTHCHECK

services:
  api:
    image: example/api:1.0.0
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/healthz || exit 1"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 30s

test 的三种写法:

写法 示例 说明
CMD ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"] exec form,直接执行,不经过 shell
CMD-SHELL ["CMD-SHELL", "curl -fsS ... || exit 1"] 经过 /bin/sh -c,支持管道和逻辑运算符
字符串 "curl -fsS http://localhost:8080/healthz" 等同于 CMD-SHELL

如果需要禁用镜像中内嵌的 HEALTHCHECK:

services:
  api:
    healthcheck:
      disable: true

8.3 健康状态转换

容器启动
starting ──(start_period 内探测失败不计数)──→ starting
   │                                              │
   ├─ 探测成功 ─→ healthy                         │
   │               │                              │
   │               ├─ 连续 retries 次失败 ─→ unhealthy
   │               │                              │
   │               └─ 下次成功 ─→ healthy          │
   │                                              │
   └─ start_period 后连续 retries 次失败 ─→ unhealthy

查看当前健康状态:

docker inspect api --format '{{json .State.Health}}'
docker inspect api --format '{{.State.Health.Status}}'
docker ps --format 'table {{.Names}}\t{{.Status}}'

docker inspect.State.Health.Log 保留最近若干次探测的 stdout/stderr 和退出码,排障时先看这里。

8.4 参数调优建议

start_period 要覆盖应用的冷启动时间。Java 应用可能需要 60-120 秒加载 class 和预热连接池;如果 start_period 只有 5 秒,容器还没就绪就被标记为 unhealthy。

interval 不要太短。每次探针都会 fork 进程并执行命令,如果 interval=1s 且探针涉及 HTTP 请求或数据库查询,可能产生不必要的负载。对大多数服务 10-30 秒够用。

timeout 要留给慢响应,不是网络抖动。如果应用正常响应需要 2 秒,timeout 设成 3 秒只留 1 秒余量,GC pause 或磁盘抖动就会误判。

retries 至少 2-3 次,避免单次偶发超时就翻转状态。

8.5 探针设计原则

探针应该只检查本进程是否可服务,不要级联检查所有下游:

# 好:只检查自己的 HTTP 端口
curl -fsS http://localhost:8080/healthz

# 不好:检查自己 + 数据库 + Redis + 外部 API
curl -fsS http://localhost:8080/deep-health

如果探针包含对 Redis、数据库、外部 API 的连通性检查,任何一个下游故障都会导致所有上游服务报 unhealthy。在编排平台中,这意味着一个 Redis 短暂不可达,会导致所有依赖它的服务同时被摘流或重启。

把下游检查留给专门的监控系统或 readiness probe;liveness/healthcheck 只验证进程自身能接受请求。

8.6 实际示例

一个 Go 服务在 /healthz 返回 200 表示可以处理请求,在 /readyz 额外检查数据库连接(用于监控,不用于 Docker 健康检查):

Dockerfile:

FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .

FROM alpine:3.20
RUN apk add --no-cache curl
COPY --from=build /app/server /app/server
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \
  CMD curl -fsS http://localhost:8080/healthz || exit 1
ENTRYPOINT ["/app/server"]

Compose:

services:
  api:
    build: .
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/healthz || exit 1"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 20s

如果镜像没有 curl,可以使用 wget -qO- ... 或在应用中内置一个 /healthz 的自检客户端避免额外安装工具。

9. depends_onservice_healthy:完整启动顺序控制

9.1 三种 condition

Compose 的 depends_on 支持三种启动条件:

condition 等待什么 适合场景
service_started 容器启动(默认值) 不关心是否真正就绪
service_healthy 健康检查变为 healthy 需要等数据库/中间件真正可用
service_completed_successfully 容器以 exit 0 结束 一次性初始化任务(迁移、seed)

9.2 典型用法:API 等数据库就绪后再启动

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: dev-only
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 30s

  migrate:
    image: example/api:1.0.0
    command: ["./migrate", "up"]
    depends_on:
      db:
        condition: service_healthy

  api:
    image: example/api:1.0.0
    depends_on:
      db:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

启动顺序:db 先启动 → db healthy 后 migrate 执行 → migrate exit 0 后 api 启动。

9.3 depends_on.restart: true 的含义

services:
  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true

restart: true 表示当你执行 docker compose restart dbdocker compose up -d db(导致 db 重建)时,Compose 也会重启 api。它不是自动故障恢复机制:如果 db 的进程崩溃后被 restart policy 拉起,Compose 不会检测到并级联重启 api。

9.4 depends_on 的边界

depends_on 只控制 docker compose updocker compose stop 时的启动/停止顺序,不提供运行时保证:

  • 数据库 healthy 不等于连接永远畅通。网络分区、连接池耗尽、权限变更都可能在运行中断开连接。
  • 生产应用必须在代码中实现连接重试、超时、断路器和降级逻辑。
  • depends_on 不支持跨 Compose 项目的依赖。

验证启动顺序:

docker compose up -d
docker compose ps
docker compose logs --since 1m

10. 信号传递与优雅退出

10.1 docker stop 的流程

docker stop api
发送 STOPSIGNAL(默认 SIGTERM)给容器 PID 1
等待 stop_timeout(默认 10 秒)
进程仍未退出 → 发送 SIGKILL

如果应用在 10 秒内无法完成清理(大量在途请求、慢 SQL、大文件刷盘),要么优化退出逻辑,要么调整超时。

10.2 Dockerfile STOPSIGNAL

某些进程不使用 SIGTERM 作为优雅退出信号。例如 Nginx 使用 SIGQUIT 做 graceful shutdown:

STOPSIGNAL SIGQUIT

创建容器时也可以覆盖:

docker run --stop-signal SIGQUIT nginx:alpine

10.3 Compose stop_grace_period

services:
  api:
    image: example/api:1.0.0
    stop_grace_period: 30s

这会把 docker stop 等待时间从默认 10 秒改为 30 秒。设置过长会拖慢部署和滚动更新;设置过短会导致 SIGKILL 强杀,丢失在途请求。

10.4 exec form vs shell form:信号能不能到达应用

这是容器信号问题最常见的根因。

exec form — 应用直接成为 PID 1,接收信号:

CMD ["./server"]
ENTRYPOINT ["./server"]

进程树:

PID 1: ./server  ← 直接收到 SIGTERM

shell form — 被 /bin/sh -c 包裹,sh 不转发信号:

CMD ./server
ENTRYPOINT ./server

进程树:

PID 1: /bin/sh -c "./server"  ← 收到 SIGTERM,但 sh 默认不转发
  └── PID 2: ./server         ← 收不到 SIGTERM

结果:docker stop 发送 SIGTERM 给 PID 1(sh),sh 不处理也不转发,等待 10 秒后 SIGKILL 强杀整个 cgroup。应用没有机会做优雅退出。

修复方式:

  1. 使用 exec form(推荐)。
  2. 如果必须用 shell form,使用 exec 替换 shell 进程:CMD exec ./server
  3. 使用 init 进程。

10.5 tini / dumb-init:信号转发 + 僵尸回收

容器内的 PID 1 还有一个特殊职责:回收孤儿进程(zombie reaping)。普通应用通常不处理 wait() 系统调用来回收子进程,导致僵尸积累。

tini 和 dumb-init 是轻量 init 进程,解决两个问题:

  1. 转发信号给子进程。
  2. 回收孤儿僵尸进程。

使用 --init 注入 Docker 内置的 tini:

docker run --init example/api:1.0.0

或在 Dockerfile 中显式安装:

RUN apk add --no-cache tini
ENTRYPOINT ["tini", "--"]
CMD ["./server"]

Compose:

services:
  api:
    image: example/api:1.0.0
    init: true

进程树变为:

PID 1: tini -- ./server
  └── PID 2: ./server  ← tini 转发 SIGTERM 到这里

不是所有应用都需要 tini。如果应用直接用 exec form 运行、自己处理信号、不产生子进程,可以不加。但如果不确定,加 --init 几乎没有副作用。

10.6 优雅退出清单

一个生产服务收到终止信号后应该按顺序执行:

1. 从负载均衡器/服务注册中摘除自己
2. 停止接受新请求/新任务
3. 等待在途请求完成(设置截止时间)
4. 关闭数据库连接池、消息队列消费者
5. 刷新日志缓冲区和指标
6. exit 0

整个过程必须在 stop_grace_period 内完成,否则会被 SIGKILL。

10.7 验证实验

测试信号传递和退出时间:

docker run --name signal-test -d \
  alpine sh -c 'trap "echo caught SIGTERM; exit 0" TERM; while true; do sleep 1; done'
time docker stop signal-test
docker logs signal-test

预期:容器在 1-2 秒内退出(trap 捕获 SIGTERM 后立即 exit),日志中出现 caught SIGTERM

对比没有 trap 的情况:

docker run --name signal-no-trap -d \
  alpine sh -c 'while true; do sleep 1; done'
time docker stop signal-no-trap

预期:等满 10 秒后被 SIGKILL,因为 sh 不处理 SIGTERM。

对比使用 --init 的情况:

docker run --name signal-init -d --init \
  alpine sh -c 'while true; do sleep 1; done'
time docker stop signal-init

预期:tini 收到 SIGTERM 后转发给子进程并退出,不需要等满 10 秒。

清理:

docker rm signal-test signal-no-trap signal-init

11. restart policy 与 OOM

容器超过 cgroup 内存上限时,内核可能选择进程并发送 SIGKILL。主进程退出通常显示 137,符合 on-failure/always/unless-stopped 的重启条件:

docker inspect api --format \
  'oom={{.State.OOMKilled}} exit={{.State.ExitCode}} restart={{.RestartCount}}'
docker stats --no-stream api
docker exec api cat /sys/fs/cgroup/memory.events

自动重启只能恢复进程,不能修复内存泄漏或过低 limit。反复 OOM 会变成 crash loop,并可能打爆日志、数据库连接和下游服务。正确做法是限制并发、分析堆和堆外内存、为运行时留余量并设置告警。

12. 如何选择策略

工作负载 建议起点 原因
本地长期运行的 API、代理 unless-stopped 异常退出恢复,同时尊重人工停机状态
必须随 daemon 恢复的基础代理 always 即使之前显式停止,daemon 重启后也恢复
允许有限重试的批处理 on-failure:N 正常完成不重跑,失败有上限
数据迁移、一次性管理命令 no 或外部 job controller 避免非幂等操作被自动重复执行
Kubernetes/其他编排管理的容器 由编排平台声明 避免两层控制器产生冲突和错误假设

生产常驻服务通常从 unless-stopped 开始,但必须同时具备健康检查、优雅退出、资源限制、日志轮转和外部监控。

13. 一套可复现的实验

创建 compose.restart.yaml

name: restart-lab

services:
  no-restart:
    image: alpine:3.20
    command: ["sh", "-c", "echo no-restart; sleep 11; exit 1"]
    restart: "no"

  retry-three:
    image: alpine:3.20
    command: ["sh", "-c", "date; sleep 11; exit 1"]
    restart: "on-failure:3"

  always:
    image: alpine:3.20
    command: ["sh", "-c", "date; sleep 11; exit 0"]
    restart: always

  unless-stopped:
    image: alpine:3.20
    command: ["sh", "-c", "date; sleep 11; exit 0"]
    restart: unless-stopped

观察状态和计数:

docker compose -f compose.restart.yaml up -d
watch 'docker compose -f compose.restart.yaml ps -a'

for id in $(docker compose -f compose.restart.yaml ps -aq); do
  docker inspect "$id" --format \
    '{{.Name}} policy={{.HostConfig.RestartPolicy.Name}} exit={{.State.ExitCode}} restarts={{.RestartCount}}'
done

然后分别执行 docker stop、重启 Docker daemon、重启宿主机,记录四种服务的状态。实验结束:

docker compose -f compose.restart.yaml down

不要在远程生产 Docker context 中做这个实验;执行前用 docker context show 确认目标。

14. restart policy 排障清单

没有重启
├── RestartPolicy 是否真写入容器?      docker inspect
├── 是不是 exit 0 + on-failure?        State.ExitCode
├── 是否被显式 docker stop?            events + 运维记录
├── 容器是否已经被删除?                docker ps -a
└── 是否由另一个编排器接管?             Compose/Swarm/K8s 配置

持续重启
├── 主进程为何退出?                     logs + State.Error/ExitCode
├── 是否 OOM?                          OOMKilled + memory.events
├── entrypoint/配置/权限是否错误?        inspect + 镜像验证
├── 下游是否不可达?                     DNS/TCP/TLS/认证
└── 重试是否造成副作用?                 数据幂等、日志、连接风暴

保留现场的命令:

docker inspect api > api.inspect.json
docker logs --timestamps api > api.log 2>&1
docker events --since 30m --until 0s > docker.events.log

15. 本篇小结

restart policy 解决的是单机"进程退出后是否自动拉起",健康检查解决的是"怎么知道服务能不能用",信号与优雅退出解决的是"怎么让服务安全地停下来"。三者配合才能让服务在正确的时候恢复、在需要的时候停止、在异常时暴露状态。生产服务还需要编排平台的摘流/滚动更新、外部监控告警和应用内的重试/断路器,restart policy 只是底线,不是全部。


上一篇 下一篇
03-镜像容器与常用命令 05-资源管理与 docker update