on-failure 不扛重启:一次 Docker restart 策略的反直觉排查
主题:容器构建与编排卫生
先说结论
restart: on-failure 不能扛住宿主机/Docker daemon 重启 ------ 只有 unless-stopped 能。
这一点和大多数人的直觉相反。我实测两台服务器后发现:
| 机器 | no |
on-failure |
unless-stopped |
重启后会自己回来 |
|---|---|---|---|---|
| 服务器 A(21 容器) | 14 | 6 | 1 | 1/21 → 20 个不恢复 |
| 服务器 B(5 容器) | 0 | 5(全部) | 0 | 0/5 → 全不恢复 |
而且服务器 B 不恢复的后果比 A 更危险(下文说明)。
一、官方语义:为什么 on-failure 不扛重启
Docker 官方文档原文:
on-failure[:max-retries]--- Restart the container if it exits due to an error, which manifests as a non-zero exit code. ... Theon-failurepolicy only prompts a restart if the container exits with a failure. It doesn't restart the container if the daemon restarts.
整理成表:
| 策略 | 容器进程自己崩 | 宿主机 / Docker daemon 重启 | 手动 stop 后 |
|---|---|---|---|
no |
❌ | ❌ 不恢复 | ❌ |
on-failure |
✅(仅非零退出) | ❌ 不恢复 ← 反直觉 | ❌ |
unless-stopped |
✅ | ✅ 恢复 | ❌(保持停止) |
💡 所以默认
restart: no的容器,在宿主机重启后根本不会回来 ------ 而"容器running过"会让人以为它一直在。
二、实测:配置分布与真实现状
配置侧(compose)
只有 12 个服务 配了 restart:11 个 on-failure + 1 个 unless-stopped 。其余 14 个服务压根没配 (no)------ 而且里面包含全部核心中间件:
mysql / redis / nacos / rabbitmq / elasticsearch / seata /
sentinel / skywalking-oap / skywalking-ui / 以及若干业务服务 / 前端
运行侧(实际容器)
- 服务器 A:6 个
on-failure+ 1 个unless-stopped+ 14 个no - 服务器 B:5 个全部
on-failure
⚠️ 一个容易犯的比较错误
这两台服务器共用同一份 compose 文件(两机合并的单一文件,共 26 个服务)。所以:
拿"文件里的总数"和"某一台机器在跑的容器数"比,会得出错误结论。 我一开始就因此误判成"配置了 11 个但服务器只有 6 个 = 没部署" ------ 实际上那 11 个里,有 5 个属于另一台机器。核对时要按"该机在跑的服务"逐个比对。
三、🔴 为什么"服务器 B 不恢复"更危险
服务器 B 上跑的是 Redis 从库 + 3 个哨兵 + 秒杀副本 ,而且全部是 on-failure → 一重启,5/5 全不恢复。
连锁反应:
css
服务器 B 重启 → 从库 + 3 哨兵全部消失
→ 服务器 A 的 Redis 主库检测不到从库
→ 主库配置了 min-replicas-to-write 1 → 拒绝所有写(NOREPLICAS,读仍可用)
→ 秒杀、登录 token 黑名单等所有【写入路径】全挂
→ 且哨兵全灭,无法自动故障转移
这是"为了高可用反而降低可用性"的一个反例:单机时 Redis 独立;上了主从之后多了一台机器的依赖,却没给它配自启。
其他实测事实:
- 两台
LiveRestoreEnabled=false→systemctl restart docker会停掉全部容器再按策略恢复 - 两台 Docker 都是
enabled(开机自启 daemon)→ daemon 会起来,但容器不会 - 没有任何 systemd 单元 / cron 兜底 (已查
/etc/systemd/system、/etc/cron.d、/etc/crontab) - 服务器 A 已连续运行 42 天 ,所以这个问题从未暴露过
⚠️ 方法学说明(诚实边界)
两台机器的容器都创建于各自本次开机之后 → 没有任何一次真实重启可供观察 。所以上面的结论来自 官方语义 + 配置实测 ,不是实测复现。
要复现只能真重启 ------ 而那是绝对不能在无变更窗口时裸试的(服务器 A 会停全站)。
四、修复方案
- compose 里把 服务器 A 的 20 个 (14
no+ 6on-failure)和 服务器 B 的 5 个 统一改成restart: unless-stopped - 逐个
docker compose up -d --force-recreate <服务>------ 改策略必须 recreate (只restart不会生效;若不想重建容器,也可用docker update --restart=unless-stopped <容器>热改运行时策略) ⚠️ 注意:up -d裸跑有风险(可能重建 ES 等有状态容器)→ 逐服务操作、低峰执行 - 可选:加 systemd 单元,在 docker 启动后执行
docker compose up -d(注意官方文档警告:不要同时用 process manager 和 restart policy,会冲突)
五、顺带:容器编排还有哪几个"看不见的约束"
排查过程中踩到/总结出的其他约束:
| # | 约束 | 后果 |
|---|---|---|
| ① | 单一事实源:同一构建产物只能有一份 | 项目里曾同时存在两份 Dockerfile (模块目录的旧 Alpine 版 + 部署目录的正式 Debian 版)→ 手动 build 会重踩 Alpine musl 与 MD5 不兼容的历史事故。清理后单一事实源 = 部署目录 |
| ② | env 是容器创建时的快照 | docker restart 不重读 env → "改了不生效"。所有"改密码/密钥/配置"类操作都必须 recreate |
| ③ | 单文件 bind mount 不能被 rename | 需要"写临时文件 + rename"的程序(Redis/Sentinel 改写配置)必然失败 (Resource busy)→ 哨兵必须挂目录 |
| ④ | 挂载文件的属主必须是容器内运行用户 | Redis 容器内是 uid 999 ,宿主机属主 + chmod 600 → 容器读不了。正解:用 docker root 容器 chown(无需宿主机 sudo) |
| ⑤ | "running" ≠ "可用" | 启动竞态后容器显示 running,实际未监听端口、未注册注册中心。验证必须看三件事 :端口监听 + 注册中心注册 + 实际请求 |
"改了不生效"的四个真凶(实用排查清单)
| 真凶 | 判据 |
|---|---|
| env 快照 | 用了 restart 而不是 up -d(recreate) |
| 中间件初始化变量 | MySQL MYSQL_ROOT_PASSWORD、RabbitMQ RABBITMQ_DEFAULT_* 等只在数据卷首次初始化时生效 |
| 配置权威源覆盖 | 远端空配置把本地默认擦掉 |
| 加载时机/懒加载 | 如 Sentinel 的 transport 默认懒加载(Dashboard 看不到但实际生效) |
六、一句总结
Docker 的 restart 策略里,只有
unless-stopped是"宿主机重启也能自愈"的那个。 而它默认是no------ 默认值不会保护你。
本文基于一个个人微服务电商演示项目的真实实施记录整理,涉及的环境信息已做脱敏处理。