ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

restartmanager-重启管理子系统

2026/8/4 2:24:12 拓冰建站 浏览量
restartmanager-重启管理子系统 1. 子系统定位restartmanager是 MobyDocker daemon中负责容器自动重启策略的最小子系统。它本身不负责启动容器只回答一个问题给定当前退出码、是否被手动停止、本次运行了多久 —— 这个容器该不该重启什么时候重启真正的重启动作由daemon/monitor.go的handleContainerExit在收到 containerd 的 exit 事件后发起。RestartManager只给出决策和退避等待。这种决策与执行分离的设计让重启策略可独立单测也方便 daemon 在 shutdown 时统一Cancel()所有重启等待。2. 上游数据结构RestartPolicy定义在api/types/container/hostconfig.go:275type RestartPolicy struct { Name RestartPolicyMode MaximumRetryCount int } type RestartPolicyMode string const ( RestartPolicyDisabled RestartPolicyMode no RestartPolicyAlways RestartPolicyMode always RestartPolicyOnFailure RestartPolicyMode on-failure RestartPolicyUnlessStopped RestartPolicyMode unless-stopped )四种模式语义由IsNone/IsAlways/IsOnFailure/IsUnlessStopped方法封装Name何时重启MaximumRetryCount 是否生效no/永不默认否必须为 0always总是无论退出码、无论是否手动停止否必须为 0unless-stopped总是除非容器被用户手动停止否必须为 0on-failure退出码非 0 时重启受最大重试次数限制是ValidateRestartPolicyhostconfig.go:320做上述约束校验always/unless-stopped/no模式下MaximumRetryCount必须为 0on-failure下不能为负。兼容性细节v25.0.0 之前默认创建的容器Name为空字符串IsNone()把也当作no处理hostconfig.go:291-293。3. RestartManager 结构restartmanager.go:22type RestartManager struct { sync.Mutex sync.Once policy container.RestartPolicy restartCount int timeout time.Duration active bool cancel chan struct{} canceled bool }字段逐项字段含义sync.Mutex保护policy/restartCount/timeout/active/canceled。sync.Once嵌入以便Cancel()用Do(...)保证幂等多次调用只关闭一次 channel。policy当前生效的重启策略可通过SetPolicy在容器docker update时动态修改。restartCount已重启次数。New()时从容器持久化字段初始化每次决策要重启时自增。timeout当前次重启前要等待的退避时长指数增长。active是否正处于一次等待退避超时的过程中。防止并发重入ShouldRestart。cancel一个chan struct{}Cancel()时关闭让正在等待的 goroutine 立刻返回ErrRestartCanceled。canceled已取消标志使后续ShouldRestart调用直接返回ErrRestartCanceled。常量const ( backoffMultiplier 2 defaultTimeout 100 * time.Millisecond maxRestartTimeout 1 * time.Minute )这三者定义了指数退避曲线100ms → 200ms → 400ms → … → 1min封顶。4. 构造与生命周期Newrestartmanager.go:34func New(policy container.RestartPolicy, restartCount int) *RestartManager { return RestartManager{policy: policy, restartCount: restartCount, cancel: make(chan struct{})} }注意timeout初始为 0首次决策时才被置为defaultTimeout。restartCount由调用方传入对应容器持久化的RestartCount字段——daemon 重启后也能恢复计数。SetPolicyrestartmanager.go:39func (rm *RestartManager) SetPolicy(policy container.RestartPolicy) { rm.Lock() rm.policy policy rm.Unlock() }被container.UpdateMonitorcontainer.go:707调用支撑docker update --restart在线变更策略。只换策略不重置timeout/restartCount——这是后续要讨论的一个细节。Cancelrestartmanager.go:125func (rm *RestartManager) Cancel() { rm.Do(func() { rm.Lock() rm.canceled true close(rm.cancel) rm.Unlock() }) }借助嵌入的sync.Once无论调用多少次都只会关闭一次cancelchannel关闭已关闭的 channel 会 panic这是关键保护。调用方container.ResetRestartManager容器被删除/重建时daemon/shutdown路径container.go:458daemon 关闭时遍历所有容器逐个Cancel()避免重启风暴daemon 启动恢复阶段重建 manager 前先 Cancel 旧的5. 核心方法ShouldRestart签名restartmanager.go:46func (rm *RestartManager) ShouldRestart( exitCode uint32, hasBeenManuallyStopped bool, executionDuration time.Duration, ) (bool, chan error, error)返回值三段bool shouldRestart—— 决策结果chan error——退避等待通道调用方读它会阻塞到退避时间到或被 Cancel收到nil表示可以重启收到ErrRestartCanceled表示取消error—— 同步错误如 manager 已 Cancel / 重入5.1 算法流程按代码顺序拆解① 快速否决restartmanager.go:47-49if rm.policy.IsNone() { return false, nil, nil }策略是no时不加锁直接返回。高频路径的优化。② 加锁 防重入 防 Cancelrm.Lock() ... if rm.canceled { return false, nil, ErrRestartCanceled } if rm.active { return false, nil, errors.New(invalid call on an active restart manager) }activetrue表示上一次的退避 goroutine 还没结束还没到 timeout此时再调ShouldRestart是逻辑错误。③ 退避时长更新关键部分restartmanager.go:67-78if executionDuration.Seconds() 10 { rm.timeout 0 } switch { case rm.timeout 0: rm.timeout defaultTimeout // 100ms case rm.timeout maxRestartTimeout: rm.timeout * backoffMultiplier // ×2 } if rm.timeout maxRestartTimeout { rm.timeout maxRestartTimeout // 封顶 1min }两条核心规则规则 A容器存活 ≥ 10 秒就重置退避。即这次跑得够久说明不是 init 崩溃循环下次重启从 100ms 重新开始数。规则 B退避指数翻倍封顶 1 分钟。100ms → 200ms → 400ms → 800ms → 1.6s → 3.2s → 6.4s → 12.8s → 25.6s → 51.2s → 60s封顶。注意rm.timeout是实例状态而非本次决策的临时变量——下次调用会基于上一次的值继续翻倍。这是实现连续失败累计退避的关键。④ 策略判定restartmanager.go:80-91var restart bool switch { case rm.policy.IsAlways(): restart true case rm.policy.IsUnlessStopped() !hasBeenManuallyStopped: restart true case rm.policy.IsOnFailure(): if maxRetryCount : rm.policy.MaximumRetryCount; maxRetryCount 0 || rm.restartCount maxRetryCount { restart exitCode ! 0 } }要点always模式无视退出码、无视手动停止标志一律重启。unless-stopped模式只看hasBeenManuallyStopped——HasBeenManuallyStopped由docker stop/docker kill等显式停止设置持久化到容器 JSON。on-failure模式三重判断退出码非 0且未设上限或当前重启计数 上限。maxRetryCount 0是不设上限的哨兵值。no模式在 ① 已提前返回这里不会进来。⑤ 不重启清理 active 并返回restartmanager.go:93-96if !restart { rm.active false return false, nil, nil }⑥ 决定重启计数 1、解锁、启动退避 goroutinerestartmanager.go:98-121rm.restartCount unlockOnExit false rm.active true rm.Unlock() ch : make(chan error) go func() { timeout : time.NewTimer(rm.timeout) defer timeout.Stop() select { case -rm.cancel: ch - ErrRestartCanceled close(ch) case -timeout.C: rm.Lock() close(ch) rm.active false rm.Unlock() } }() return true, ch, nil这段是子系统的并发心脏几个值得注意的细节unlockOnExit false在 goroutine 启动前已手动Unlock()避免defer二次解锁 panic。goroutine 在select上等两个事件Cancelrm.cancel被 close或退避超时timeout.C。Cancel 路径往ch发送ErrRestartCanceled再 closeactive没被改manager 已废无所谓。超时路径直接close(ch)让 receiver 收到零值nil表示等待结束可以重启然后加锁把active置回false。两种路径都close(ch)确保 channel 不会被泄漏调用方一旦读到值就说明等待结束。调用方读 channel 的语义ch nil→ 退避到时可以重启ch收到ErrRestartCanceled→ 被取消放弃重启channel close 后读取零值 → 等同 nil可重启6. 调用方与 daemon 的集成6.1 持有container.RestartManager()container.go:720func (container *Container) RestartManager() *restartmanager.RestartManager { if container.restartManager nil { container.restartManager restartmanager.New( container.HostConfig.RestartPolicy, container.RestartCount, ) } return container.restartManager }懒初始化第一次访问时按当前HostConfig.RestartPolicy和持久化的RestartCount创建实例。这是 daemon 重启后恢复重启计数的关键——RestartCount是持久化在容器 JSON 里的。6.2 决策消费daemon/monitor.go:99execDuration : time.Since(c.State.StartedAt) restart, wait, err : c.RestartManager().ShouldRestart( uint32(ctrExitStatus.ExitCode), daemonShutdown || c.HasBeenManuallyStopped, execDuration, )handleContainerExit的关键拼装exitCode来自 containerd 的 exit 事件hasBeenManuallyStoppeddaemon 正在关闭或容器被手动停止二者任一为真就当作手动停止影响unless-stopped判定execDuration从State.StartedAt到 now6.3 执行重启daemon/monitor.go:148-177if restart { go func() { waitErr : -wait // 阻塞等退避 if waitErr nil { daemon.waitForStartupDone() if err : daemon.containerStart(...); err ! nil { waitErr err ... } } if waitErr ! nil { // 重启失败回退到 stopped 状态、可能 auto-remove ... } }() }异步启动新 goroutine等待退避结束再触发containerStart。这是为什么ShouldRestart要返回 channel 而不是同步阻塞——daemon 主循环不能为退避卡住。6.4 在线更新策略container.UpdateMonitorfunc (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) { container.RestartManager().SetPolicy(restartPolicy) }docker update --restart...时调用。注意只换policy不重置timeout/restartCount——如果容器正在崩溃循环退避中更新策略后仍按原退避节奏继续。7. 状态机与并发模型7.1 RestartManager 状态New() │ ▼ ┌─────────── activefalse ───────────┐ │ │ │ ShouldRestart() 判定要重启 │ │ │ ▼ │ activetrue │ 启动 goroutine │ 等待 cancel 或 timeout │ │ │ ┌──────┴───────┐ │ │ │ │ timeout 到 Cancel() │ │ │ │ ▼ ▼ │ activefalse canceledtrue │ (永不再重启)7.2 并发安全要点policy/restartCount/timeout/active/canceled全程在锁保护下访问。退避 goroutine在超时分支里才取锁设activefalseCancel 分支不取锁manager 已废弃。ShouldRestart的锁释放路径成功路径手动Unlock()后启动 goroutine失败路径由defer释放——靠unlockOnExit标志二选一。Cancel()靠sync.Once防止重复 close channel 的 panic。active防重入避免同一容器多次 exit 事件并发触发两次退避 goroutine。8. 边界条件与陷阱理解这个子系统时下面几条最容易踩坑8.1 存活 ≥ 10 秒重置退避是单边决策executionDuration 10s时把rm.timeout 0然后switch 里又把它设为defaultTimeout。所以重置的真实含义是下次退避从 100ms 重新开始而不是不退避立即重启。一个跑了 10s 后崩溃的容器第一次重启仍要等 100ms。8.2on-failure的退出码 0 也会消耗一次等待如果exitCode 0且策略是on-failurerestart保持false——但前面的退避 timeout 已经被更新过了。也就是说成功结束也会改rm.timeout。不过因为接下来activefalse直接返回没有 goroutine 真正等这个 timeout所以实践中无影响。但下次容器再启动并失败时新一次ShouldRestart会基于一个已经被重置或翻倍过的timeout。这通常无害因为同时也会走 10 秒重置逻辑但属于隐式状态。8.3always模式下hasBeenManuallyStopped也重启case rm.policy.IsAlways(): restart true // 不检查 hasBeenManuallyStopped所以docker stop一个--restartalways的容器后daemon 仍会尝试重启它。这就是为什么 CLI 要先Cancel()容器的 RestartManager或把它标 stopped再 stop。对比之下unless-stopped才真正记住用户主动停止过。8.4MaximumRetryCount 0的双重含义if maxRetryCount : rm.policy.MaximumRetryCount; maxRetryCount 0 || rm.restartCount maxRetryCount { restart exitCode ! 0 }0在这里是哨兵值 不限次。所以--restarton-failure:0等价于无限重试而非不重试。CLI 默认on-failure不带数字时填 0。8.5 daemon shutdown 路径的 Cancelcontainer.go:458在 daemon 关闭时遍历所有容器Cancel()。这会让正在退避 goroutine 里的-rm.cancel立刻触发channel 收到ErrRestartCanceled。monitor.go:172显式判断errors.Is(waitErr, restartmanager.ErrRestartCanceled)后不打错误日志——这是预期行为。8.6restartCount与MaximumRetryCount的比较时机rm.restartCount maxRetryCount在自增之前比较。所以on-failure:3实际允许 3 次重启count: 0→1, 1→2, 2→3 都满足 3count: 3 不满足停止。命名上MaximumRetryCount 3语义就是最多重试 3 次一致。8.7docker update --restart不重置状态SetPolicy只换策略字段。如果容器当前restartCount已经是 5、timeout已经退避到 30s把策略从on-failure:10改成always下一次崩溃仍按 30s 退避仍带 count6。这是有意为之避免用户通过 update 重置来绕过崩溃循环保护。9. 测试视角restartmanager_test.go只有两个用例系统足够小TestRestartManagerTimeout策略 always executionDuration1s → 应该重启且rm.timeout是defaultTimeout100ms。TestRestartManagerTimeoutReset手动把rm.timeout设为 5s调用ShouldRestart(_, _, 10s)→ 由于 executionDuration ≥ 10srm.timeout应回到defaultTimeout。未覆盖但值得补测的场景on-failure模式下MaximumRetryCount边界count max-1/count maxunless-stopped模式下hasBeenManuallyStoppedtrueCancel()后再调ShouldRestart返回ErrRestartCanceledactivetrue时重入返回同步 error退避翻倍曲线多次连续失败 timeout 演化Cancel goroutine 让-ch收到ErrRestartCanceled10. 学习路径建议先读hostconfig.go:275-344理解RestartPolicy四种模式与校验。再读restartmanager.go全文仅 ~130 行一气呵成聚焦ShouldRestart的三段——退避更新、策略判定、goroutine 启动。接着读daemon/container/container.go:720-740看 manager 如何被懒初始化、持久化字段如何注入。最后读daemon/monitor.go:34-180看决策如何被消费、退避 channel 如何驱动containerStart。动手实验docker run --restarton-failure:3 ...一个会exit 1的脚本docker inspect观察RestartCount与State对照退避时间表100ms→200ms→…验证。关键调用关系图container.RestartManager() ── 懒初始化 ── restartmanager.New() │ ├── policy ← HostConfig.RestartPolicy └── restartCount ← Container.RestartCount (持久化) container.UpdateMonitor(newPolicy) ── SetPolicy() daemon.handleContainerExit(c, e) [daemon/monitor.go:34] │ ├── c.RestartManager().ShouldRestart(exitCode, manual, dur) │ │ │ ├── 退避更新指数 2 / 封顶 1min / 10s 重置 │ ├── 策略判定always / unless-stopped / on-failure │ └── 启动 goroutine 等 cancel 或 timeout │ ├── if restart: c.State.SetRestarting(...) 启动新 goroutine 等 wait │ └── -wait nil 时调 daemon.containerStart(...) │ └── else: c.State.SetStopped(...) 可能 autoRemove daemon shutdown / container 删除 ── RestartManager.Cancel() └── Do(close(rm.cancel)) ← goroutine 收到 ErrRestartCanceled