on-failure 不扛重启:一次 Docker restart 策略的反直觉排查

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. ... The on-failure policy 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 会停全站)。


四、修复方案

  1. compose 里把 服务器 A 的 20 个 (14 no + 6 on-failure)和 服务器 B 的 5 个 统一改成 restart: unless-stopped
  2. 逐个 docker compose up -d --force-recreate <服务> ------ 改策略必须 recreate (只 restart 不会生效;若不想重建容器,也可用 docker update --restart=unless-stopped <容器> 热改运行时策略) ⚠️ 注意:up -d 裸跑有风险(可能重建 ES 等有状态容器)→ 逐服务操作、低峰执行
  3. 可选:加 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 ------ 默认值不会保护你。


本文基于一个个人微服务电商演示项目的真实实施记录整理,涉及的环境信息已做脱敏处理。

相关推荐
房西杭1 小时前
GM指令:仓库扩容、清空仓库(行会仓库 + 个人仓库,Lua + proto + GMMgr注册)
后端
大哥43091 小时前
不报错 ≠ 生效:三类"静默失效"缺陷与它们的共同指纹
后端
房西杭1 小时前
整理背包 SortBag 完整实现逻辑
后端
Marcus学原理1 小时前
Playwright 入门:让浏览器自动完成重复操作
后端
弦儿1 小时前
后端 Web 实战:部门管理模块全流程(前后端分离 + RESTful + 三层架构 + 日志)
后端·restful
房西杭1 小时前
背包<->仓库转移接口(BagMgr + StoreMgr,Lua)
后端
tntxia1 小时前
设计模式中模板方法的钩子使用
后端
我的div丢了肿么办1 小时前
go语言对路由中的业务逻辑进行抽离
后端·go
leo在掘金1 小时前
纯Rust重写Photoshop,一个月7.2万Star
后端·架构