1. 子系统定位
restartmanager 是 Moby(Docker daemon)中负责容器自动重启策略的最小子系统。它本身不负责"启动容器",只回答一个问题:
"给定当前退出码、是否被手动停止、本次运行了多久 ------ 这个容器该不该重启?什么时候重启?"
真正的重启动作由 daemon/monitor.go 的 handleContainerExit 在收到 containerd 的 exit 事件后发起。RestartManager 只给出决策 和退避等待。
这种"决策与执行分离"的设计让重启策略可独立单测,也方便 daemon 在 shutdown 时统一 Cancel() 所有重启等待。
2. 上游数据结构:RestartPolicy
定义在 api/types/container/hostconfig.go:275:
Go
type 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 / "" | 永不(默认) | 否(必须为 0) |
| always | 总是,无论退出码、无论是否手动停止 | 否(必须为 0) |
| unless-stopped | 总是,除非容器被用户手动停止 | 否(必须为 0) |
| on-failure | 退出码非 0 时重启,受最大重试次数限制 | 是 |
ValidateRestartPolicy(hostconfig.go:320)做上述约束校验:always/unless-stopped/no 模式下 MaximumRetryCount 必须为 0;on-failure 下不能为负。
兼容性细节:v25.0.0 之前默认创建的容器 Name 为空字符串,IsNone() 把 "" 也当作 no 处理(hostconfig.go:291-293)。
3. RestartManager 结构
restartmanager.go:22:
Go
type 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。 |
常量
Go
const (
backoffMultiplier = 2
defaultTimeout = 100 * time.Millisecond
maxRestartTimeout = 1 * time.Minute
)
这三者定义了指数退避曲线:100ms → 200ms → 400ms → ... → 1min(封顶)。
4. 构造与生命周期
New(restartmanager.go:34)
Go
func New(policy container.RestartPolicy, restartCount int) *RestartManager {
return &RestartManager{policy: policy, restartCount: restartCount, cancel: make(chan struct{})}
}
注意 timeout 初始为 0(首次决策时才被置为 defaultTimeout)。restartCount 由调用方传入,对应容器持久化的 RestartCount 字段------daemon 重启后也能恢复计数。
SetPolicy(restartmanager.go:39)
Go
func (rm *RestartManager) SetPolicy(policy container.RestartPolicy) {
rm.Lock()
rm.policy = policy
rm.Unlock()
}
被 container.UpdateMonitor(container.go:707)调用,支撑 docker update --restart 在线变更策略。只换策略,不重置 timeout/ restartCount------这是后续要讨论的一个细节。
Cancel(restartmanager.go:125)
Go
func (rm *RestartManager) Cancel() {
rm.Do(func() {
rm.Lock()
rm.canceled = true
close(rm.cancel)
rm.Unlock()
})
}
借助嵌入的 sync.Once,无论调用多少次都只会关闭一次 cancel channel(关闭已关闭的 channel 会 panic,这是关键保护)。调用方:
container.ResetRestartManager(容器被删除/重建时)
daemon/shutdown路径(container.go:458:daemon 关闭时遍历所有容器,逐个Cancel(),避免重启风暴)
- daemon 启动恢复阶段,重建 manager 前先 Cancel 旧的
5. 核心方法:ShouldRestart
签名(restartmanager.go:46):
Go
func (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-49)
Go
if rm.policy.IsNone() {
return false, nil, nil
}
策略是 no 时,不加锁直接返回。高频路径的优化。
② 加锁 + 防重入 + 防 Cancel
Go
rm.Lock()
...
if rm.canceled { return false, nil, ErrRestartCanceled }
if rm.active { return false, nil, errors.New("invalid call on an active restart manager") }
active=true 表示上一次的退避 goroutine 还没结束(还没到 timeout),此时再调 ShouldRestart 是逻辑错误。
③ 退避时长更新 (关键部分,restartmanager.go:67-78)
Go
if 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-91)
Go
var 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-96)
Go
if !restart {
rm.active = false
return false, nil, nil
}
⑥ 决定重启:计数 +1、解锁、启动退避 goroutine (restartmanager.go:98-121)
Go
rm.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上等两个事件:Cancel (rm.cancel被 close)或 退避超时 (timeout.C)。
- Cancel 路径 :往
ch发送ErrRestartCanceled再 close;active没被改(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:720)
Go
func (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:99
Go
execDuration := time.Since(c.State.StartedAt)
restart, wait, err := c.RestartManager().ShouldRestart(
uint32(ctrExitStatus.ExitCode),
daemonShutdown || c.HasBeenManuallyStopped,
execDuration,
)
handleContainerExit 的关键拼装:
exitCode:来自 containerd 的 exit 事件
hasBeenManuallyStopped:daemon 正在关闭 或 容器被手动停止 ,二者任一为真就当作"手动停止"(影响unless-stopped判定)
execDuration:从State.StartedAt到 now
6.3 执行重启:daemon/monitor.go:148-177
Go
if 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.UpdateMonitor
Go
func (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) {
container.RestartManager().SetPolicy(restartPolicy)
}
docker update --restart=... 时调用。注意只换 policy,不重置 timeout/ restartCount------如果容器正在崩溃循环退避中,更新策略后仍按原退避节奏继续。
7. 状态机与并发模型
7.1 RestartManager 状态
Go
New()
│
▼
┌─────────── active=false ───────────┐
│ │
│ ShouldRestart() 判定要重启 │
│ │
▼ │
active=true │
启动 goroutine │
等待 cancel 或 timeout │
│ │
┌──────┴───────┐ │
│ │ │
timeout 到 Cancel() │
│ │ │
▼ ▼ │
active=false canceled=true │
(永不再重启)
7.2 并发安全要点
policy/restartCount/timeout/active/canceled全程在锁保护下访问。
- 退避 goroutine 在超时分支里才取锁(设
active=false);Cancel 分支不取锁(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.2 on-failure 的退出码 0 也会消耗一次"等待"
如果 exitCode == 0 且策略是 on-failure,restart 保持 false------但前面的退避 timeout 已经被更新过 了。也就是说"成功结束"也会改 rm.timeout。不过因为接下来 active=false 直接返回,没有 goroutine 真正等这个 timeout,所以实践中无影响。但下次容器再启动并失败时,新一次 ShouldRestart 会基于一个已经被"重置或翻倍"过的 timeout。这通常无害(因为同时也会走 10 秒重置逻辑),但属于隐式状态。
8.3 always 模式下 hasBeenManuallyStopped 也重启
Go
case rm.policy.IsAlways():
restart = true // 不检查 hasBeenManuallyStopped
所以 docker stop 一个 --restart=always 的容器后,daemon 仍会尝试重启它。这就是为什么 CLI 要先 Cancel() 容器的 RestartManager(或把它标 stopped)再 stop。对比之下 unless-stopped 才真正"记住用户主动停止过"。
8.4 MaximumRetryCount == 0 的双重含义
Go
if maxRetryCount := rm.policy.MaximumRetryCount; maxRetryCount == 0 || rm.restartCount < maxRetryCount {
restart = exitCode != 0
}
0 在这里是"哨兵值 = 不限次"。所以 --restart=on-failure:0 等价于无限重试,而非"不重试"。CLI 默认 on-failure 不带数字时填 0。
8.5 daemon shutdown 路径的 Cancel
container.go:458 在 daemon 关闭时遍历所有容器 Cancel()。这会让正在退避 goroutine 里的 <-rm.cancel 立刻触发,channel 收到 ErrRestartCanceled。monitor.go:172 显式判断 errors.Is(waitErr, restartmanager.ErrRestartCanceled) 后不打错误日志------这是预期行为。
8.6 restartCount 与 MaximumRetryCount 的比较时机
rm.restartCount < maxRetryCount 在自增之前 比较。所以 on-failure:3 实际允许 3 次重启(count: 0→1, 1→2, 2→3 都满足 < 3;count: 3 不满足,停止)。命名上"MaximumRetryCount = 3"语义就是"最多重试 3 次",一致。
8.7 docker update --restart 不重置状态
SetPolicy 只换策略字段。如果容器当前 restartCount 已经是 5、timeout 已经退避到 30s,把策略从 on-failure:10 改成 always,下一次崩溃仍按 30s 退避,仍带 count=6。这是有意为之:避免用户通过 update 重置来绕过崩溃循环保护。
9. 测试视角
restartmanager_test.go 只有两个用例(系统足够小):
TestRestartManagerTimeout:策略 always + executionDuration=1s → 应该重启,且rm.timeout是defaultTimeout(100ms)。
TestRestartManagerTimeoutReset:手动把rm.timeout设为 5s,调用ShouldRestart(_, _, 10s)→ 由于 executionDuration ≥ 10s,rm.timeout应回到defaultTimeout。
未覆盖但值得补测的场景:
on-failure模式下MaximumRetryCount边界(count == max-1/count == max)
unless-stopped模式下hasBeenManuallyStopped=true
Cancel()后再调ShouldRestart返回ErrRestartCanceled
active=true时重入返回同步 error
- 退避翻倍曲线(多次连续失败 timeout 演化)
- Cancel goroutine 让
<-ch收到ErrRestartCanceled
10. 学习路径建议
- 先读
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 --restart=on-failure:3 ...一个会exit 1的脚本,docker inspect观察RestartCount与State,对照退避时间表(100ms→200ms→...)验证。
关键调用关系图
Go
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