moby-容器对象与状态学习

scss 复制代码
                ┌──────────────────────────────────────────────┐
                │              *Container                       │
                │   (container.go · 真正的"容器对象")            │
                │                                              │
                │   ┌────────────────────────────────────────┐  │
                │   │  *State  (嵌入字段)                     │  │
                │   │  (state.go · 状态机 + 全局锁)            │  │
                │   │                                        │  │
                │   │  sync.Mutex                            │  │
                │   │  Running/Paused/Restarting/...        │  │
                │   │  ctr/task (libcontainerd 引用)         │  │
                │   └────────────────────────────────────────┘  │
                │                                              │
                │   Config / HostConfig / NetworkSettings      │
                │   MountPoints / RWLayer / StreamConfig       │
                │   LogDriver / RestartManager / ...           │
                └──────────────────────────────────────────────┘
                              │
                              │ CheckpointTo / CommitInMemory
                              │   (深拷贝后存入)
                              ▼
                ┌──────────────────────────────────────────────┐
                │              ViewDB                          │
                │   (view.go · ACID 内存事务存储)               │
                │                                              │
                │   内部用 hashicorp/go-memdb                  │
                │   ┌─────────────────┐  ┌──────────────────┐  │
                │   │ containers 表    │  │ names 表          │  │
                │   │ (按 ID 索引)      │  │ (name ↔ ID)       │  │
                │   └─────────────────┘  └──────────────────┘  │
                └──────────────────────────────────────────────┘
                              │
                              │ Snapshot() 返回只读视图
                              ▼
                ┌──────────────────────────────────────────────┐
                │              *View                            │
                │   (view.go · 一次事务的快照读)                 │
                │                                              │
                │   Get(id) → *Snapshot                        │
                │   All()  → []*Snapshot                       │
                │   GetID(name) → containerID                  │
                └──────────────────────────────────────────────┘
                              │
                              │ transform() 把 *Container 摊平
                              ▼
                ┌──────────────────────────────────────────────┐
                │              Snapshot                         │
                │   (view.go · 查询用的扁平结构)                 │
                │                                              │
                │   嵌入 container.Summary(API 类型)           │
                │   额外字段:CreatedAt/Running/Pid/ExitCode ... │
                └──────────────────────────────────────────────┘

三句话总结:

  1. *Container唯一权威的运行时容器对象("写"端,受 State 锁保护)。
  1. ViewDB只读副本仓库(用 memdb 实现的 ACID 事务存储)。
  1. 写操作完成后调 CheckpointTo/CommitInMemory 把当前 *Container 深拷一份喂给 ViewDB,
    读操作(如 docker psdocker inspect)从 ViewDB 拿 Snapshot不碰 Container 锁。

第 1 章 Container:容器对象的"主存"

文件:daemon/container/container.go

1.1 结构体定义(精简版)

go 复制代码
type Container struct {
    StreamConfig *stream.Config                // stdio 流管理
    *State       `json:"State"`                // 嵌入:状态机 + 全局锁
    
    Root    string  `json:"-"`                 // 容器元数据目录(host 路径)
    BaseFS  string  `json:"-"`                 // graphdriver 挂载点(rootfs)
    RWLayer RWLayer `json:"-"`                 // 可写层引用
    
    ID              string                     // 64 字符 SHA256
    Created         time.Time
    Managed         bool                       // 是否由 Swarm 管理
    Path            string                     // entrypoint
    Args            []string                   // 命令行参数
    Config          *containertypes.Config     // 用户提交的"容器配置"
    ImageID         image.ID
    ImageManifest   *ocispec.Descriptor
    NetworkSettings *network.Settings
    LogPath         string
    Name            string
    Driver          string                     // 存储驱动(overlay2 等)
    
    ImagePlatform ocispec.Platform
    
    RestartCount             int
    HasBeenStartedBefore     bool
    HasBeenManuallyStopped   bool              // unless-stopped 策略用
    HasBeenManuallyRestarted bool `json:"-"`
    
    MountPoints   map[string]*volumemounts.MountPoint
    HostConfig    *containertypes.HostConfig `json:"-"`  // 不进 config.v2.json
    ExecCommands  *ExecStore                 `json:"-"`
    
    DependencyStore  agentexec.DependencyGetter `json:"-"`  // Swarm 依赖
    SecretReferences []*swarmtypes.SecretReference
    ConfigReferences []*swarmtypes.ConfigReference
    
    LogDriver      logger.Logger  `json:"-"`   // 运行时实例
    LogCopier      *logger.Copier `json:"-"`
    restartManager *restartmanager.RestartManager
    attachContext  *attachContext
    
    // 平台特定字段(Unix)
    SecurityOptions
    HostnamePath   string
    HostsPath      string
    ShmPath        string
    ResolvConfPath string
    
    // 平台特定字段(Windows)
    NetworkSharedContainerID string   `json:"-"`
    SharedEndpointList       []string `json:"-"`
    LocalLogCacheMeta        localLogCacheMeta
}

1.2 字段分类

把字段按"功能维度"分组更容易理解:

维度 字段 说明
标识 ID, Name, Created, Managed 容器身份证
运行时状态 *State(嵌入) 详见第 2 章
用户配置 Config, HostConfig 来自 docker run 参数
镜像 ImageID, ImageManifest, ImagePlatform 容器从哪个镜像跑起来
文件系统 Root, BaseFS, RWLayer, Driver, MountPoints rootfs 与卷
网络 NetworkSettings IP/网关/端口映射
日志 LogDriver, LogCopier, LogPath, LocalLogCacheMeta 日志驱动相关
Swarm 编排 DependencyStore, SecretReferences, ConfigReferences, Managed 集群相关
进程 I/O StreamConfig, attachContext stdin/stdout/stderr + attach
重启策略 RestartCount, restartManager, HasBeenManuallyStopped/Restarted --restart 策略
平台特定 SecurityOptions, HostnamePath, NetworkSharedContainerID, ... Unix/Windows 各异

1.3 json tag 的玄机

注意几个带json:"-"的字段------它们 被持久化到config.v2.json

  • RootBaseFSRWLayer:路径在重启后可能变化,由 daemon 启动时重建
  • HostConfig:单独写到hostconfig.json(与 config 分离,便于做迁移兼容)
  • LogDriverLogCopierrestartManager:函数/通道等不可序列化的运行时对象
  • DependencyStoreSecretReferences:Swarm 才会用到,由 Swarm 重新注入
  • HasBeenManuallyRestarted:只对当前 daemon 生命周期有意义

被持久化的字段,会在 daemon 重启后通过FromDisk()重建。

1.4 三类方法

Container 上的方法可分三类:

(a) 状态变更(必须持锁)

  • Reset():清掉 stdio/log,让容器能被 restart(见 monitor.go)
  • ExitOnNext():通知 RestartManager 不要再重启

(b) 路径与资源访问

  • ConfigPath()/HostConfigPath():元数据 JSON 路径
  • GetResourcePath(p):相对于 rootfs 的路径(处理符号链接)
  • GetRootResourcePath(p):相对于元数据目录的路径
  • CheckpointDir():检查点目录
  • MountsResourcePath()/SecretMountPath():Swarm 相关

(c) 持久化与视图同步

  • FromDisk():从config.v2.json+hostconfig.json加载
  • CheckpointTo(ctx, store)落盘 + 同步给 ViewDB(事务性)
  • CommitInMemory(store)只同步给 ViewDB,不落盘(用于中间状态可见性)

注意CheckpointToCommitInMemory都用 JSON encode→decode 做深拷贝。这是个有趣的 trick:避免手写Clone(),也避免循环引用导致的深拷贝陷阱。


第 2 章 State:状态机的 7 个 bool

文件:daemon/container/state.go

2.1 结构体

go 复制代码
type State struct {
    sync.Mutex                  // ← 容器级全局锁,导出!
    
    Running           bool
    Paused            bool
    Restarting        bool
    OOMKilled         bool
    RemovalInProgress bool `json:"-"`
    Dead              bool
    Removed           bool `json:"-"`
    
    Pid         int
    ExitCode    int    `json:"ExitCode"`
    ErrorMsg    string `json:"Error"`
    StartedAt   time.Time
    FinishedAt  time.Time
    Health      *Health
    
    stopWaiters       []chan<- StateStatus
    removeOnlyWaiters []chan<- StateStatus
    
    ctr  libcontainerdtypes.Container  // containerd 容器引用
    task libcontainerdtypes.Task       // 运行时 task 引用
}

2.2 为什么用多个 bool 而不是枚举?

docker 容器的状态不是互斥的。这是设计上最容易被误解的点:

Running Paused Restarting 真实含义
F F F created(已创建未启动)或 exited(已退出)
T F F running(正常运行)
T T F paused(被冻结,但进程还活着)
T F T restarting(旧进程刚退,即将拉起新进程)
F F F (Dead=T) dead(已标记删除,无法再 start)

为什么 Paused 时 Running 也是 true?

  • Linux 用 freezer cgroup冻结进程,不是 kill
  • 进程仍在运行队列里,只是不被调度
  • 所以"Running + Paused"反映"进程还在,但被冻结"

为什么 Restarting 时 Running 也是 true?

  • 重启是个过程:旧 task 退出 → daemon 收到 Exit 事件 → 拉起新 task
  • 在这个窗口期,外部检查(如docker rmdocker stop)需要把容器当作"还在运行"对待
  • 否则会出现"重启中突然被 rm"导致资源残留

2.3 状态机转换图

sql 复制代码
                        ┌──────────────┐
                        │   created    │ ← create 成功
                        │ (no StartedAt)│
                        └──────┬───────┘
                               │ start
                               ▼
                        ┌──────────────┐
              ┌─────────│   running    │─────────┐
              │         │ (Running=T)  │         │
              │         └──────────────┘         │
              │ pause              exit/restart  │
              ▼                                  ▼
       ┌──────────────┐                  ┌──────────────┐
       │   paused     │                  │   exited     │
       │ Running+Pause│                  │ Running=F    │
       └──────┬───────┘                  └──────┬───────┘
              │ unpause                         │ restart policy
              └──────────► running ◄────────────┘
                                            │
                                            │ restart 异步发起
                                            ▼
                                     ┌──────────────┐
                                     │  restarting  │
                                     │ Running+Rest │
                                     └──────┬───────┘
                                            │ 新 task 起来
                                            ▼
                                       running
                                            │
                                            │ docker rm
                                            ▼
                                     ┌──────────────┐
                                     │  RemovalIn   │
                                     │  Progress    │
                                     └──────┬───────┘
                                            │
                                            ▼
                                     ┌──────────────┐
                                     │    Dead      │
                                     │  (Removed=T) │
                                     └──────────────┘

2.4 关键状态变更方法

每个方法都对应一个生命周期事件:

方法 设置 谁调用
SetRunning(ctr, task, startedAt) Running=T, Paused=F, Restarting=F, Pid, StartedAt start.go::containerStart 成功
SetStopped(exitStatus) Running=F, ExitCode, FinishedAt, 通知 stopWaiters processExit 退出处理
SetRestarting(exitStatus) Running=T, Restarting=T, Pid=0, 通知 stopWaiters processExit 决定重启时
SetRemovalInProgress() RemovalInProgress=T daemon.delete 开始
SetRemoved()/SetRemovalError(err) Removed=T, 通知所有 waiter daemon.delete 完成
SetError(err) ErrorMsg 失败路径记录原因

2.5 State() 方法:把多 bool 折叠成枚举字符串

由于字段非互斥,对外暴露时要按优先级排序:

kotlin 复制代码
func (s *State) State() container.ContainerState {
    if s.Running {
        if s.Paused {
            return container.StatePaused
        }
        if s.Restarting {
            return container.StateRestarting
        }
        return container.StateRunning
    }
    if s.RemovalInProgress {
        return container.StateRemoving
    }
    if s.Dead {
        return container.StateDead
    }
    if s.StartedAt.IsZero() {
        return container.StateCreated
    }
    return container.StateExited
}

这就是docker psSTATUS 列显示的状态字符串:created / running / paused / restarting / removing / dead / exited。

类似的,String()方法返回的是带时间信息的更人类可读形式:

  • Up 2 hours------ 正常运行
  • Up 2 hours (Paused)------ 暂停
  • Up 2 hours (unhealthy)------ 健康检查失败
  • Exited (0) 5 minutes ago------ 已退出
  • Restarting (1) 3 seconds ago------ 重启中
  • Created------ 创建未启动
  • Dead------ 已死

2.6 Wait:异步等待的秘密

go 复制代码
func (s *State) Wait(ctx, condition) <-chan StateStatus {
    s.Lock()
    defer s.Unlock()
    
    // 1. 如果条件已满足,立即返回
    if s.conditionAlreadyMet(condition) {
        return alreadyMetChannel(...)
    }
    
    // 2. 否则注册一个 waiter
    waitC := make(chan StateStatus, 1)
    if condition == WaitConditionRemoved {
        s.removeOnlyWaiters = append(s.removeOnlyWaiters, waitC)
    } else {
        s.stopWaiters = append(s.stopWaiters, waitC)
    }
    
    // 3. 起 goroutine 监听 ctx 和 waitC
    go func() {
        select {
        case <-ctx.Done():  // 超时或取消
            resultC <- StateStatus{exitCode: -1, err: ctx.Err()}
        case status := <-waitC:  // 容器真的停了
            resultC <- status
        }
    }()
    
    return resultC
}

这是 docker wait命令的核心机制

  • docker wait不轮询,而是注册 channel
  • SetStopped/SetRestarting/SetRemoved都会调notifyAndClear向所有 waiter 发信号
  • ctx 超时也通过 select 处理,不会泄漏 goroutine

2.7 ctr / task 引用:为什么是 unexported?

arduino 复制代码
ctr  libcontainerdtypes.Container  // 小写
task libcontainerdtypes.Task       // 小写

注释说明:强制通过 getter 访问,避免忘了 nil 检查。

go 复制代码
func (s *State) Task() (_ libcontainerdtypes.Task, ok bool) {
    return s.task, s.task != nil
}

func (s *State) C8dContainer() (_ libcontainerdtypes.Container, ok bool) {
    return s.ctr, s.ctr != nil
}

返回值是多值的:(_, ok bool)。这样调用方写:

go 复制代码
task, ok := container.State.Task()
if !ok {
    return errors.New("no task")
}
task.Kill(...)

如果忘了检查ok,编译器会警告(未使用的变量)。这是个值得借鉴的 Go 习惯用法


第 3 章 ViewDB:让查询不阻塞写

文件:daemon/container/view.go

3.1 问题:为什么需要 ViewDB?

考虑这个场景:

css 复制代码
时刻 T1:daemon 正在 stop 容器 A(持有 A 的锁)
时刻 T2:用户敲 docker ps(需要列出所有容器)

没有 ViewDB 的做法

  • docker ps要遍历daemon.containers,对每个容器拿锁、读字段、释放锁
  • 容器 A 的锁被 stop 持有,docker ps卡住等
  • 大量短查询会被长操作阻塞

有 ViewDB 的做法

  • 写操作(start/stop/create)持有 Container 锁,修改 Container 对象
  • 写完后调CheckpointTo(ctx, viewDB)深拷一份给 ViewDB
  • 读操作从 ViewDB 拿快照,完全不碰 Container 锁
  • ViewDB 内部用 memdb 提供事务,读和写并发安全

3.2 memdb:HashiCorp 的内存事务数据库

view.go用了github.com/hashicorp/go-memdb。这是个ACID 内存事务数据库

  • Atomic:一个 txn 内的多步修改要么全成功,要么全回滚
  • Consistent:schema 校验(unique 索引等)
  • Isolated:事务间通过 MVCC 隔离(多版本并发控制)
  • D urable:嗯......内存数据库,持久化(容器对象自己负责落盘)

MVCC 是关键:读事务看到的是一个一致的快照,即使写事务同时在改。

3.3 schema:两张表

go 复制代码
var schema = &memdb.DBSchema{
    Tables: map[string]*memdb.TableSchema{
        "containers": {                       // 容器表
            Indexes: map[string]*memdb.IndexSchema{
                "id": {                       // 唯一 ID 索引
                    Unique:  true,
                    Indexer: &containerByIDIndexer{},
                },
            },
        },
        "names": {                            // 名字表
            Indexes: map[string]*memdb.IndexSchema{
                "id": {                       // name → containerID(name 是主键)
                    Unique:  true,
                    Indexer: &namesByNameIndexer{},
                },
                "containerid": {              // containerID → 所有 name(反向)
                    Indexer: &namesByContainerIDIndexer{},
                },
            },
        },
    },
}

为什么不直接在 Container 上加 Name 字段做单一表?

因为:

  1. 一个容器可以有多个名字(aliases,主要用于 Swarm)
  1. 需要 O(1) 反查"这个名字指向哪个容器"
  1. memdb 不支持多值字段索引,得拆表

3.4 自定义 indexer:为什么要 "\x00" 终止符?

go 复制代码
const terminator = "\x00"

func (e *containerByIDIndexer) FromObject(obj any) (bool, []byte, error) {
    c := obj.(*Container)
    return true, []byte(c.ID + terminator), nil
}

memdb 的索引 key 是字节切片,前缀查询(Getid_prefix)按字典序匹配。

如果不加终止符,abc既是abc的前缀,也是abcd的前缀------查询abc会同时命中两者。

加上\x00(ASCII 0)后,因为正常 ID 不会包含\x00,前缀匹配就能精确到字符边界。

这是个经典的 C 字符串思路在 Go 里的复刻。

3.5 写入:Save / Delete

go 复制代码
func (db *ViewDB) Save(c *Container) error {
    return db.withTxn(func(txn *memdb.Txn) error {
        return txn.Insert(memdbContainersTable, c)
    })
}

func (db *ViewDB) withTxn(cb func(*memdb.Txn) error) error {
    txn := db.store.Txn(true)            // true = 写事务
    err := cb(txn)
    if err != nil {
        txn.Abort()
        return err
    }
    txn.Commit()
    return nil
}

重要约定 :传入 Save 的*Container必须是深拷贝 ,调用方负责生成(见 Container.CheckpointTo)。

否则 memdb 里存的还是原对象指针,就失去了"快照隔离"的意义。

3.6 读取:Snapshot / View

go 复制代码
type View struct {
    txn *memdb.Txn
}

func (db *ViewDB) Snapshot() *View {
    return &View{txn: db.store.Txn(false)}    // false = 读事务
}

func (v *View) Get(id string) (*Snapshot, error) {
    s, err := v.txn.First(memdbContainersTable, memdbIDIndex, id)
    if s == nil {
        return nil, errdefs.NotFound(...)
    }
    return v.transform(s.(*Container)), nil
}

View 是个事务包装器 ,每次 Snapshot() 都开一个新的读事务。

调用方拿到 View 后,可以多次调用 Get/All/GetID,它们都基于同一个一致快照。

3.7 transform:从 Container 到 Snapshot

yaml 复制代码
func (v *View) transform(ctr *Container) *Snapshot {
    snapshot := &Snapshot{
        Summary: container.Summary{
            ID:      ctr.ID,
            Names:   v.getNames(ctr.ID),
            ImageID: ctr.ImageID.String(),
            ...
        },
        CreatedAt:    ctr.Created,
        StartedAt:    ctr.State.StartedAt,
        Name:         ctr.Name,
        Pid:          ctr.State.Pid,
        Running:      ctr.State.Running,
        Paused:       ctr.State.Paused,
        ExitCode:     ctr.State.ExitCode,
        ...
    }
    // ... 组装 ports、networks、health 等
    return snapshot
}

Snapshot嵌入了 API 类型container.Summary(就是 docker ps 看到的 JSON),并加上几个查询过滤用的额外字段(CreatedAt 纳秒精度、Running、Paused 等)。

为什么不让 docker ps直接遍历 Container?

  • 一次docker ps -f status=running -f health=unhealthy可能要过滤几百个容器
  • 每个容器都要拿锁、读字段、释放锁 → 锁竞争激烈
  • ViewDB 提供一致快照,过滤逻辑可以在快照上跑,零锁竞争

第 4 章 三者协同时序:一次 docker start 的视角

下图展示一次docker start <id>完整流程,重点突出 Container / State / ViewDB 的协作:

scss 复制代码
HTTP POST /containers/{id}/start
   │
   ▼
daemon.ContainerStart
   │
   ▼
container.Lock()                  ←── 获取 State 的全局锁
   │
   ├─ validateState()              ←── 读 State.Running/Paused/Dead
   │
   ├─ containerStart()
   │    │
   │    ├─ attachStdio
   │    ├─ network.Setup
   │    ├─ container.ToOCI()
   │    │
   │    ├─ containerd.NewContainer(...)
   │    │
   │    ├─ container.NewTask(...)
   │    │
   │    ├─ task.Start()             ←── runc 起进程
   │    │
   │    └─ State.SetRunning(ctr, task, now)
   │         │
   │         ├─ State.Running = true
   │         ├─ State.Pid = task.Pid()
   │         ├─ State.StartedAt = now
   │         └─ State.ctr = ctr, State.task = task
   │
   ├─ container.CheckpointTo(ctx, viewDB)
   │    │
   │    ├─ container.toDisk()       ←── 落盘 config.v2.json
   │    │    │
   │    │    └─ JSON encode→decode 深拷贝
   │    │
   │    └─ viewDB.Save(deepCopy)    ←── 深拷贝入 ViewDB(写事务)
   │
   ├─ daemon.LogContainerEvent(start)
   │
container.Unlock()                ←── 释放锁

# 异步:containerd 在某时刻推送 Exit 事件
   │
   ▼
daemon.processExit(container, info)
   │
   ▼
container.Lock()
   │
   ├─ 询问 RestartManager.ShouldRestart?
   │
   ├─ 如不重启:
   │    State.SetStopped(exitStatus)
   │      ├─ State.Running = false
   │      ├─ State.ExitCode = info.ExitCode
   │      ├─ State.FinishedAt = now
   │      └─ notifyAndClear(&stopWaiters)    ←── 唤醒 docker wait
   │
   ├─ 如重启:
   │    State.SetRestarting(exitStatus)
   │      ├─ State.Running = true
   │      ├─ State.Restarting = true
   │      ├─ notifyAndClear(&stopWaiters)    ←── 注意:重启也算"停止"事件
   │      └─ go daemon.containerRestart()
   │
   ├─ container.CheckpointTo(ctx, viewDB)
   │
container.Unlock()

# 此时如果有人调 docker ps:
#    viewDB.Snapshot() → View.All() → 遍历 transform 出 Snapshot
#    整个过程零 Container 锁竞争!

关键观察

  1. 写时持锁,写完同步:所有修改 Container 的操作都要先 Lock,写完后立刻 CheckpointTo
  1. State 字段是 in-place 修改 :直接改State.Running = true,不创建新 State
  1. ViewDB 的快照隔离:读 ViewDB 的人和写 ViewDB 的人并发跑,互不阻塞
  1. CheckpointTo 是事务性的:落盘 + ViewDB 同步,要么都成功,要么都回滚(但实际回滚逻辑要看 daemon 实现)

第 5 章 持久化与重启恢复

5.1 落盘的两个文件

每个容器在/var/lib/docker/containers/<id>/下有:

  • config.v2.json------ Container 大部分字段的 JSON
  • hostconfig.json------ HostConfig(单独存放,便于做兼容性)
ini 复制代码
const (
    configFileName     = "config.v2.json"
    hostConfigFileName = "hostconfig.json"
)

为什么 HostConfig 要单独?

  • HostConfig 包含主机相关的设置(cgroup、binds、cap_add 等)
  • 容器迁移到另一台主机时,HostConfig 可能完全不同
  • 分离后便于"换主机重写 HostConfig,保留 config"的迁移逻辑

5.2 写盘的原子性:atomicwriter

css 复制代码
f, err := atomicwriter.New(pth, 0o600)

atomicwriter来自github.com/moby/sys/atomicwriter

  • 先写到临时文件<pth>.tmp
  • 写完后rename成正式文件
  • rename 在同一文件系统下是原子的
  • 即使中途崩溃,也不会留下半写的 JSON

5.3 深拷贝的副产物

go 复制代码
func (container *Container) toDisk() (*Container, error) {
    // ... 写盘 ...
    
    // 同时把 JSON 解析回一个新的 *Container
    var deepCopy Container
    if err := json.NewDecoder(&buf).Decode(&deepCopy); err != nil {
        return nil, err
    }
    deepCopy.HostConfig, err = container.WriteHostConfig()
    return &deepCopy, nil
}

这是个一箭双雕的设计:

  1. 写盘是主要目的
  1. JSON encode→decode 顺带做了深拷贝------返回的对象可以安全传给 ViewDB

优点:不用手写 Clone(),所有 unexported 字段也都被正确处理(除 json:"-" 的)

缺点:性能不如反射拷贝,但 docker 容器数量级不大,可以接受

5.4 重启恢复流程

daemon 启动时:

bash 复制代码
1. 遍历 /var/lib/docker/containers/*/config.v2.json
2. 对每个文件:
   container := NewBaseContainer(id, root)
   container.FromDisk()           ← 读 config + hostconfig
3. 把 container 加入 daemon.containers(memoryStore)
4. container.CheckpointTo(viewDB) ← 同步给 ViewDB
5. 如果 State.Running:
   - 容器在 daemon 重启前是 running 状态
   - 调 RestoreTask() 重新连接到 containerd 里仍存活的 task
   - 或者标记为 exited(取决于 daemon 重启策略)

关键点:daemon 重启不会 杀死运行中的容器进程(因为 containerd 是独立进程)。

它只是"丢失"了内存中的 *Container 对象。重启后通过 FromDisk + RestoreTask 重新关联。


第 6 章 名字与 ID:两套索引

容器有 ID(64 字符 SHA256)和 Name(用户起的可读名字)。ViewDB 维护两套索引:

6.1 ID 前缀查询

go 复制代码
func (db *ViewDB) GetByPrefix(s string) (string, error) {
    iter, err := db.store.Txn(false).Get(memdbContainersTable, memdbIDIndexPrefix, s)
    // 遍历匹配项
    // 0 个 → NotFound
    // 1 个 → 返回完整 ID
    // 多个 → InvalidParameter("multiple IDs found")
}

这就是docker start abc123能用缩写匹配完整 ID 的原理。

6.2 名字注册与释放

go 复制代码
func (db *ViewDB) ReserveName(name, containerID string) error {
    // 写事务里:
    //   1. 查 name 表,如果已被占用且不是同一个 containerID → Conflict
    //   2. 否则插入 nameAssociation{name, containerID}
}

func (db *ViewDB) ReleaseName(name string) error {
    // 删除 name 表中对应行
}

幂等性ReserveName(name, containerID)对相同 (name, containerID) 重复调用是 OK 的;

对相同 name 但不同 containerID 会失败。

6.3 反向查询:容器有哪些名字

go 复制代码
func (v *View) getNames(containerID string) []string {
    iter, _ := v.txn.Get(memdbNamesTable, memdbContainerIDIndex, containerID)
    // 收集所有 nameAssociation.name
}

Swarm 模式下一个容器可能有多个名字(容器名 + service 名 + task 名),所以需要支持多对一。

6.4 删除容器时的清理

go 复制代码
func (db *ViewDB) Delete(c *Container) error {
    return db.withTxn(func(txn *memdb.Txn) error {
        view := &View{txn: txn}
        names := view.getNames(c.ID)        // 先查出所有名字
        
        for _, name := range names {
            txn.Delete(memdbNamesTable, nameAssociation{name: name})
        }
        
        // 容器本身可能不存在(清理残留调用),忽略错误
        txn.Delete(memdbContainersTable, NewBaseContainer(c.ID, c.Root))
        return nil
    })
}

事务原子性保证:名字和容器要么都删,要么都不删,绝不会出现"名字删了但容器还在"的脏状态。


第 7 章 锁的层次与并发陷阱

7.1 三层锁

docker daemon 涉及三层锁,从外到内:

层级 保护什么
daemon 级 daemon.containers内的 RWMutex containers map 的增删
容器级 *Container.State.Mutex 单个容器的所有字段
子系统级 health.mu / ViewDB 内部锁 子系统自己的状态

7.2 锁顺序(必须遵守!)

获取顺序:daemon 级 → 容器级 → 子系统级

释放顺序:相反(或按作用域)

违反的后果:死锁。

例如:

scss 复制代码
// 错误:先拿容器锁,再遍历 containers map
container.Lock()                  // 容器锁
for id, c := range daemon.containers.List() {
    c.Lock()                      // 别的容器锁
    // ...
}

// 此时另一个 goroutine:
//   daemon.containers.Lock()       // 想 Lock map
//   container.Lock()               // 想拿同一把锁
// → 死锁

7.3 持锁期间不能做的事

  • ❌长时间网络请求(拖死整个 daemon)
  • ❌等待 channel(容易死锁,特别是反向通知的 channel)
  • ❌嵌套获取别的容器锁(死锁风险)
  • ❌调用 ctx 卡住的外部 API
  • ✅内存数据结构修改
  • ✅简短的本地文件 I/O(如 toDisk)

7.4 State 锁是"嵌入"的

注意 State 的锁是通过嵌入给 Container 的:

go 复制代码
type State struct {
    sync.Mutex
    // ...
}

type Container struct {
    *State   `json:"State"`
    // ...
}

所以container.Lock()container.State.Lock()是同一把锁。

Container 上的所有状态变更方法(InitDNSHostConfigReset等)都用这把锁。

7.5 ViewDB 的读事务:可长可短

go 复制代码
// 推荐用法:用完就关
view := db.Snapshot()
defer view.txn.Abort()        // 注意:View 没暴露 Close,但 memdb 推荐显式 Abort
result, _ := view.Get(id)

// 不推荐:把 View 长期持有
// 虽然 MVCC 保证看到一致快照,但 memdb 会保留所有历史版本直到读事务结束
// → 内存泄漏风险

实际上 view.go 的 View 没有 Close 方法,依赖 GC 回收。短期使用问题不大。


第 8 章 关键方法速查

Container 方法

方法 文件位置 作用
NewBaseContainer(id, root) container.go:182 工厂函数
FromDisk() container.go:195 从 config.v2.json + hostconfig.json 加载
CheckpointTo(ctx, store) container.go:264 落盘 + 同步 ViewDB(事务)
CommitInMemory(store) container.go:341 仅同步 ViewDB(不落盘)
GetResourcePath(p) container.go:401 rootfs 内的路径(处理符号链接)
GetRootResourcePath(p) container.go:443 元数据目录内的路径
ExitOnNext() container.go:452 通知 RestartManager 别再重启
StartLogger() container.go:477 根据配置创建日志驱动
ShouldRestart() container.go:581 询问 RestartManager
StopSignal() container.go:633 取停止信号(默认 SIGTERM)
StopTimeout() container.go:646 取停止超时
RestartManager() container.go:721 懒加载重启管理器
Reset() monitor.go:17 重启前清理(关流、关日志)
GetRunningTask() container.go:910 取当前 task,必须 Running
RestoreTask(ctx, client) container.go:887 daemon 重启后重新关联 task
AttachContext() container.go:740 用于 attach 调用的 ctx
InitializeStdio(iop) container.go:789 libcontainerd 回调:连 stdio

State 方法

方法 文件位置 作用
State() container.ContainerState state.go:154 折叠成枚举状态
String() string state.go:116 人类可读描述(带时间)
Wait(ctx, condition) state.go:190 异步等待容器状态变化
IsRunning() / IsPaused() / IsRestarting() state.go:259/369/388 加锁读取
GetPID() state.go:266 加锁读 PID
SetRunning(ctr, task, start) state.go:279 标记为运行
SetRunningExternal(ctr, task) state.go:284 外部已起的进程标记为运行
SetStopped(exitStatus) state.go:312 标记为停止
SetRestarting(exitStatus) state.go:329 标记为重启中
SetError(err) state.go:349 记录错误原因
SetRemovalInProgress() state.go:396 标记删除中
SetRemoved() / SetRemovalError(err) state.go:429/435 标记已删除
Task() state.go:478 取 task + ok 标志
C8dContainer() state.go:468 取 containerd 容器引用

ViewDB 方法

方法 文件位置 作用
NewViewDB() view.go:94 工厂
GetByPrefix(s) view.go:105 ID 前缀查询
Snapshot() view.go:134 开一个读事务
Save(c) view.go:153 保存容器(必须深拷)
Delete(c) view.go:160 删容器 + 关联名字
ReserveName(name, id) view.go:179 注册名字
ReleaseName(name) view.go:197 释放名字

View 方法

方法 文件位置 作用
All() view.go:209 列出所有 Snapshot
Get(id) view.go:228 按 ID 查
GetID(name) view.go:260 名字查 ID
GetAllNames() view.go:272 所有容器→名字映射
transform(ctr) view.go:293 Container 摊平为 Snapshot
相关推荐
xiaoshuai10241 小时前
Controller 直连了数据库、模块缠成死结:用 ArchUnit 把架构钉死
后端
陈随易13 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·后端·程序员
IT_陈寒15 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰16 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
用户83562907805117 小时前
Python 实现 PDF 文件加密与解密方法
后端·python
小满zs17 小时前
Go语言第二章(小无相功)
后端·go
用户83562907805117 小时前
使用 Python 冻结与拆分 Excel 窗格教程
后端·python
karry_k17 小时前
MyBatis批量insert-select踩坑:useGeneratedKeys=true 可能让PostgreSQL返回大量插入结果
java·后端