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 ... │
└──────────────────────────────────────────────┘
三句话总结:
*Container是唯一权威的运行时容器对象("写"端,受 State 锁保护)。
ViewDB是只读副本仓库(用 memdb 实现的 ACID 事务存储)。
- 写操作完成后调
CheckpointTo/CommitInMemory把当前*Container深拷一份喂给 ViewDB,
读操作(如docker ps、docker 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:
Root、BaseFS、RWLayer:路径在重启后可能变化,由 daemon 启动时重建
HostConfig:单独写到hostconfig.json(与 config 分离,便于做迁移兼容)
LogDriver、LogCopier、restartManager:函数/通道等不可序列化的运行时对象
DependencyStore、SecretReferences: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,不落盘(用于中间状态可见性)
注意CheckpointTo和CommitInMemory都用 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 rm、docker 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 字段做单一表?
因为:
- 一个容器可以有多个名字(aliases,主要用于 Swarm)
- 需要 O(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 是字节切片,前缀查询(Get用id_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 锁竞争!
关键观察
- 写时持锁,写完同步:所有修改 Container 的操作都要先 Lock,写完后立刻 CheckpointTo
- State 字段是 in-place 修改 :直接改
State.Running = true,不创建新 State
- ViewDB 的快照隔离:读 ViewDB 的人和写 ViewDB 的人并发跑,互不阻塞
- 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
}
这是个一箭双雕的设计:
- 写盘是主要目的
- 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 上的所有状态变更方法(InitDNSHostConfig、Reset等)都用这把锁。
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 |