docker-05-volume

模块概览

模块定位与职责

职责边界

volume 模块负责容器卷的完整生命周期管理,提供持久化存储能力:

  1. 卷生命周期管理

    • 创建卷(命名卷/匿名卷)
    • 删除卷(引用计数保护)
    • 列出卷(支持过滤)
    • 检查卷详情
  2. 卷挂载管理

    • 挂载卷到容器(引用计数)
    • 卸载卷(自动清理)
    • 支持子路径挂载
    • 支持只读挂载
  3. 卷驱动管理

    • 内置 local 驱动(本地目录)
    • 插件驱动(NFS/Ceph/…)
    • 驱动发现与注册
    • 驱动能力查询
  4. 卷存储管理

    • 卷元数据存储(BoltDB)
    • 卷引用追踪(容器引用)
    • 卷垃圾回收(删除未使用卷)
    • 卷备份与迁移

上下游依赖

上游调用方

  • Volume Router:处理卷相关的 HTTP API
  • Daemon:容器创建时需要挂载卷
  • Container:容器启动时挂载卷

下游被依赖方

  • Volume Drivers(local/插件驱动)
  • PluginManager:管理卷插件
  • 文件系统:实际的数据存储

模块架构图

flowchart TB
    subgraph API["API 层"]
        VolumeRouter[Volume Router]
    end
    
    subgraph Service["卷服务层"]
        VolumesService[VolumesService]
        VolumeStore[VolumeStore]
    end
    
    subgraph Core["核心组件"]
        Volume[Volume 接口]
        MountPoint[MountPoint 管理器]
        RefCounter[引用计数器]
    end
    
    subgraph Drivers["卷驱动"]
        DriverStore[Driver Store]
        LocalDriver[Local 驱动]
        PluginDriver[插件驱动]
    end
    
    subgraph Storage["存储层"]
        Metadata[(卷元数据<br/>BoltDB)]
        FileSystem[(文件系统<br/>/var/lib/docker/volumes)]
    end
    
    VolumeRouter --> VolumesService
    VolumesService --> VolumeStore
    
    VolumeStore --> Volume
    VolumeStore --> DriverStore
    VolumeStore --> RefCounter
    
    Volume --> MountPoint
    
    DriverStore --> LocalDriver
    DriverStore --> PluginDriver
    
    LocalDriver --> FileSystem
    PluginDriver -.HTTP/gRPC.-> ExternalDriver[外部存储<br/>NFS/Ceph/EBS]
    
    VolumeStore --> Metadata
    LocalDriver --> FileSystem

架构说明

1. 卷服务层

  • VolumesService:对外统一接口

    type VolumesService struct {
        vs           *VolumeStore
        ds           driverLister
        pruneRunning atomic.Bool
        eventLogger  VolumeEventLogger
    }
    

- **VolumeStore**:卷存储管理

  ```go
  type VolumeStore struct {
      locks   *locker.Locker
      volumes map[string]*volumeMetadata
      names   map[string]volume.Volume
      refs    map[string][]string  // volumeName -> []containerID
      drivers *drivers.Store
      db      *bolt.DB
  }

2. 核心组件

  • Volume 接口:统一的卷抽象

    type Volume interface {
        Name() string
        DriverName() string
        Path() string
        Mount(id string) (string, error)
        Unmount(id string) error
        Status() map[string]interface{}
    }
    

- **MountPoint**:容器挂载点

  ```go
  type MountPoint struct {
      Type        mount.Type      // bind/volume/tmpfs
      Name        string          // 卷名称
      Source      string          // 源路径
      Destination string          // 容器内路径
      Driver      string          // 驱动名称
      RW          bool            // 读写权限
      Volume      volume.Volume   // 卷对象
      Propagation mount.Propagation
  }
  • 引用计数器:防止正在使用的卷被删除

    type refCounter struct {
        refs map[string]map[string]struct{}  // volumeName -> set(refID)
        mu   sync.Mutex
    }
    
    func (rc *refCounter) Add(volume, ref string) {
        rc.refs[volume][ref] = struct{}{}
    }
    
    func (rc *refCounter) HasRef(volume string) bool {
        return len(rc.refs[volume]) > 0
    }
    

**3. 卷驱动**

- **Local 驱动**
  - 数据存储:`/var/lib/docker/volumes/<volume-name>/_data`
  - 元数据:`/var/lib/docker/volumes/<volume-name>/_meta.json`
  - 支持配额限制(Linux quota
  - 支持用户命名空间映射

- **插件驱动**
  - 通过 HTTP/gRPC 与外部驱动通信
  - 支持远程存储(NFS/Ceph/EBS/Azure Disk
  - 驱动能力查询(Scope: local/global
  - 驱动配置传递

---

## 卷创建与挂载时序图

```mermaid
sequenceDiagram
    autonumber
    participant Client as docker CLI
    participant Router as Volume Router
    participant Service as VolumesService
    participant Store as VolumeStore
    participant Driver as Local Driver
    participant FS as 文件系统
    
    Note over Client: docker volume create mydata
    Client->>Router: POST /volumes/create
    Router->>Service: Create(name, driver, options)
    
    Note over Service: 阶段1: 验证参数
    Service->>Service: 验证卷名(字符限制)
    alt 名称为空
        Service->>Service: 生成随机 ID
        Service->>Service: 标记为匿名卷
    end
    
    Note over Service: 阶段2: 检查冲突
    Service->>Store: 加锁(卷名)
    Store->>Store: 检查卷是否已存在
    alt 卷已存在
        Store-->>Service: 返回已存在的卷
    end
    
    Note over Service: 阶段3: 创建驱动实例
    Service->>Store: GetDriver(driverName)
    Store->>Driver: 初始化驱动
    
    Note over Driver: 阶段4: 创建卷目录
    Store->>Driver: Create(name, opts)
    Driver->>Driver: 生成卷路径
    Note over Driver: /var/lib/docker/volumes/mydata
    
    Driver->>FS: MkdirAll(rootPath, 0o701)
    FS-->>Driver: 根目录创建成功
    
    Driver->>FS: MkdirAll(dataPath, 0o755)
    Note over FS: /var/lib/docker/volumes/mydata/_data
    FS-->>Driver: 数据目录创建成功
    
    alt 配置了选项(quota/size
        Driver->>Driver: 设置配额限制
        Driver->>FS: setQuota(path, size)
    end
    
    Note over Driver: 阶段5: 保存元数据
    Driver->>Driver: 创建 Volume 对象
    Driver->>FS: 写入 _meta.json
    Driver-->>Store: Volume 对象
    
    Note over Service: 阶段6: 注册卷
    Store->>Store: volumes[name] = volume
    Store->>Store: 持久化到 BoltDB
    Store->>Service: 触发卷创建事件
    Store->>Store: 解锁(卷名)
    
    Service-->>Router: Volume 详情
    Router-->>Client: 201 Created
    
    Note over Client: docker run -v mydata:/data nginx
    Note over Client: (容器创建过程省略)
    
    Note over Client: 容器启动时挂载卷
    Client->>Service: Mount(volume, containerID)
    
    Note over Service: 阶段7: 获取卷对象
    Service->>Store: Get(volumeName)
    Store-->>Service: Volume 对象
    
    Note over Service: 阶段8: 增加引用计数
    Service->>Store: AddReference(volumeName, containerID)
    Store->>Store: refs[volumeName].Add(containerID)
    
    Note over Service: 阶段9: 挂载卷
    Service->>Driver: Mount(containerID)
    Driver->>Driver: mountCount++
    
    alt 首次挂载
        Driver->>FS: 确保数据目录可访问
        Driver->>Driver: 记录挂载引用
    end
    
    Driver-->>Service: 卷路径
    Note over Service: /var/lib/docker/volumes/mydata/_data
    
    Service-->>Client: 挂载成功,返回路径
    
    Note over Client: 容器启动,卷已挂载
    
    Note over Client: 容器停止
    Client->>Service: Unmount(volume, containerID)
    
    Note over Service: 阶段10: 卸载卷
    Service->>Driver: Unmount(containerID)
    Driver->>Driver: mountCount--
    
    alt mountCount == 0
        Driver->>Driver: 清理挂载状态
    end
    
    Driver-->>Service: 卸载成功
    
    Note over Service: 阶段11: 释放引用
    Service->>Store: RemoveReference(volumeName, containerID)
    Store->>Store: refs[volumeName].Remove(containerID)
    
    Service-->>Client: 卸载成功

时序图关键点说明

阶段1-2:验证与冲突检查(5-10ms)

  • 卷名验证:
    • 长度限制:1-255 字符
    • 字符限制:字母、数字、-_.
    • Windows:不允许 :\
  • 匿名卷:
    • 未指定名称时自动生成 64 位十六进制 ID
    • 添加标签 com.docker.volume.anonymous=""
    • 容器删除时自动清理

阶段3:驱动选择(<1ms)

// 驱动优先级
if driverName == "" {
    driverName = "local"  // 默认驱动
}

driver, err := s.drivers.CreateDriver(driverName)

阶段4:创建卷目录(10-30ms)

# Local 驱动的目录结构
/var/lib/docker/volumes/mydata/
├── _data/           # 实际数据目录(容器挂载点)
└── _meta.json       # 元数据(驱动、选项、创建时间)

_meta.json 内容示例

{
  "Driver": "local",
  "Labels": {},
  "Options": {},
  "CreatedAt": "2023-01-01T00:00:00Z"
}

阶段5-6:元数据持久化(5-15ms)

// VolumeStore 持久化
func (s *VolumeStore) save(v volume.Volume) error {
    return s.db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte("volumes"))
        data := marshalVolume(v)
        return b.Put([]byte(v.Name()), data)
    })
}

阶段7-9:挂载卷(20-50ms)

  • 引用计数:

    // 防止正在使用的卷被删除
    type volumeMetadata struct {
        volume volume.Volume
        refs   map[string]struct{}  // containerID -> struct{}
    }
    
    func (vm *volumeMetadata) HasRefs() bool {
        return len(vm.refs) > 0
    }
    

- 挂载流程:
  1. 获取卷对象(从 Store)
  2. 增加引用计数(volumeName -> containerID)
  3. 调用驱动的 Mount 方法(返回宿主机路径)
  4. 容器启动时 bind mount 到容器内

**阶段10-11:卸载与引用释放(5-10ms)**

- 卸载条件:
  - 容器停止时自动卸载
  - 引用计数递减
  - 当引用计数为 0 时,卷可被删除

---

## 卷类型对比

| 特性 | 命名卷 | 匿名卷 | Bind Mount | Tmpfs Mount |
|---|---|---|---|---|
| **创建方式** | `docker volume create` | `-v /data` | `-v /host:/data` | `--tmpfs /data` |
| **持久化** | ✓ | ✓ | ✓ | ✗(内存中) |
| **容器删除后保留** | ✓ | ✗ | ✓ | ✗ |
| **权限管理** | Docker 管理 | Docker 管理 | 宿主机权限 | 容器内权限 |
| **跨容器共享** | ✓ | ✗ | ✓ | ✗ |
| **驱动支持** | ✓ | ✓ | ✗ | ✗ |
| **备份迁移** | 易 | 易 | 难 | 不可用 |
| **用例** | 生产环境数据 | 临时数据 | 开发环境 | 临时缓存 |

---

## 卷驱动插件机制

### 插件通信协议

```mermaid
sequenceDiagram
    autonumber
    participant Docker as dockerd
    participant Plugin as 卷插件
    
    Note over Docker: 发现插件
    Docker->>Plugin: GET /Plugin.Activate
    Plugin-->>Docker: {"Implements": ["VolumeDriver"]}
    
    Note over Docker: 创建卷
    Docker->>Plugin: POST /VolumeDriver.Create
    Note over Docker: {"Name": "myvol", "Opts": {...}}
    Plugin->>Plugin: 创建远程存储
    Plugin-->>Docker: {"Err": ""}
    
    Note over Docker: 挂载卷
    Docker->>Plugin: POST /VolumeDriver.Mount
    Note over Docker: {"Name": "myvol", "ID": "container-id"}
    Plugin->>Plugin: 挂载远程存储到本地
    Plugin-->>Docker: {"Mountpoint": "/mnt/myvol", "Err": ""}
    
    Note over Docker: 获取卷路径
    Docker->>Plugin: POST /VolumeDriver.Path
    Plugin-->>Docker: {"Mountpoint": "/mnt/myvol", "Err": ""}
    
    Note over Docker: 卸载卷
    Docker->>Plugin: POST /VolumeDriver.Unmount
    Note over Docker: {"Name": "myvol", "ID": "container-id"}
    Plugin->>Plugin: 卸载远程存储
    Plugin-->>Docker: {"Err": ""}
    
    Note over Docker: 删除卷
    Docker->>Plugin: POST /VolumeDriver.Remove
    Plugin->>Plugin: 删除远程存储
    Plugin-->>Docker: {"Err": ""}

插件接口定义

// VolumeDriver 插件接口
type VolumeDriver interface {
    // Create 创建卷
    Create(req *CreateRequest) error
    
    // Remove 删除卷
    Remove(req *RemoveRequest) error
    
    // Mount 挂载卷,返回挂载点路径
    Mount(req *MountRequest) (*MountResponse, error)
    
    // Unmount 卸载卷
    Unmount(req *UnmountRequest) error
    
    // Get 获取卷详情
    Get(req *GetRequest) (*GetResponse, error)
    
    // List 列出所有卷
    List() (*ListResponse, error)
    
    // Path 获取卷在宿主机的路径
    Path(req *PathRequest) (*PathResponse, error)
    
    // Capabilities 查询驱动能力
    Capabilities() (*CapabilitiesResponse, error)
}

插件能力

type CapabilitiesResponse struct {
    Capabilities Capability
}

type Capability struct {
    // Scope 卷的作用域
    // - local: 仅本地节点可访问
    // - global: 集群所有节点可访问
    Scope string  // "local" | "global"
}

性能优化

卷引用计数优化

// 使用 sync.Map 减少锁竞争
type refCounter struct {
    refs sync.Map  // volumeName -> sync.Map(refID -> struct{})
}

func (rc *refCounter) Add(volume, ref string) {
    refs, _ := rc.refs.LoadOrStore(volume, &sync.Map{})
    refs.(*sync.Map).Store(ref, struct{}{})
}

func (rc *refCounter) HasRef(volume string) bool {
    refs, ok := rc.refs.Load(volume)
    if !ok {
        return false
    }
    
    hasRef := false
    refs.(*sync.Map).Range(func(_, _ interface{}) bool {
        hasRef = true
        return false  // 找到一个引用即可停止
    })
    return hasRef
}

卷元数据缓存

// 缓存卷元数据,避免频繁读取文件系统
type volumeCache struct {
    cache map[string]*cachedVolume
    mu    sync.RWMutex
    ttl   time.Duration
}

type cachedVolume struct {
    volume    volume.Volume
    expiresAt time.Time
}

func (vc *volumeCache) Get(name string) (volume.Volume, bool) {
    vc.mu.RLock()
    defer vc.mu.RUnlock()
    
    if cv, ok := vc.cache[name]; ok {
        if time.Now().Before(cv.expiresAt) {
            return cv.volume, true
        }
    }
    return nil, false
}

并发挂载优化

// 使用 singleflight 避免重复挂载
type mountManager struct {
    group singleflight.Group
}

func (mm *mountManager) Mount(ctx context.Context, vol volume.Volume, ref string) (string, error) {
    key := vol.Name() + ":" + ref
    
    result, err, _ := mm.group.Do(key, func() (interface{}, error) {
        return vol.Mount(ref)
    })
    
    if err != nil {
        return "", err
    }
    return result.(string), nil
}

最佳实践

命名卷 vs 匿名卷

# 推荐:命名卷(持久化、易管理)
docker volume create mydata
docker run -v mydata:/data nginx

# 不推荐:匿名卷(容器删除后丢失)
docker run -v /data nginx

卷备份与恢复

# 备份卷数据
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/mydata.tar.gz -C /data .

# 恢复卷数据
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/mydata.tar.gz -C /data

卷迁移

# 方式1: 使用 docker volume export/import(需插件支持)
docker volume export mydata > mydata.tar
docker volume import mydata < mydata.tar

# 方式2: 使用容器复制数据
docker run --rm \
  -v old_volume:/from \
  -v new_volume:/to \
  alpine sh -c "cp -av /from/. /to"

卷清理

# 删除未使用的卷
docker volume prune

# 强制删除卷(即使有容器引用)
docker volume rm -f mydata

# 查看卷占用空间
docker system df -v

插件驱动使用

# 安装 NFS 驱动插件
docker plugin install vieux/sshfs

# 创建 NFS 卷
docker volume create \
  --driver vieux/sshfs \
  --opt sshcmd=user@host:/path \
  --opt password=secret \
  nfs-volume

# 使用远程卷
docker run -v nfs-volume:/data nginx

性能优化

# 使用 bind mount(避免卷层开销)
docker run -v /host/data:/data nginx

# 使用 tmpfs(内存文件系统,极高性能)
docker run --tmpfs /tmp:rw,size=1g nginx

# 使用卷的 nocopy 选项(跳过初始化复制)
docker run -v mydata:/data:nocopy nginx

安全加固

# 只读卷
docker run -v mydata:/data:ro nginx

# 使用卷标签管理
docker volume create \
  --label env=production \
  --label app=web \
  mydata

# 限制卷大小(需驱动支持)
docker volume create \
  --opt size=10G \
  mydata

故障排查

卷挂载失败

# 检查卷是否存在
docker volume inspect mydata

# 检查卷驱动是否可用
docker plugin ls | grep volume

# 查看卷挂载点权限
ls -la /var/lib/docker/volumes/mydata/_data

# 检查容器日志
docker logs <container-id>

卷无法删除

# 查看卷引用(哪些容器正在使用)
docker ps -a --filter volume=mydata

# 停止并删除引用容器
docker rm -f $(docker ps -aq --filter volume=mydata)

# 强制删除卷
docker volume rm -f mydata

卷数据丢失

# 检查卷数据是否在磁盘上
ls -la /var/lib/docker/volumes/

# 检查 BoltDB 元数据
strings /var/lib/docker/volumes/metadata.db | grep mydata

# 恢复卷元数据
docker volume create --name mydata
# 手动复制数据到 /var/lib/docker/volumes/mydata/_data

API接口

本文档详细描述 Volume 模块对外提供的 HTTP API 接口,包括请求/响应结构、核心代码、调用链路与时序图。


API 目录

序号 API 方法 路径 说明
1 列出所有卷 GET /volumes 获取卷列表(支持过滤器)
2 获取卷详情 GET /volumes/{name:.*} 获取指定卷的详细信息
3 创建卷 POST /volumes/create 创建新卷(本地或集群)
4 更新卷 PUT /volumes/{name:.*} 更新集群卷配置(v1.42+)
5 删除卷 DELETE /volumes/{name:.*} 删除卷
6 清理卷 POST /volumes/prune 清理未使用的卷(v1.25+)

1. 列出所有卷

基本信息

  • 路径GET /volumes
  • 用途:获取所有卷的列表
  • 最小 API 版本:v1.24
  • 幂等性:是

请求参数

Query 参数

参数 类型 必填 说明
filters string JSON 编码的过滤器

过滤器选项

过滤器 说明 示例
dangling 未使用的卷 {"dangling":["true"]}
name 卷名称(部分匹配) {"name":["myvol"]}
driver 驱动类型 {"driver":["local"]}
label 标签过滤 {"label":["env=prod"]}

响应结构体

type ListResponse struct {
    // Volumes 卷列表
    Volumes []*Volume `json:"Volumes"`
    
    // Warnings 警告信息(例如驱动错误)
    Warnings []string `json:"Warnings"`
}

type Volume struct {
    // Name 卷名称
    Name string `json:"Name"`
    
    // Driver 驱动名称
    Driver string `json:"Driver"`
    
    // Mountpoint 挂载点路径
    Mountpoint string `json:"Mountpoint"`
    
    // CreatedAt 创建时间
    CreatedAt string `json:"CreatedAt,omitempty"`
    
    // Status 驱动状态(驱动特定)
    Status map[string]interface{} `json:"Status,omitempty"`
    
    // Labels 标签
    Labels map[string]string `json:"Labels"`
    
    // Scope 作用域(local/global)
    Scope string `json:"Scope"`
    
    // Options 驱动选项
    Options map[string]string `json:"Options"`
    
    // UsageData 使用情况
    UsageData *UsageData `json:"UsageData,omitempty"`
    
    // ClusterVolume 集群卷信息(v1.42+)
    ClusterVolume *ClusterVolume `json:"ClusterVolume,omitempty"`
}

入口函数与核心代码

HTTP Handlerdaemon/server/router/volume/volume_routes.go):

func (v *volumeRouter) getVolumesList(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    // 1. 解析过滤器
    f, err := filters.FromJSON(r.Form.Get("filters"))
    
    // 2. 获取本地卷列表
    volumes, warnings, err := v.backend.List(ctx, f)
    
    // 3. 如果是 Swarm Manager,追加集群卷
    version := httputils.VersionFromContext(ctx)
    if versions.GreaterThanOrEqualTo(version, "1.42") && v.cluster.IsManager() {
        clusterVolumes, swarmErr := v.cluster.GetVolumes(volumebackend.ListOptions{Filters: f})
        if swarmErr != nil {
            warnings = append(warnings, swarmErr.Error())
        }
        volumes = append(volumes, clusterVolumes...)
    }
    
    // 4. 返回响应
    return httputils.WriteJSON(w, http.StatusOK, &volume.ListResponse{
        Volumes:  volumes,
        Warnings: warnings,
    })
}

Backend 实现daemon/volume/service/service.go):

func (s *VolumesService) List(ctx context.Context, filter filters.Args) ([]*volumetypes.Volume, []string, error) {
    // 1. 转换过滤器
    by, err := filtersToBy(filter, acceptedListFilters)
    
    // 2. 从 VolumeStore 查找
    vols, warns, err := s.vs.Find(ctx, by)
    
    // 3. 转换为 API 类型
    return s.volumesToAPI(ctx, vols, useCachedPath(true)), warns, nil
}

VolumeStore 查找daemon/volume/service/store.go):

func (s *VolumeStore) Find(ctx context.Context, by By) ([]*volumeWrapper, []string, error) {
    s.locks.Lock()
    defer s.locks.Unlock()
    
    // 1. 过滤卷列表
    var volumes []*volumeWrapper
    var warnings []string
    
    for _, v := range s.vols {
        // 应用过滤器(名称/驱动/标签/dangling)
        if by.Name != "" && !strings.Contains(v.Name(), by.Name) {
            continue
        }
        if by.Driver != "" && v.DriverName() != by.Driver {
            continue
        }
        // ... 其他过滤逻辑
        
        volumes = append(volumes, v)
    }
    
    return volumes, warnings, nil
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Backend as VolumesService
    participant Store as VolumeStore
    participant Cluster as SwarmCluster
    
    Client->>Router: GET /volumes?filters={...}
    Router->>Router: 解析过滤器
    Router->>Backend: List(ctx, filters)
    Backend->>Store: Find(ctx, by)
    Store->>Store: 遍历 s.vols 映射
    Store->>Store: 应用过滤器
    Store-->>Backend: [volumes], warnings
    Backend-->>Router: [volumes], warnings
    
    alt API >= 1.42 且 Swarm Manager
        Router->>Cluster: GetVolumes(listOptions)
        Cluster-->>Router: clusterVolumes
        Router->>Router: 合并本地与集群卷
    end
    
    Router-->>Client: 200 OK<br/>{Volumes, Warnings}

异常与性能

异常场景

  • 过滤器格式错误:返回 400 Bad Request
  • 驱动故障:记录警告但不阻塞列表

性能指标

  • 平均响应时间:10-50ms
  • 优化:内存缓存所有卷对象

2. 获取卷详情

基本信息

  • 路径GET /volumes/{name:.*}
  • 用途:获取指定卷的详细信息
  • 最小 API 版本:v1.24
  • 幂等性:是

请求参数

路径参数

参数 类型 说明
name string 卷名称或 ID(集群卷)

响应结构体

列出所有卷,但返回单个 Volume 对象。

入口函数与核心代码

HTTP Handler

func (v *volumeRouter) getVolumeByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    version := httputils.VersionFromContext(ctx)
    
    // 1. 优先从本地查找
    vol, err := v.backend.Get(ctx, vars["name"], opts.WithGetResolveStatus)
    
    // 2. 本地未找到,尝试从 Swarm 集群查找(v1.42+)
    if cerrdefs.IsNotFound(err) &&
       versions.GreaterThanOrEqualTo(version, "1.42") &&
       v.cluster.IsManager() {
        swarmVol, err := v.cluster.GetVolume(vars["name"])
        vol = &swarmVol
    }
    
    // 3. 返回卷信息
    return httputils.WriteJSON(w, http.StatusOK, vol)
}

Backend 实现

func (s *VolumesService) Get(ctx context.Context, name string, getOpts ...opts.GetOption) (*volumetypes.Volume, error) {
    // 1. 从 VolumeStore 获取
    v, err := s.vs.Get(ctx, name, getOpts...)
    
    // 2. 转换为 API 类型
    vol := volumeToAPIType(v)
    
    // 3. 如果需要状态信息,调用驱动
    var cfg opts.GetConfig
    for _, o := range getOpts {
        o(&cfg)
    }
    if cfg.ResolveStatus {
        vol.Status = v.Status()  // 调用驱动的 Status() 方法
    }
    
    return &vol, nil
}

VolumeStore 获取

func (s *VolumeStore) Get(ctx context.Context, name string, opts ...opts.GetOption) (volume.Volume, error) {
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    // 1. 从内存缓存获取
    v, exists := s.vols[name]
    if !exists {
        return nil, &OpErr{Op: "get", Name: name, Err: ErrNoSuchVolume}
    }
    
    return v, nil
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Backend as VolumesService
    participant Store as VolumeStore
    participant Driver as Volume Driver
    participant Cluster as SwarmCluster
    
    Client->>Router: GET /volumes/myvol
    Router->>Backend: Get(ctx, "myvol", WithResolveStatus)
    Backend->>Store: Get(ctx, "myvol")
    Store->>Store: 查找 s.vols["myvol"]
    
    alt 卷存在
        Store-->>Backend: volumeWrapper
        Backend->>Driver: Status()
        Driver-->>Backend: statusMap
        Backend-->>Router: Volume{Status}
        Router-->>Client: 200 OK<br/>{Volume}
    else 卷不存在(本地)
        Store-->>Backend: ErrNoSuchVolume
        alt API >= 1.42 且 Swarm Manager
            Router->>Cluster: GetVolume("myvol")
            Cluster-->>Router: swarmVolume
            Router-->>Client: 200 OK<br/>{ClusterVolume}
        else
            Router-->>Client: 404 Not Found
        end
    end

异常与性能

异常场景

  • 卷不存在:404 Not Found
  • 驱动无响应:Status 字段为空

性能指标

  • 平均响应时间:5-20ms

3. 创建卷

基本信息

  • 路径POST /volumes/create
  • 用途:创建新卷
  • 最小 API 版本:v1.24
  • 幂等性:否(重复创建同名卷会返回已存在的卷)

请求结构体

type CreateOptions struct {
    // Name 卷名称(可选,留空则生成随机名)
    Name string `json:"Name"`
    
    // Driver 驱动名称(默认 "local")
    Driver string `json:"Driver"`
    
    // DriverOpts 驱动选项
    DriverOpts map[string]string `json:"DriverOpts"`
    
    // Labels 标签
    Labels map[string]string `json:"Labels"`
    
    // ClusterVolumeSpec 集群卷规格(v1.42+)
    ClusterVolumeSpec *ClusterVolumeSpec `json:"ClusterVolumeSpec,omitempty"`
}
字段 类型 必填 默认 说明
Name string 随机ID 卷名称
Driver string local 驱动类型
DriverOpts map {} 驱动特定选项
Labels map {} 标签
ClusterVolumeSpec object nil 集群卷配置

DriverOpts 示例(local 驱动)

{
    "type": "nfs",
    "o": "addr=192.168.1.100,rw",
    "device": ":/path/to/dir"
}

响应结构体

列出所有卷,返回创建的 Volume 对象。

入口函数与核心代码

HTTP Handler

func (v *volumeRouter) postVolumesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    // 1. 解析请求
    var req volume.CreateOptions
    if err := httputils.ReadJSON(r, &req); err != nil {
        return err
    }
    
    version := httputils.VersionFromContext(ctx)
    
    // 2. 判断是否为集群卷
    var vol *volume.Volume
    var err error
    
    if req.ClusterVolumeSpec != nil && versions.GreaterThanOrEqualTo(version, "1.42") {
        // 集群卷:通过 Swarm 创建
        vol, err = v.cluster.CreateVolume(req)
    } else {
        // 本地卷:通过 VolumesService 创建
        vol, err = v.backend.Create(ctx, req.Name, req.Driver,
            opts.WithCreateOptions(req.DriverOpts),
            opts.WithCreateLabels(req.Labels))
    }
    
    // 3. 返回创建的卷
    return httputils.WriteJSON(w, http.StatusCreated, vol)
}

Backend 实现

func (s *VolumesService) Create(ctx context.Context, name, driverName string, options ...opts.CreateOption) (*volumetypes.Volume, error) {
    // 1. 生成匿名卷名称
    if name == "" {
        name = stringid.GenerateRandomID()
        if driverName == "" {
            driverName = volume.DefaultDriverName
        }
        options = append(options, opts.WithCreateLabel(AnonymousLabel, ""))
    }
    
    // 2. 调用 VolumeStore 创建
    v, err := s.vs.Create(ctx, name, driverName, options...)
    
    // 3. 转换为 API 类型
    apiV := volumeToAPIType(v)
    return &apiV, nil
}

VolumeStore 创建daemon/volume/service/store.go):

func (s *VolumeStore) Create(ctx context.Context, name, driverName string, opts ...opts.CreateOption) (volume.Volume, error) {
    // 1. 应用选项
    var cfg opts.CreateConfig
    for _, o := range opts {
        o(&cfg)
    }
    
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    // 2. 检查卷是否已存在
    if v, exists := s.vols[name]; exists {
        if v.DriverName() != driverName {
            return nil, &OpErr{Op: "create", Name: name, Err: ErrVolumeExists}
        }
        return v, nil  // 幂等返回
    }
    
    // 3. 获取驱动
    if driverName == "" {
        driverName = volume.DefaultDriverName
    }
    driver, err := volumedrivers.GetDriver(driverName)
    
    // 4. 调用驱动创建卷
    v, err := driver.Create(name, cfg.Options)
    
    // 5. 包装并缓存
    vw := &volumeWrapper{
        name:        name,
        driverName:  driverName,
        labels:      cfg.Labels,
        v:           v,
        options:     cfg.Options,
    }
    s.vols[name] = vw
    
    // 6. 持久化元数据
    s.setMeta(name, volumeMetadata{
        Name:    name,
        Driver:  driverName,
        Labels:  cfg.Labels,
        Options: cfg.Options,
    })
    
    // 7. 记录事件
    s.eventLogger.LogVolumeEvent(name, events.ActionCreate, attributes)
    
    return vw, nil
}

Local 驱动创建daemon/volume/local/local.go):

func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) {
    // 1. 验证卷名称
    if err := r.validateName(name); err != nil {
        return nil, err
    }
    
    r.m.Lock()
    defer r.m.Unlock()
    
    // 2. 创建卷目录
    path := r.DataPath(name)
    if err := os.MkdirAll(path, 0755); err != nil {
        return nil, err
    }
    
    // 3. 应用挂载选项(NFS/CIFS 等)
    v := &localVolume{
        name:       name,
        path:       path,
        opts:       opts,
    }
    
    // 4. 如果有挂载选项,执行挂载
    if optsConfig != nil {
        if err := v.mount(); err != nil {
            return nil, err
        }
    }
    
    return v, nil
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Backend as VolumesService
    participant Store as VolumeStore
    participant Driver as Volume Driver
    participant FS as Filesystem
    participant DB as BoltDB
    
    Client->>Router: POST /volumes/create<br/>{Name, Driver, DriverOpts}
    Router->>Router: 解析 CreateOptions
    
    alt 集群卷(ClusterVolumeSpec != nil
        Router->>Router: cluster.CreateVolume()
        Note right of Router: Swarm 集群卷创建流程
    else 本地卷
        Router->>Backend: Create(ctx, name, driver, opts)
        Backend->>Store: Create(ctx, name, driver, opts)
        
        Store->>Store: 检查卷是否已存在
        alt 卷已存在
            Store-->>Backend: 返回已存在的卷(幂等)
        else 卷不存在
            Store->>Driver: GetDriver(driverName)
            Store->>Driver: Create(name, opts)
            
            Driver->>FS: MkdirAll(/var/lib/docker/volumes/name)
            FS-->>Driver: ok
            
            alt 有挂载选项(NFS/CIFS
                Driver->>FS: mount -t nfs ...
                FS-->>Driver: ok
            end
            
            Driver-->>Store: volume
            
            Store->>Store: 包装为 volumeWrapper
            Store->>Store: 缓存到 s.vols[name]
            Store->>DB: setMeta(name, metadata)
            DB-->>Store: ok
            
            Store->>Store: LogVolumeEvent("create")
            Store-->>Backend: volumeWrapper
        end
        
        Backend-->>Router: Volume
        Router-->>Client: 201 Created<br/>{Volume}
    end

异常与性能

异常场景

  • 驱动不存在:404 Plugin not found
  • 挂载失败(NFS):500 Internal Server Error
  • 磁盘空间不足:507 Insufficient Storage

性能指标

  • 本地卷平均创建时间:10-30ms
  • NFS 卷平均创建时间:50-200ms(取决于网络)

4. 更新卷

基本信息

  • 路径PUT /volumes/{name:.*}
  • 用途:更新集群卷配置
  • 最小 API 版本:v1.42
  • 幂等性:是
  • 限制:仅支持 Swarm 集群卷

请求参数

路径参数

参数 类型 说明
name string 卷名称或 ID

Query 参数

参数 类型 必填 说明
version uint64 卷的 Swarm 对象版本号

请求结构体

type UpdateOptions struct {
    // Spec 新的集群卷规格
    Spec *ClusterVolumeSpec `json:"Spec,omitempty"`
}

入口函数与核心代码

HTTP Handler

func (v *volumeRouter) putVolumesUpdate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    // 1. 检查 Swarm 状态
    if !v.cluster.IsManager() {
        return errdefs.Unavailable(errors.New("volume update only valid for cluster volumes"))
    }
    
    // 2. 解析版本号
    rawVersion := r.URL.Query().Get("version")
    version, err := strconv.ParseUint(rawVersion, 10, 64)
    
    // 3. 解析更新选项
    var req volume.UpdateOptions
    if err := httputils.ReadJSON(r, &req); err != nil {
        return err
    }
    
    // 4. 调用 Swarm 更新
    return v.cluster.UpdateVolume(vars["name"], version, req)
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Cluster as SwarmCluster
    participant Raft as Raft Store
    
    Client->>Router: PUT /volumes/myvol?version=10<br/>{Spec}
    Router->>Router: 检查 IsManager()
    Router->>Router: 解析 version
    Router->>Router: 解析 UpdateOptions
    Router->>Cluster: UpdateVolume(name, version, opts)
    Cluster->>Raft: 更新集群卷对象
    Raft-->>Cluster: ok
    Cluster-->>Router: ok
    Router-->>Client: 200 OK

异常与性能

异常场景

  • 非 Manager 节点:503 Service Unavailable
  • 版本不匹配:409 Conflict
  • 卷不存在:404 Not Found

5. 删除卷

基本信息

  • 路径DELETE /volumes/{name:.*}
  • 用途:删除卷
  • 最小 API 版本:v1.24
  • 幂等性:是(force=true)

请求参数

路径参数

参数 类型 说明
name string 卷名称

Query 参数

参数 类型 默认 说明
force bool false 强制删除(忽略不存在错误)

入口函数与核心代码

HTTP Handler

func (v *volumeRouter) deleteVolumes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    force := httputils.BoolValue(r, "force")
    
    // 1. 尝试删除本地卷
    err := v.backend.Remove(ctx, vars["name"], opts.WithPurgeOnError(force))
    
    // 2. 如果本地未找到,尝试删除集群卷
    if cerrdefs.IsNotFound(err) || force {
        version := httputils.VersionFromContext(ctx)
        if versions.GreaterThanOrEqualTo(version, "1.42") && v.cluster.IsManager() {
            err = v.cluster.RemoveVolume(vars["name"], force)
        }
    }
    
    if err != nil {
        return err
    }
    w.WriteHeader(http.StatusNoContent)
    return nil
}

Backend 实现

func (s *VolumesService) Remove(ctx context.Context, name string, rmOpts ...opts.RemoveOption) error {
    var cfg opts.RemoveConfig
    for _, o := range rmOpts {
        o(&cfg)
    }
    
    // 1. 获取卷
    v, err := s.vs.Get(ctx, name)
    if err != nil {
        if IsNotExist(err) && cfg.PurgeOnError {
            return nil  // force 模式忽略不存在
        }
        return err
    }
    
    // 2. 调用 VolumeStore 删除
    err = s.vs.Remove(ctx, v, rmOpts...)
    if IsInUse(err) {
        err = errdefs.Conflict(err)
    }
    return err
}

VolumeStore 删除

func (s *VolumeStore) Remove(ctx context.Context, v volume.Volume, opts ...opts.RemoveOption) error {
    name := v.Name()
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    // 1. 检查引用计数
    vw, exists := s.vols[name]
    if vw.refCount() > 0 {
        return &OpErr{Op: "remove", Name: name, Err: ErrVolumeInUse}
    }
    
    // 2. 调用驱动删除
    if err := vw.getVolume().Remove(); err != nil {
        return err
    }
    
    // 3. 从内存删除
    delete(s.vols, name)
    
    // 4. 从数据库删除
    s.removeMeta(name)
    
    // 5. 记录事件
    s.eventLogger.LogVolumeEvent(name, events.ActionDestroy, nil)
    
    return nil
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Backend as VolumesService
    participant Store as VolumeStore
    participant Driver as Volume Driver
    participant FS as Filesystem
    participant DB as BoltDB
    
    Client->>Router: DELETE /volumes/myvol?force=false
    Router->>Backend: Remove(ctx, "myvol")
    Backend->>Store: Get(ctx, "myvol")
    Store-->>Backend: volumeWrapper
    
    Backend->>Store: Remove(ctx, volumeWrapper)
    Store->>Store: 检查引用计数
    
    alt refCount > 0
        Store-->>Backend: ErrVolumeInUse
        Backend-->>Router: 409 Conflict
        Router-->>Client: 409 Conflict:<br/>volume is in use
    else refCount == 0
        Store->>Driver: Remove()
        Driver->>FS: rmdir /var/lib/docker/volumes/myvol
        FS-->>Driver: ok
        Driver-->>Store: ok
        
        Store->>Store: delete(s.vols, name)
        Store->>DB: removeMeta(name)
        DB-->>Store: ok
        
        Store->>Store: LogVolumeEvent("destroy")
        Store-->>Backend: ok
        Backend-->>Router: ok
        Router-->>Client: 204 No Content
    end

异常与性能

异常场景

  • 卷不存在:404 Not Found(force=false)
  • 卷正在使用:409 Conflict
  • 驱动删除失败:500 Internal Server Error

性能指标

  • 平均删除时间:10-50ms

6. 清理卷

基本信息

  • 路径POST /volumes/prune
  • 用途:删除未使用的卷
  • 最小 API 版本:v1.25
  • 幂等性:是

请求参数

Query 参数

参数 类型 必填 说明
filters string JSON 编码的过滤器

过滤器选项

过滤器 说明
label 标签过滤
label! 标签排除
all 清理所有未使用卷(v1.42+默认仅匿名卷)

响应结构体

type PruneReport struct {
    // VolumesDeleted 已删除的卷名称列表
    VolumesDeleted []string `json:"VolumesDeleted"`
    
    // SpaceReclaimed 回收的磁盘空间(字节)
    SpaceReclaimed uint64 `json:"SpaceReclaimed"`
}

入口函数与核心代码

HTTP Handler

func (v *volumeRouter) postVolumesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    // 1. 解析过滤器
    pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
    
    // 2. API < 1.42 时,默认清理所有卷(兼容旧版本)
    if versions.LessThan(httputils.VersionFromContext(ctx), "1.42") {
        pruneFilters.Add("all", "true")
    }
    
    // 3. 执行清理
    pruneReport, err := v.backend.Prune(ctx, pruneFilters)
    
    return httputils.WriteJSON(w, http.StatusOK, pruneReport)
}

Backend 实现daemon/volume/service/service.go):

func (s *VolumesService) Prune(ctx context.Context, filter filters.Args) (*volumetypes.PruneReport, error) {
    // 1. 防止并发清理
    if !s.pruneRunning.CompareAndSwap(false, true) {
        return nil, errdefs.Conflict(errors.New("a prune operation is already running"))
    }
    defer s.pruneRunning.Store(false)
    
    // 2. 转换过滤器
    by, err := filtersToBy(filter, acceptedPruneFilters)
    
    // 3. 查找符合条件的卷
    vols, _, err := s.vs.Find(ctx, by)
    
    // 4. 清理每个卷
    var (
        deleted []string
        spaceReclaimed uint64
    )
    
    for _, v := range vols {
        // 跳过正在使用的卷
        if v.refCount() > 0 {
            continue
        }
        
        // 如果不是 "all" 模式,只删除匿名卷
        if !by.All && v.labels[AnonymousLabel] == "" {
            continue
        }
        
        // 获取卷大小
        if du, err := v.CachedPath(); err == nil {
            spaceReclaimed += uint64(du)
        }
        
        // 删除卷
        if err := s.vs.Remove(ctx, v); err == nil {
            deleted = append(deleted, v.Name())
        }
    }
    
    return &volumetypes.PruneReport{
        VolumesDeleted: deleted,
        SpaceReclaimed: spaceReclaimed,
    }, nil
}

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant Router as volumeRouter
    participant Backend as VolumesService
    participant Store as VolumeStore
    
    Client->>Router: POST /volumes/prune<br/>?filters={"label":["env=test"]}
    Router->>Router: 解析过滤器
    Router->>Backend: Prune(ctx, filters)
    
    Backend->>Backend: 检查 pruneRunning 标志
    Backend->>Store: Find(ctx, by)
    Store-->>Backend: [volumes]
    
    loop 每个卷
        alt refCount == 0 && 匹配过滤器
            Backend->>Store: 计算卷大小
            Backend->>Store: Remove(ctx, volume)
            Store-->>Backend: ok
            Backend->>Backend: 累计已删除与空间
        else refCount > 0
            Note right of Backend: 跳过正在使用的卷
        end
    end
    
    Backend-->>Router: PruneReport
    Router-->>Client: 200 OK<br/>{VolumesDeleted, SpaceReclaimed}

异常与性能

异常场景

  • 并发清理:409 Conflict
  • 过滤器格式错误:400 Bad Request

性能指标

  • 清理 100 个卷:1-5 秒
  • 优化:并发删除(计划中)

附录:驱动选项参考

Local 驱动(NFS)

{
    "Driver": "local",
    "DriverOpts": {
        "type": "nfs",
        "o": "addr=192.168.1.100,vers=4,soft,timeo=180,bg,tcp,rw",
        "device": ":/exported/path"
    }
}

Local 驱动(CIFS/SMB)

{
    "Driver": "local",
    "DriverOpts": {
        "type": "cifs",
        "o": "username=user,password=pass,vers=3.0",
        "device": "//192.168.1.100/share"
    }
}

文档版本:v1.0
最后更新:2025-10-04


数据结构

本文档详细描述卷模块的核心数据结构,包括 UML 类图、字段说明、接口定义与使用场景。


数据结构概览

classDiagram
    class VolumesService {
        -*VolumeStore vs
        -*drivers.Store ds
        -atomic.Bool pruneRunning
        -VolumeEventLogger eventLogger
        +Create(ctx, name, driver, options) (*Volume, error)
        +Get(ctx, name, options) (*Volume, error)
        +List(ctx, filter) ([]*Volume, []string, error)
        +Remove(ctx, name, options) error
        +Prune(ctx, filter) (*PruneReport, error)
        +Mount(ctx, vol, ref) (string, error)
        +Unmount(ctx, vol, ref) error
        +Release(ctx, name, ref) error
    }
    
    class VolumeStore {
        -*locker.Locker locks
        -*drivers.Store drivers
        -map~string,Volume~ names
        -map~string,map~ refs
        -map~string,map~ labels
        -map~string,map~ options
        -*bolt.DB db
        -VolumeEventLogger eventLogger
        +Create(ctx, name, driver, opts) (Volume, error)
        +Get(ctx, name, opts) (Volume, error)
        +Find(ctx, by) ([]Volume, []string, error)
        +Remove(ctx, volume, opts) error
        +Release(ctx, name, ref) error
        +Acquire(name, ref) (Volume, error)
    }
    
    class Volume {
        <<interface>>
        +Name() string
        +DriverName() string
        +Path() string
        +Mount(id) (string, error)
        +Unmount(id) error
        +CreatedAt() (time.Time, error)
        +Status() map[string]any
    }
    
    class volumeWrapper {
        -Volume Volume
        -map~string,string~ labels
        -map~string,string~ options
        -string scope
        +Labels() map[string]string
        +Options() map[string]string
        +Scope() string
        +CachedPath() string
        +LiveRestoreVolume(ctx, ref) error
    }
    
    class Driver {
        <<interface>>
        +Name() string
        +Create(name, opts) (Volume, error)
        +Remove(vol) error
        +List() ([]Volume, error)
        +Get(name) (Volume, error)
        +Scope() string
    }
    
    class localVolume {
        -string name
        -string path
        -map~string,string~ opts
        -*quotaCtl quotaCtl
        -sync.Mutex mu
        -int mountCount
        +Name() string
        +Path() string
        +Mount(id) (string, error)
        +Unmount(id) error
        +Status() map[string]any
    }
    
    class volumeMetadata {
        +string Name
        +string Driver
        +map~string,string~ Labels
        +map~string,string~ Options
    }
    
    class driversStore {
        -map~string,Driver~ drivers
        -*plugingetter.PluginGetter pg
        -sync.RWMutex mu
        +CreateDriver(name) (Driver, error)
        +GetDriver(name) (Driver, error)
        +GetDriverList() []string
    }
    
    class By {
        +string Name
        +string Driver
        +[]string Labels
        +bool Dangling
        +bool All
    }
    
    VolumesService "1" *-- "1" VolumeStore : owns
    VolumeStore "1" *-- "*" volumeWrapper : manages
    volumeWrapper "1" *-- "1" Volume : wraps
    VolumeStore "1" --> "1" driversStore : uses
    driversStore "1" *-- "*" Driver : manages
    Driver ..> Volume : creates
    localVolume ..|> Volume : implements
    Volume <|-- localVolume

1. VolumesService(卷服务)

结构定义

type VolumesService struct {
    // 卷存储
    vs *VolumeStore // 核心卷存储管理器
    
    // 驱动列表
    ds driverLister // 驱动注册表
    
    // 清理状态
    pruneRunning atomic.Bool // 防止并发清理
    
    // 事件日志
    eventLogger VolumeEventLogger // 卷事件记录器
}

字段说明

字段 类型 说明
vs *VolumeStore 核心卷存储管理器
ds driverLister 驱动注册表(获取驱动列表)
pruneRunning atomic.Bool 清理操作标志(防止并发)
eventLogger VolumeEventLogger 事件记录器(记录创建/删除等事件)

核心方法

// 卷生命周期
func (s *VolumesService) Create(ctx context.Context, name, driverName string, options ...opts.CreateOption) (*volumetypes.Volume, error)
func (s *VolumesService) Get(ctx context.Context, name string, getOpts ...opts.GetOption) (*volumetypes.Volume, error)
func (s *VolumesService) List(ctx context.Context, filter filters.Args) ([]*volumetypes.Volume, []string, error)
func (s *VolumesService) Remove(ctx context.Context, name string, rmOpts ...opts.RemoveOption) error
func (s *VolumesService) Prune(ctx context.Context, filter filters.Args) (*volumetypes.PruneReport, error)

// 卷挂载
func (s *VolumesService) Mount(ctx context.Context, vol *volumetypes.Volume, ref string) (string, error)
func (s *VolumesService) Unmount(ctx context.Context, vol *volumetypes.Volume, ref string) error

// 引用计数
func (s *VolumesService) Release(ctx context.Context, name string, ref string) error

// 恢复
func (s *VolumesService) LiveRestoreVolume(ctx context.Context, vol *volumetypes.Volume, ref string) error

使用场景

// 创建卷服务
vs, err := service.NewVolumeService(
    "/var/lib/docker",
    pluginGetter,
    idtools.Identity{UID: 0, GID: 0},
    eventLogger,
)

// 创建卷
vol, err := vs.Create(ctx, "myvol", "local",
    opts.WithCreateLabel("env", "prod"),
    opts.WithCreateOptions(map[string]string{
        "type": "nfs",
        "o": "addr=192.168.1.100,rw",
        "device": ":/exported/path",
    }),
)

// 挂载卷
mountPath, err := vs.Mount(ctx, vol, "container-id")
// mountPath: /var/lib/docker/volumes/myvol/_data

// 卸载卷
err = vs.Unmount(ctx, vol, "container-id")

2. VolumeStore(卷存储)

结构定义

type VolumeStore struct {
    // 并发控制
    locks      *locker.Locker // 卷级锁(细粒度锁定)
    globalLock sync.RWMutex   // 全局锁(保护映射)
    
    // 驱动管理
    drivers *drivers.Store // 驱动存储
    
    // 卷映射
    names   map[string]volume.Volume       // 卷名称 → 卷对象
    refs    map[string]map[string]struct{} // 卷名称 → 引用集合
    labels  map[string]map[string]string   // 卷名称 → 标签
    options map[string]map[string]string   // 卷名称 → 选项
    
    // 持久化
    db *bolt.DB // BoltDB 数据库(存储元数据)
    
    // 事件日志
    eventLogger VolumeEventLogger
}

字段说明

字段 类型 说明
locks *locker.Locker 卷级锁(每个卷独立锁定)
globalLock sync.RWMutex 全局读写锁(保护映射访问)
drivers *drivers.Store 驱动注册表
names map 卷名称到卷对象的映射
refs map 卷引用计数(容器 ID 集合)
labels map 卷标签存储
options map 卷选项存储
db *bolt.DB 元数据持久化数据库

核心方法

// 卷操作
func (s *VolumeStore) Create(ctx context.Context, name, driverName string, opts ...opts.CreateOption) (volume.Volume, error)
func (s *VolumeStore) Get(ctx context.Context, name string, opts ...opts.GetOption) (volume.Volume, error)
func (s *VolumeStore) Find(ctx context.Context, by By) ([]volume.Volume, []string, error)
func (s *VolumeStore) Remove(ctx context.Context, v volume.Volume, opts ...opts.RemoveOption) error

// 引用管理
func (s *VolumeStore) Acquire(name, ref string) (volume.Volume, error)
func (s *VolumeStore) Release(ctx context.Context, name, ref string) error
func (s *VolumeStore) hasRef(name string) bool
func (s *VolumeStore) refs(name string) int

// 元数据持久化
func (s *VolumeStore) setMeta(name string, meta volumeMetadata) error
func (s *VolumeStore) getMeta(name string) (volumeMetadata, error)
func (s *VolumeStore) removeMeta(name string) error

// 恢复
func (s *VolumeStore) restore()

引用计数机制

// 引用计数示例
type VolumeStore struct {
    refs map[string]map[string]struct{} // volume-name → {ref1, ref2, ...}
}

// 获取卷并增加引用
func (s *VolumeStore) Acquire(name, ref string) (volume.Volume, error) {
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    v, exists := s.names[name]
    if !exists {
        return nil, ErrNoSuchVolume
    }
    
    // 添加引用
    s.globalLock.Lock()
    if s.refs[name] == nil {
        s.refs[name] = make(map[string]struct{})
    }
    s.refs[name][ref] = struct{}{}
    s.globalLock.Unlock()
    
    return v, nil
}

// 释放引用
func (s *VolumeStore) Release(ctx context.Context, name, ref string) error {
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    s.globalLock.Lock()
    if s.refs[name] != nil {
        delete(s.refs[name], ref)
        if len(s.refs[name]) == 0 {
            delete(s.refs, name)
        }
    }
    s.globalLock.Unlock()
    
    return nil
}

// 检查卷是否正在使用
func (s *VolumeStore) hasRef(name string) bool {
    s.globalLock.RLock()
    defer s.globalLock.RUnlock()
    return len(s.refs[name]) > 0
}

持久化存储

数据库路径/var/lib/docker/volumes/metadata.db

存储结构

BoltDB
└── volumes (bucket)
    ├── "myvol" → volumeMetadata (JSON)
    ├── "vol2"  → volumeMetadata (JSON)
    └── ...

volumeMetadata

type volumeMetadata struct {
    Name    string            // 卷名称
    Driver  string            // 驱动名称
    Labels  map[string]string // 标签
    Options map[string]string // 驱动选项
}

3. Volume 接口

接口定义

type Volume interface {
    // Name 返回卷名称
    Name() string
    
    // DriverName 返回驱动名称
    DriverName() string
    
    // Path 返回卷的绝对路径
    Path() string
    
    // Mount 挂载卷并返回挂载点
    // id: 挂载引用 ID(通常为容器 ID)
    Mount(id string) (string, error)
    
    // Unmount 卸载卷
    // id: 挂载引用 ID
    Unmount(id string) error
    
    // CreatedAt 返回卷创建时间
    CreatedAt() (time.Time, error)
    
    // Status 返回驱动特定的状态信息
    Status() map[string]any
}

扩展接口

// DetailedVolume 扩展接口(包含标签/选项/作用域)
type DetailedVolume interface {
    Volume
    Labels() map[string]string
    Options() map[string]string
    Scope() string
}

// LiveRestorer 卷恢复接口
type LiveRestorer interface {
    // LiveRestoreVolume 恢复卷资源(用于容器 live-restore)
    LiveRestoreVolume(ctx context.Context, ref string) error
}

实现类:volumeWrapper

type volumeWrapper struct {
    volume.Volume            // 嵌入原始卷对象
    labels        map[string]string // 用户标签
    options       map[string]string // 驱动选项
    scope         string            // 作用域(local/global)
}

func (v volumeWrapper) Labels() map[string]string {
    // 返回标签副本(防止修改)
    labels := make(map[string]string, len(v.labels))
    for key, value := range v.labels {
        labels[key] = value
    }
    return labels
}

func (v volumeWrapper) Scope() string {
    return v.scope
}

func (v volumeWrapper) CachedPath() string {
    // 优先返回缓存路径(避免重复计算)
    if vv, ok := v.Volume.(interface{ CachedPath() string }); ok {
        return vv.CachedPath()
    }
    return v.Volume.Path()
}

4. Driver 接口

接口定义

type Driver interface {
    // Name 返回驱动名称
    Name() string
    
    // Create 创建新卷
    Create(name string, opts map[string]string) (Volume, error)
    
    // Remove 删除卷
    Remove(vol Volume) error
    
    // List 列出所有卷
    List() ([]Volume, error)
    
    // Get 获取指定卷
    Get(name string) (Volume, error)
    
    // Scope 返回驱动作用域(local/global)
    Scope() string
}

Capability(驱动能力)

type Capability struct {
    // Scope 作用域
    // - local: 驱动仅管理本地资源
    // - global: 驱动管理集群范围资源
    Scope string
}

内置驱动:localVolume

type localVolume struct {
    // 基本信息
    name string // 卷名称
    path string // 卷路径
    
    // 挂载配置
    opts       map[string]string // 驱动选项(NFS/CIFS 配置)
    active     activeMount       // 活动挂载信息
    
    // 配额控制
    quotaCtl *quotaCtl // 磁盘配额管理
    
    // 挂载计数
    mu         sync.Mutex // 锁
    mountCount int        // 挂载计数(引用计数)
}

func (v *localVolume) Mount(id string) (string, error) {
    v.mu.Lock()
    defer v.mu.Unlock()
    
    // 增加挂载计数
    v.mountCount++
    
    // 首次挂载时执行实际挂载
    if v.mountCount == 1 {
        if err := v.mount(); err != nil {
            v.mountCount--
            return "", err
        }
    }
    
    return v.path, nil
}

func (v *localVolume) Unmount(id string) error {
    v.mu.Lock()
    defer v.mu.Unlock()
    
    // 减少挂载计数
    v.mountCount--
    
    // 最后一个引用释放时卸载
    if v.mountCount == 0 {
        return v.unmount()
    }
    
    return nil
}

5. 驱动管理

driversStore(驱动存储)

type driversStore struct {
    // 内置驱动
    drivers map[string]Driver // name → Driver
    
    // 插件驱动
    pg *plugingetter.PluginGetter // 插件获取器
    
    // 并发控制
    mu sync.RWMutex
}

func (s *driversStore) GetDriver(name string) (Driver, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    // 1. 查找内置驱动
    if drv, exists := s.drivers[name]; exists {
        return drv, nil
    }
    
    // 2. 查找插件驱动
    p, err := s.pg.Get(name, "VolumeDriver", plugingetter.Lookup)
    if err != nil {
        return nil, ErrNoSuchDriver
    }
    
    return NewPluginDriver(p), nil
}

驱动类型

驱动 类型 Scope 说明
local 内置 local 本地文件系统卷
nfs 插件 global NFS 网络卷
ceph 插件 global Ceph RBD 卷
glusterfs 插件 global GlusterFS 卷
convoy 插件 local 块设备卷

6. 查询与过滤

By 结构(查询条件)

type By struct {
    // 名称过滤(部分匹配)
    Name string
    
    // 驱动过滤
    Driver string
    
    // 标签过滤
    Labels []string
    
    // 是否 dangling(未使用)
    Dangling bool
    
    // 是否包含所有卷(默认仅匿名卷)
    All bool
}

过滤实现

func (s *VolumeStore) Find(ctx context.Context, by By) ([]volume.Volume, []string, error) {
    s.globalLock.RLock()
    defer s.globalLock.RUnlock()
    
    var volumes []volume.Volume
    var warnings []string
    
    for _, v := range s.names {
        // 名称过滤
        if by.Name != "" && !strings.Contains(v.Name(), by.Name) {
            continue
        }
        
        // 驱动过滤
        if by.Driver != "" && v.DriverName() != by.Driver {
            continue
        }
        
        // dangling 过滤
        if by.Dangling && s.hasRef(v.Name()) {
            continue
        }
        
        // 标签过滤
        if !matchLabels(s.labels[v.Name()], by.Labels) {
            continue
        }
        
        volumes = append(volumes, v)
    }
    
    return volumes, warnings, nil
}

数据结构关系图

整体架构

Daemon
  └── VolumesService
        ├── VolumeStore
        │     ├── names: map[string]Volume
        │     ├── refs:  map[string]map[string]struct{}
        │     ├── db:    BoltDB (metadata)
        │     └── drivers: driversStore
        │           ├── local: localVolume
        │           └── plugins: PluginDriver
        └── eventLogger: EventsService

卷生命周期


1. 创建阶段:
   Create()  VolumeStore.Create()
      Driver.Create()
      mkdir /var/lib/docker/volumes/{name}/_data
      setMeta(volumeMetadata)
      LogVolumeEvent("create")

2. 使用阶段:
   Acquire(ref)  refs[name][ref] = struct{}{}
   Mount(id)     volume.Mount(id)  mountCount++
   Unmount(id)   volume.Unmount(id)  mountCount--
   Release(ref)  delete(refs[name], ref)

3. 删除阶段:
   Remove()
      检查 hasRef() == false
      Driver.Remove()
      rmdir /var/lib/docker/volumes/{name}
      removeMeta(name)
      LogVolumeEvent("destroy")

使用场景

场景 1:创建本地卷

vs, _ := service.NewVolumeService(...)

// 创建简单本地卷
vol, err := vs.Create(ctx, "data", "local", nil)
// 路径:/var/lib/docker/volumes/data/_data

// 创建 NFS 卷
vol, err := vs.Create(ctx, "nfs-data", "local",
    opts.WithCreateOptions(map[string]string{
        "type":   "nfs",
        "o":      "addr=192.168.1.100,rw",
        "device": ":/exports/data",
    }),
)
// 挂载:mount -t nfs -o addr=... 192.168.1.100:/exports/data /var/lib/docker/volumes/nfs-data/_data

场景 2:容器使用卷

// 1. 容器启动时获取卷
vol, err := vs.Get(ctx, "data")

// 2. 挂载卷
mountPath, err := vs.Mount(ctx, vol, containerID)
// mountPath: /var/lib/docker/volumes/data/_data

// 3. 容器运行中...
// 4. 容器停止时卸载
err = vs.Unmount(ctx, vol, containerID)

// 5. 容器删除时释放引用
err = vs.Release(ctx, "data", containerID)

场景 3:清理未使用卷

// 清理所有匿名卷
report, err := vs.Prune(ctx, filters.Args{})
// report.VolumesDeleted: ["64f57...", "a3f21..."]
// report.SpaceReclaimed: 1024000000

// 清理特定标签的卷
report, err := vs.Prune(ctx, filters.NewArgs(
    filters.Arg("label", "env=test"),
))

存储目录结构

/var/lib/docker/
└── volumes/
    ├── metadata.db         # BoltDB 元数据库
    ├── myvol/              # 卷目录
       └── _data/          # 实际数据目录
           └── (用户文件)
    ├── nfs-vol/            # NFS 卷
       └── _data/          # NFS 挂载点
           └── (远程文件)
    └── anonymous-vol-64f57/  # 匿名卷
        └── _data/

文档版本:v1.0
最后更新:2025-10-04


时序图

本文档通过时序图展示卷模块的典型操作流程,包括卷创建、挂载、引用管理等关键场景。


时序图目录

  1. 卷创建流程
  2. 卷挂载与卸载流程
  3. 容器使用卷的完整流程
  4. 卷删除流程
  5. 卷清理流程
  6. NFS 卷挂载流程

1. 卷创建流程

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant API as volumeRouter
    participant Service as VolumesService
    participant Store as VolumeStore
    participant Driver as Volume Driver
    participant FS as Filesystem
    participant DB as BoltDB
    participant Events as EventsService
    
    Note over Client,Events: 阶段 1API 请求
    Client->>API: POST /volumes/create<br/>{Name: "myvol", Driver: "local"}
    API->>API: 解析 CreateOptions
    API->>Service: Create(ctx, "myvol", "local", opts)
    
    Note over Service,Store: 阶段 2:名称生成
    alt Name 为空
        Service->>Service: name = stringid.Generate()
        Service->>Service: 添加匿名卷标签
    end
    
    Note over Store,Driver: 阶段 3:卷存储创建
    Service->>Store: Create(ctx, "myvol", "local", opts)
    Store->>Store: 加锁 locks.Lock("myvol")
    
    Store->>Store: 检查 names["myvol"]
    alt 卷已存在
        Store-->>Service: 返回已存在的卷(幂等)
    else 卷不存在
        Store->>Store: 获取驱动 GetDriver("local")
        
        Note over Driver,FS: 阶段 4:驱动创建卷
        Store->>Driver: Create("myvol", options)
        Driver->>Driver: 验证卷名称
        Driver->>FS: MkdirAll(/var/lib/docker/volumes/myvol/_data, 0755)
        FS-->>Driver: ok
        
        alt 有挂载选项(NFS/CIFS
            Driver->>Driver: 解析挂载选项
            Driver->>FS: mount -t nfs ...
            FS-->>Driver: ok
        end
        
        Driver-->>Store: localVolume
        
        Note over Store,DB: 阶段 5:包装与持久化
        Store->>Store: 创建 volumeWrapper
        Store->>Store: names["myvol"] = wrapper
        Store->>Store: labels["myvol"] = {...}
        Store->>Store: options["myvol"] = {...}
        
        Store->>DB: setMeta("myvol", metadata)
        Note right of DB: BoltDB.Update()<br/>volumeMetadata JSON
        DB-->>Store: ok
        
        Note over Store,Events: 阶段 6:事件记录
        Store->>Events: LogVolumeEvent("myvol", "create")
        Events-->>Store: ok
        
        Store->>Store: 解锁 locks.Unlock("myvol")
        Store-->>Service: volumeWrapper
    end
    
    Note over Service: 阶段 7:转换为 API 类型
    Service->>Service: volumeToAPIType(wrapper)
    Service-->>API: Volume{Name, Driver, Mountpoint, ...}
    API-->>Client: 201 Created<br/>{Volume}

说明

图意概述

展示卷创建的完整流程,从 API 请求到驱动创建、元数据持久化、事件记录的全过程。

关键步骤详解

阶段 1-2:请求处理与名称生成(步骤 1-5)

// 匿名卷名称生成
if name == "" {
    name = stringid.GenerateRandomID()  // 生成 64 字符随机 ID
    options = append(options, opts.WithCreateLabel(AnonymousLabel, ""))
}

阶段 3:并发控制(步骤 6-10)

// 卷级细粒度锁
store.locks.Lock(name)
defer store.locks.Unlock(name)

// 幂等性检查
if v, exists := store.names[name]; exists {
    if v.DriverName() != driverName {
        return ErrVolumeExists  // 驱动不匹配
    }
    return v, nil  // 返回已存在的卷
}

阶段 4:驱动创建(步骤 11-17)

# Local 驱动创建卷
mkdir -p /var/lib/docker/volumes/myvol/_data
chmod 755 /var/lib/docker/volumes/myvol/_data

# 如果有挂载选项(NFS)
mount -t nfs -o addr=192.168.1.100,rw 192.168.1.100:/exports/data \
    /var/lib/docker/volumes/myvol/_data

阶段 5:持久化(步骤 18-25)

// BoltDB 存储的 volumeMetadata
{
    "Name": "myvol",
    "Driver": "local",
    "Labels": {"env": "prod"},
    "Options": {"type": "nfs", "o": "addr=192.168.1.100,rw", "device": ":/exports/data"}
}

边界条件

  • 卷名称冲突:返回已存在的卷(幂等)
  • 驱动不匹配:同名但驱动不同,返回错误
  • 挂载失败(NFS):回滚目录创建
  • 磁盘空间不足:mkdir 失败,返回错误

性能指标

  • 本地卷创建:10-30ms
    • mkdir:5-10ms
    • setMeta:5-10ms
    • 其他:5-10ms
  • NFS 卷创建:50-200ms(取决于网络延迟)

2. 卷挂载与卸载流程

时序图

sequenceDiagram
    autonumber
    participant Container as Container
    participant Service as VolumesService
    participant Store as VolumeStore
    participant Volume as localVolume
    participant FS as Filesystem
    
    Note over Container,FS: 挂载流程
    Container->>Service: Mount(ctx, vol, "container-123")
    Service->>Store: Get(ctx, "myvol")
    Store-->>Service: volumeWrapper
    
    Service->>Volume: Mount("container-123")
    Volume->>Volume: mu.Lock()
    Volume->>Volume: mountCount++
    
    alt mountCount == 1(首次挂载)
        alt 有挂载选项(NFS/CIFS
            Volume->>FS: mount -t nfs ...
            FS-->>Volume: ok
        end
        Volume->>Volume: 记录 active mount
    else mountCount > 1(已挂载)
        Note right of Volume: 复用现有挂载
    end
    
    Volume->>Volume: mu.Unlock()
    Volume-->>Service: mountPath: /var/lib/docker/volumes/myvol/_data
    Service-->>Container: mountPath
    
    Note over Container: 容器运行中...
    
    Note over Container,FS: 卸载流程
    Container->>Service: Unmount(ctx, vol, "container-123")
    Service->>Store: Get(ctx, "myvol")
    Store-->>Service: volumeWrapper
    
    Service->>Volume: Unmount("container-123")
    Volume->>Volume: mu.Lock()
    Volume->>Volume: mountCount--
    
    alt mountCount == 0(最后一个引用)
        alt 有挂载选项
            Volume->>FS: umount /var/lib/docker/volumes/myvol/_data
            FS-->>Volume: ok
        end
        Volume->>Volume: 清除 active mount
    else mountCount > 0(还有引用)
        Note right of Volume: 保持挂载状态
    end
    
    Volume->>Volume: mu.Unlock()
    Volume-->>Service: ok
    Service-->>Container: ok

说明

图意概述

展示卷挂载与卸载的流程,重点是引用计数机制和 NFS 挂载的延迟卸载。

挂载计数机制

type localVolume struct {
    mu         sync.Mutex
    mountCount int  // 挂载引用计数
}

func (v *localVolume) Mount(id string) (string, error) {
    v.mu.Lock()
    defer v.mu.Unlock()
    
    v.mountCount++
    
    // 仅首次挂载时执行实际挂载操作
    if v.mountCount == 1 && v.needsMount() {
        if err := v.mount(); err != nil {
            v.mountCount--
            return "", err
        }
    }
    
    return v.path, nil
}

func (v *localVolume) Unmount(id string) error {
    v.mu.Lock()
    defer v.mu.Unlock()
    
    if v.mountCount == 0 {
        return errors.New("volume not mounted")
    }
    
    v.mountCount--
    
    // 仅最后一个引用释放时卸载
    if v.mountCount == 0 && v.active.mounted {
        return v.unmount()
    }
    
    return nil
}

使用场景

容器 A 启动:Mount(vol, "container-A")  → mountCount = 1 → 执行 mount
容器 B 启动:Mount(vol, "container-B")  → mountCount = 2 → 复用挂载
容器 A 停止:Unmount(vol, "container-A") → mountCount = 1 → 保持挂载
容器 B 停止:Unmount(vol, "container-B") → mountCount = 0 → 执行 umount

边界条件

  • 重复挂载:mountCount 正确递增
  • 挂载失败:回滚 mountCount
  • 卸载未挂载卷:返回错误

3. 容器使用卷的完整流程

时序图

sequenceDiagram
    autonumber
    participant Daemon as Daemon
    participant Service as VolumesService
    participant Store as VolumeStore
    participant Volume as Volume
    participant Container as Container
    
    Note over Daemon,Container: 容器创建阶段
    Daemon->>Service: 解析 HostConfig.Binds
    
    loop 每个 Bind
        alt 卷不存在
            Daemon->>Service: Create(ctx, name, driver)
            Service-->>Daemon: volume
        else 卷已存在
            Daemon->>Service: Get(ctx, name)
            Service-->>Daemon: volume
        end
        
        Daemon->>Store: Acquire(name, containerID)
        Note right of Store: refs[name][containerID] = struct{}{}
        Store-->>Daemon: volume
    end
    
    Note over Daemon,Container: 容器启动阶段
    Daemon->>Service: Mount(ctx, volume, containerID)
    Service->>Volume: Mount(containerID)
    Volume-->>Service: mountPath
    Service-->>Daemon: mountPath
    
    Daemon->>Container: 配置 BindMount
    Note right of Container: mount --bind<br/>mountPath → /container/path
    
    Note over Container: 容器运行中...
    Note right of Container: 应用读写数据到卷
    
    Note over Daemon,Container: 容器停止阶段
    Container->>Daemon: 容器停止
    Daemon->>Service: Unmount(ctx, volume, containerID)
    Service->>Volume: Unmount(containerID)
    Volume-->>Service: ok
    
    Note over Daemon,Container: 容器删除阶段
    alt 不删除卷(默认)
        Daemon->>Service: Release(ctx, name, containerID)
        Service->>Store: Release(ctx, name, containerID)
        Note right of Store: delete(refs[name], containerID)
        Store-->>Daemon: ok
        Note right of Daemon: 卷保留,可被其他容器使用
    else 删除卷(--rm 或手动)
        Daemon->>Service: Remove(ctx, name)
        Service->>Store: Remove(ctx, volume)
        Store->>Store: 检查 hasRef() == false
        Store->>Volume: Remove()
        Volume-->>Store: ok
        Store-->>Daemon: ok
    end

说明

图意概述

展示容器从创建到删除的完整卷使用流程,包括引用管理和挂载绑定。

关键阶段

1. 卷获取与引用(步骤 1-9)

// 解析 HostConfig.Binds
for _, bind := range hostConfig.Binds {
    parts := strings.SplitN(bind, ":", 2)
    volumeName := parts[0]
    containerPath := parts[1]
    
    // 获取或创建卷
    vol, err := volumeService.Get(ctx, volumeName)
    if err != nil {
        vol, err = volumeService.Create(ctx, volumeName, "")
    }
    
    // 获取卷并增加引用
    vol, err = volumeStore.Acquire(volumeName, containerID)
}

2. 挂载绑定(步骤 10-14)

# 1. 卷挂载(如果需要)
mount -t nfs ... /var/lib/docker/volumes/myvol/_data

# 2. 绑定到容器
mount --bind /var/lib/docker/volumes/myvol/_data \
    /var/lib/docker/containers/{id}/mounts/{mountID}

# 3. 容器 rootfs 配置
# runc 配置中添加 mount 规则

3. 引用释放(步骤 17-23)

// 容器删除时
for _, vol := range container.MountPoints {
    // 卸载
    volumeService.Unmount(ctx, vol, containerID)
    
    // 释放引用
    volumeService.Release(ctx, vol.Name, containerID)
}

// 如果是匿名卷且容器设置了 --rm
if vol.Anonymous && container.RemoveVolume {
    volumeService.Remove(ctx, vol.Name)
}

边界条件

  • 容器异常退出:引用保留,可通过 live-restore 恢复
  • 匿名卷:容器删除时自动删除
  • 命名卷:容器删除后保留

4. 卷删除流程

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant API as volumeRouter
    participant Service as VolumesService
    participant Store as VolumeStore
    participant Volume as Volume
    participant FS as Filesystem
    participant DB as BoltDB
    participant Events as EventsService
    
    Client->>API: DELETE /volumes/myvol?force=false
    API->>Service: Remove(ctx, "myvol", opts)
    Service->>Store: Get(ctx, "myvol")
    Store-->>Service: volumeWrapper
    
    Service->>Store: Remove(ctx, volumeWrapper, opts)
    Store->>Store: 加锁 locks.Lock("myvol")
    
    Note over Store: 检查引用计数
    Store->>Store: hasRef("myvol")
    
    alt refCount > 0
        Store-->>Service: ErrVolumeInUse
        Service-->>API: 409 Conflict
        API-->>Client: 409 Conflict:<br/>volume is in use
    else refCount == 0
        Note over Volume,FS: 删除卷数据
        Store->>Volume: Remove()
        
        alt 有挂载(NFS/CIFS
            Volume->>FS: umount /var/lib/docker/volumes/myvol/_data
            FS-->>Volume: ok
        end
        
        Volume->>FS: RemoveAll(/var/lib/docker/volumes/myvol)
        FS-->>Volume: ok
        Volume-->>Store: ok
        
        Note over Store,DB: 清理元数据
        Store->>Store: delete(names, "myvol")
        Store->>Store: delete(refs, "myvol")
        Store->>Store: delete(labels, "myvol")
        Store->>Store: delete(options, "myvol")
        
        Store->>DB: removeMeta("myvol")
        Note right of DB: BoltDB.Update()<br/>Delete key
        DB-->>Store: ok
        
        Note over Store,Events: 记录事件
        Store->>Events: LogVolumeEvent("myvol", "destroy")
        Events-->>Store: ok
        
        Store->>Store: 解锁 locks.Unlock("myvol")
        Store-->>Service: ok
        Service-->>API: ok
        API-->>Client: 204 No Content
    end

说明

图意概述

展示卷删除的完整流程,重点是引用计数检查和元数据清理。

引用计数检查

func (s *VolumeStore) Remove(ctx context.Context, v volume.Volume, opts ...opts.RemoveOption) error {
    name := v.Name()
    s.locks.Lock(name)
    defer s.locks.Unlock(name)
    
    // 检查引用
    if s.hasRef(name) {
        refs := s.countRefs(name)
        return &OpErr{
            Op:   "remove",
            Name: name,
            Err:  fmt.Errorf("volume is in use - %d container(s) reference it", refs),
        }
    }
    
    // 调用驱动删除
    if err := v.Remove(); err != nil {
        return err
    }
    
    // 清理元数据
    s.globalLock.Lock()
    delete(s.names, name)
    delete(s.refs, name)
    delete(s.labels, name)
    delete(s.options, name)
    s.globalLock.Unlock()
    
    s.removeMeta(name)
    s.eventLogger.LogVolumeEvent(name, events.ActionDestroy, nil)
    
    return nil
}

边界条件

  • 卷正在使用:返回 409 Conflict
  • 卷不存在:返回 404 Not Found(force=false)
  • 驱动删除失败:返回 500 Internal Server Error
  • force=true:忽略不存在错误

5. 卷清理流程

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant API as volumeRouter
    participant Service as VolumesService
    participant Store as VolumeStore
    
    Client->>API: POST /volumes/prune<br/>?filters={"label":["env=test"]}
    API->>API: 解析过滤器
    
    alt API < 1.42
        API->>API: 添加 "all" 过滤器(兼容旧版本)
    end
    
    API->>Service: Prune(ctx, filters)
    
    Service->>Service: 检查 pruneRunning 标志
    alt 已有清理任务
        Service-->>API: 409 Conflict
        API-->>Client: 409 Conflict:<br/>prune already running
    end
    
    Service->>Service: pruneRunning.Store(true)
    Service->>Store: Find(ctx, by)
    Store-->>Service: [volumes]
    
    Note over Service: 开始清理循环
    loop 每个卷
        Service->>Store: hasRef(vol.Name())
        
        alt refCount > 0
            Note right of Service: 跳过正在使用的卷
        else refCount == 0
            alt 匹配过滤器
                alt 不是 "all" 模式
                    Service->>Service: 检查是否匿名卷
                    alt 不是匿名卷
                        Note right of Service: 跳过命名卷
                    end
                end
                
                Service->>Service: 计算卷大小
                Service->>Store: Remove(ctx, vol)
                Store-->>Service: ok
                Service->>Service: 累计 deleted & spaceReclaimed
            end
        end
    end
    
    Service->>Service: pruneRunning.Store(false)
    Service-->>API: PruneReport{VolumesDeleted, SpaceReclaimed}
    API-->>Client: 200 OK<br/>{VolumesDeleted, SpaceReclaimed}

说明

图意概述

展示卷清理的完整流程,包括并发控制、过滤逻辑、匿名卷判断。

清理逻辑

func (s *VolumesService) Prune(ctx context.Context, filter filters.Args) (*volumetypes.PruneReport, error) {
    // 防止并发清理
    if !s.pruneRunning.CompareAndSwap(false, true) {
        return nil, errdefs.Conflict(errors.New("prune already running"))
    }
    defer s.pruneRunning.Store(false)
    
    // 查找卷
    by, _ := filtersToBy(filter, acceptedPruneFilters)
    vols, _, _ := s.vs.Find(ctx, by)
    
    var deleted []string
    var spaceReclaimed uint64
    
    for _, v := range vols {
        // 跳过正在使用
        if s.vs.hasRef(v.Name()) {
            continue
        }
        
        // API >= 1.42: 默认仅清理匿名卷
        if !by.All && v.labels[AnonymousLabel] == "" {
            continue
        }
        
        // 计算空间
        if size, err := calculateSize(v.Path()); err == nil {
            spaceReclaimed += size
        }
        
        // 删除卷
        if err := s.vs.Remove(ctx, v); err == nil {
            deleted = append(deleted, v.Name())
        }
    }
    
    return &volumetypes.PruneReport{
        VolumesDeleted: deleted,
        SpaceReclaimed: spaceReclaimed,
    }, nil
}

版本差异

API < 1.42

  • 默认清理所有未使用卷(匿名 + 命名)
  • filters 隐式添加 {"all": ["true"]}

API >= 1.42

  • 默认仅清理匿名未使用卷
  • 需要显式添加 {"all": ["true"]} 才清理命名卷

边界条件

  • 并发清理:返回 409 Conflict
  • 过滤器错误:返回 400 Bad Request
  • 部分删除失败:成功的卷仍记录在报告中

6. NFS 卷挂载流程

时序图

sequenceDiagram
    autonumber
    participant Client as Docker Client
    participant API as volumeRouter
    participant Service as VolumesService
    participant Driver as local Driver
    participant FS as Filesystem
    participant NFS as NFS Server
    
    Note over Client,NFS: 创建 NFS 
    Client->>API: POST /volumes/create<br/>{Name: "nfs-vol", Driver: "local",<br/>DriverOpts: {type: "nfs", ...}}
    API->>Service: Create(ctx, "nfs-vol", "local", opts)
    Service->>Driver: Create("nfs-vol", opts)
    
    Driver->>Driver: 解析挂载选项
    Note right of Driver: type: nfs<br/>o: addr=192.168.1.100,rw<br/>device: :/exports/data
    
    Driver->>FS: MkdirAll(/var/lib/docker/volumes/nfs-vol/_data)
    FS-->>Driver: ok
    
    Driver->>FS: mount -t nfs<br/>-o addr=192.168.1.100,rw<br/>192.168.1.100:/exports/data<br/>/var/lib/docker/volumes/nfs-vol/_data
    
    FS->>NFS: TCP 连接 & RPC 握手
    NFS-->>FS: NFS mount 成功
    FS-->>Driver: ok
    
    Driver-->>Service: volume
    Service-->>API: Volume
    API-->>Client: 201 Created
    
    Note over Client,NFS: 容器挂载卷
    Client->>Service: Mount(ctx, vol, "container-123")
    Service->>Driver: Mount("container-123")
    
    Driver->>Driver: mu.Lock()
    Driver->>Driver: mountCount++
    
    alt mountCount == 1
        Note right of Driver: 已在创建时挂载,无需重复
    end
    
    Driver->>Driver: mu.Unlock()
    Driver-->>Service: /var/lib/docker/volumes/nfs-vol/_data
    
    Note over Client,NFS: 容器卸载卷
    Client->>Service: Unmount(ctx, vol, "container-123")
    Service->>Driver: Unmount("container-123")
    
    Driver->>Driver: mu.Lock()
    Driver->>Driver: mountCount--
    
    alt mountCount == 0
        Driver->>FS: umount /var/lib/docker/volumes/nfs-vol/_data
        FS->>NFS: NFS UMOUNT RPC
        NFS-->>FS: ok
        FS-->>Driver: ok
    end
    
    Driver->>Driver: mu.Unlock()
    Driver-->>Service: ok

说明

图意概述

展示 NFS 卷的特殊挂载流程,包括 NFS 协议交互和挂载计数管理。

NFS 挂载选项

{
    "Driver": "local",
    "DriverOpts": {
        "type": "nfs",
        "o": "addr=192.168.1.100,vers=4,soft,timeo=180,bg,tcp,rw",
        "device": ":/exports/data"
    }
}

对应的 mount 命令

mount -t nfs \
    -o addr=192.168.1.100,vers=4,soft,timeo=180,bg,tcp,rw \
    192.168.1.100:/exports/data \
    /var/lib/docker/volumes/nfs-vol/_data

挂载时机

  • 创建时挂载:有 DriverOpts 时立即挂载
  • 首次使用时挂载:无 DriverOpts 的 NFS 卷

边界条件

  • NFS 服务器不可达:创建失败(超时)
  • 网络中断:soft 选项允许超时返回
  • 权限不足:EACCES 错误