Containerd Snapshot 服务

概述

本文主要介绍 Containerd Snapshot 服务的源码分析,其服务的核心是实现抽象的 Snapshotter 用于容器的 rootfs 挂载和卸载等操作功能 。 Snapshotter 设计替代在 docker 早期版本称之为 graphdriver 存储驱动的设计。为支持更丰富的文件系统如 overlay 文件系统 ,引入上层抽象 snapshot 快照概念,使 docker 存储驱动更加简化同时兼容了块设备快照与 overlay 文件系统。相关说明可以参考conatinerd graph driver 作者背后的替代 graphdriver 的思考

containerd-snapshot.png

Ctr 命令行工具 snapshots 的应用

ctr contained 命令行工具 prepare 命令,基于指定的已提交态快照作为"父"创建一个新快照。

cmd/ctr/commands/snapshots/snapshots.go:274

var prepareCommand = cli.Command{
    Name:      "prepare",
    Usage:     "prepare a snapshot from a committed snapshot",
    ArgsUsage: "[flags] <key> [<parent>]",
    Flags: []cli.Flag{
        cli.StringFlag{
            Name:  "target, t",
            Usage: "mount target path, will print mount, if provided",
        },
    },
    Action: func(context *cli.Context) error {
        if narg := context.NArg(); narg < 1 || narg > 2 {
            return cli.ShowSubcommandHelp(context)
        }
        var (
            target = context.String("target")
            key    = context.Args().Get(0)
            parent = context.Args().Get(1)
        )
        client, ctx, cancel, err := commands.NewClient(context)
        if err != nil {
            return err
        }
        defer cancel()

    // 获取全局的 snapshotter 
        snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
        labels := map[string]string{
            "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
        }

    // snapshotter.Prepare 基于 “parent”镜像层创建一个工作态快照,由 Key 引用
        mounts, err := snapshotter.Prepare(ctx, key, parent, snapshots.WithLabels(labels))
        if err != nil {
            return err
        }

        if target != "" {
            printMounts(target, mounts)   // +打印显示 mount 挂载信息
        }

        return nil
    },
}

cmd/ctr/commands/snapshots/snapshots.go:624

func printMounts(target string, mounts []mount.Mount) {
    for _, m := range mounts {
        fmt.Printf("mount -t %s %s %s -o %s\n", m.Type, m.Source, target, strings.Join(m.Options, ","))
    }
}

首先通过 ctr 的 prepare 命令来看一下感受一下 snapshots 其作用。从上面可以看到通过 GRPC client 方式获取系统的 snapshotter 快照管理对象(即存储驱动实现对象),通过指定的"父" 来生成一个新快照,返回标准格式的 mounts 文件系统挂载信息。printMounts() 打印显示 mount 挂载信息" mount -t XXX"。

如果熟悉 docker 的启动过程,从操作的行为可以看出 containerd 的 snapshots 服务是在为容器运行时提供需要的 rootfs 以及相关的操作。接下来我们将展开深入的分析 Snapshot 服务代码逻辑。

Snapshot 插件注册

上层 GRPC Plugin 注册,返回 Snapshot RPC 服务实现类 service{} 对象

services/snapshots/service.go:3

func init() {
    plugin.Register(&plugin.Registration{
        Type: plugin.GRPCPlugin,          // GRPC 插件类型 "io.containerd.grpc.v1"
        ID:   "snapshots",               
        Requires: []plugin.Type{
            plugin.ServicePlugin,
        },
    InitFn: newService,             // +初始化 Func newService()
    })
}

services/snapshots/service.go:52

func newService(ic *plugin.InitContext) (interface{}, error) {
    plugins, err := ic.GetByType(plugin.ServicePlugin)
    if err != nil {
        return nil, err
    }
    p, ok := plugins[services.SnapshotsService]  // +snapshot 服务插件ID "snapshots-service"
    if !ok {
        return nil, errors.New("snapshots service not found")
    }
    i, err := p.Instance()                      // 返回 plugin InitFn func 实例化返回
    if err != nil {
        return nil, err
    }
    ss := i.(map[string]snapshots.Snapshotter)
    return &service{ss: ss}, nil        // service实现类包含 map[string]snapshots.Snapshotter
}

下层 snapshot 服务插件注册,服务 ID 为 "snapshots-service"

services/snapshots/snapshotters.go:37

func init() {
    plugin.Register(&plugin.Registration{
        Type: plugin.ServicePlugin,         // 服务插件类型 "io.containerd.service.v1"
        ID:   services.SnapshotsService,    // "snapshots-service"
        Requires: []plugin.Type{
            plugin.MetadataPlugin,
        },
        InitFn: func(ic *plugin.InitContext) (interface{}, error) {
            m, err := ic.Get(plugin.MetadataPlugin)
            if err != nil {
                return nil, err
            }

            db := m.(*metadata.DB)
            ss := make(map[string]snapshots.Snapshotter)
            for n, sn := range db.Snapshotters() {  // 元数据库内所注册的 Snapshotter
                ss[n] = newSnapshotter(sn, ic.Events)
            }
            return ss, nil               // 返回 map[string]snapshots.Snapshotter
        },
    })
}

func newSnapshotter(sn snapshots.Snapshotter, publisher events.Publisher) snapshots.Snapshotter {
    return &snapshotter{  // snapshotter 结构为对 snapshots.Snapshotter 和 events 上层封装
        Snapshotter: sn,            
        publisher:   publisher,
    }
}

此时下层需要获取元数据库内的所有注册的 snapshotter 驱动实现对象,目前支持多种驱动器的实现,我们以标准的 AUFS 联合文件系统为例查看初始化 aufs 插件注册过程。

!FILENAME vendor/github.com/containerd/aufs/aufs.go:43

func init() {
    plugin.Register(&plugin.Registration{
        Type: plugin.SnapshotPlugin,            // "io.containerd.snapshotter.v1"
        ID:   "aufs",
        InitFn: func(ic *plugin.InitContext) (interface{}, error) {
            ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
            ic.Meta.Exports["root"] = ic.Root   //  $root/io.containerd.snapshotter.v1.aufs
            return New(ic.Root)                 // +创建 aufs Snapshotter
        },
    })
}

创建 aufs 驱动的 snapshotter 实现类对象返回

!FILENAME vendor/github.com/containerd/aufs/aufs.go:66

func New(root string) (snapshots.Snapshotter, error) {
    if err := supported(); err != nil {
        return nil, errors.Wrap(plugin.ErrSkipPlugin, err.Error())
    }
    if err := os.MkdirAll(root, 0700); err != nil {
        return nil, err
    }
    ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
    if err != nil {
        return nil, err
    }
    if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err)     
  {
        return nil, err
    }
    return &snapshotter{          // aufs.snapshotter 实现类返回
        root: root,
        ms:   ms,
    }, nil
}

Snapshotter 快照操作器

Snapshotter 定义了“快照操作器”的一组可对快照分配、创建和文件系统的 changesets (变化集) 挂载方法接口。Snapshotter 以父-子关系的工作模式创建变化的集合。一个快照表示一个文件系统的状态。每个快照都有一个“父”快照(如果为空则由空字符表示)。通过对父与自身的快照 diff 操作去生成一个经典的 layer (镜像层)。

Snapshotter 通过调用 “ Prepare ” 来创建一个当前 active (工作态) 的快照。其挂载后,所有操作变更都在工作态快照之上,在通过 “ commit “ 操作来创建一个已提交态快照,此已提交态快照将成为工作态快照的父级。工作态快照不能充当 “父” 角色,只有当 commit 创建的已提交态快照才能充当 “父”。

通过快照的操作来更好的理解它们的生命周期:工作态快照总是由 ” Prepare “ 或 “ View ” 操作创建。已提交态快照 总是由“ commit ” 操作创建, 工作态快照永远不会成为已提交态快照,反之亦然。 所有的快照是是可以被 “ Remove ” 移除。

快照操作实例描述:

1. 导入 layer 镜像层

要导入一个镜像层,我们只需要让 Snapshotter 提供一个要应用的挂载列表,这样我们的目的是将捕获一个变化集。我们首先获取 layer 层 tar 文件的路径,然后创建一个临时位置将其解包到:

layerPath, tmpDir := getLayerPath(), mkTmpDir() // 仅 tar 文件层路径

我们首先使用 Snapshotter 准备一个新的快照事务,使用 key 并从空父级 “ ”开始递减。为了防止我们的层在解包期间被垃圾收集,我们添加了 “containerd.io/gc.root” 标签选项:

noGcOpt := snapshots.WithLabels(map[string]string{
        "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
})
mounts, err := snapshotter.Prepare(ctx, key, "", noGcOpt)
if err != nil { ... }

我们从 Snapshotter.Prepare 获取挂载列表,使用 key 标识工作态的快照 ,然后使用以下命令将其装载到临时位置:

if err := mount.All(mounts, tmpDir); err != nil { ... }

一旦执行了挂载,我们的临时位置下就可以捕获 diff, 实际上,这类似于文件系统事务。

接下来是解包 layer。有一个特殊的函数 unpacklayer 将 layer 的内容应用到目标位置,并计算解包层的 diffid( 这是docker 实现的要求):

layer, err := os.Open(layerPath)
if err != nil { ... }
digest, err := unpackLayer(tmpLocation, layer) // 解包至指定位置 ,返回layer 的 diffid
if err != nil { ... }

当上面操作完成,我们将有一个文件系统可表示 layer 的内容。更完善的功能实现应该还需要验证摘要是否与预期的 diffid 匹配。 当所有操作都完成后,我们最后可以 umount (卸载)前面所挂载的文件系统

unmount(mounts) // 当前可选

现在我们已经验证和解包我们的 Layer ,我们 commit 提交一个工作态镜像至 name 即已提交镜像,此实例我们仅使用 layer 的摘要值,但实际使用中可能会使用 ChainID (链ID)。此操作也移除了工作态镜像。

if err := snapshotter.Commit(ctx, digest.String(), key, noGcOpt); err != nil { ... }

现在,snapshotter 中有一个 layer 层,可以使用 commit 期间提供的摘要进行访问。

2. 导入后继 Layer

与上面的过程一样,使一个 layer 层依赖于 “父”,仅在调用 Manager.Prepare 时指定上面所生成的的标识 ID 作为父级(假设一个干净、唯一的 Key 标识符):

mounts, err := snapshotter.Prepare(ctx, key, parentDigest, noGcOpt)
3. 运行容器

要运行容器,我们只需提供 Snapshotter.Prepare 将已提交的映像快照作为“父”。挂载后,所准备好的路径可以直接用作容器的文件系统:

mounts, err := snapshotter.Prepare(ctx, containerKey, imageRootFSChainID)

然后,可以将返回的挂载直接传递给容器运行时。如果要从文件系统创建新映像,则调用manager.commit:

if err := snapshotter.Commit(ctx, newImageSnapshot, containerKey); err != nil { ... }

或者,对于大多数容器运行,将调用 snapshotter.remove 以通知 snapshotter 放弃更改。

Snapshotter 接口定义与方法说明

为了保持一致性,定义了以下术语用于 Snapshotter 实现的整个接口:

ctx - context.Context 的引用
key - 一个工作态的 snapshot 引用 (唯一值,docker 一般用于容器ID)
name - 一个 已提交态的 snapshot 引用
parent - 指向其 “父”

大多数的 Snapshotter 的方法都有混合使用上面这些(术语)。通常情况下,使用 nameparent 仅用于获取已提交态的快照。大数情况下除非有特列说明, key 被用于对工作态的快照引用。用于访问快照的所有变量使用相同的key空间(如:工作态快照不能与已提交态快照共享同一key)

!FILENAME snapshots/snapshotter.go:238

type Snapshotter interface {
    // Stat 基于快照的 name 或 key,返回快照的 Info 结构对象信息
  // 应被使用于“父”级的区分、是否存在检测以及快照类型的识别
    Stat(ctx context.Context, key string) (Info, error)
  
    // Update 更新快照的 Info 信息
  // 仅快照可写属性值被更新
    Update(ctx context.Context, info Info, fieldpaths ...string) (Info, error)

  // Usage 返回一个快照的资源使用统计,不包含“父”快照所使用的资源。
  // 
  // 此工作态快照调用的运行时间取决于实现,但可能与资源大小成正比。调用方应考虑此问题。
  // 实现应该尝试增强上下文取消并在进行计算时避免使用锁。
    Usage(ctx context.Context, key string) (Usage, error)

  // Mounts 返回由 key 为标识的工作态快照事务的挂载,可以对读写或只读事务调用,仅有效于工作态快照。
  // 用于在调用 View 或 Prepare 操作后恢复挂载
    Mounts(ctx context.Context, key string) ([]mount.Mount, error)

  // prepare创建一个工作态快照,该快照由从传参提供的“父”递减的键标识。
  // 返回的挂载可用于挂载快照以捕获更改的内容。
  // 
  // 如果传参提供了"父",则在执行装载之后,目标将以父级的内容开始。“父”必须是提交的快照。
  // 将相对于“父”捕获对已挂载目标的更改内容。默认父目录“”为空目录。
  //
  // 可以通过调用 commit 将变化部分内容保存到已提交态快照中。完成事务后,应在 key 上调用remove。
  // 多次以同样的 key 调用 Prepare 或 View 操作将失败。
    Prepare(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)

  // View 的行为与prepare相同,只是结果可能不会提交回快照 snapshotter。
  // View 返回父级上的只读视图,工作态快照由给定的键跟踪。
  // 
  // 此方法的操作与 prepare 相同,只是返回的 mounts 可能设置了 readonly 标志。
  // 对底层文件系统的任何修改都将被忽略。实现可以以一种更有效的方式执行此操作,
  // 这与使用“prepare”尝试的方式不同。
  // 
  // 不能对传参时提供的 key 调用 commit,它将返回一个错误。
  // 若要收集与键关联的资源,必须使用键作为参数调用 remove。
    View(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)

    // A committed snapshot will be created under name with the parent of the
    // active snapshot.
    // 
  // Commit 捕获指定的 key 与其 “父” 快照两者的所有改变部分,并生成以 name 作为标识的快照。
  // 此 name 则又能用于 snapshotter 的其它方法去创建"子"快照。
  // 
  // 将使用工作态快照的父快照的名称创建提交的快照 
  // 在 commit 完成后,由 key 所标识的快照将被移除
    Commit(ctx context.Context, name, key string, opts ...Opt) error

  // Remove 移除指定 Key 的快照(已提交态或工作态的),移除操作将移除与之相关的所有资源。
  // 如果此快照是另外快照的“父”,在处理移除操作之前须先将其“子”先移除
    Remove(ctx context.Context, key string) error

  // 在 snapshotter 中遍历所有快照
    Walk(ctx context.Context, fn func(context.Context, Info) error) error

  // 释放内部资源,Close 用于 snapshotter 生命周期的最后阶段,非强制操作
    Close() error
}

默认情况下 Containerd 自带了btrfs、devmapper、lcow、native、overlay、windows 存储驱动,如下所示代码位置:

╭─ ../src/github.com/containerd/containerd/snapshots  ‹master*› 
╰─➤  tree -L 1
.
├── btrfs
├── devmapper
├── lcow
├── native
├── overlay
├── proxy
├── snapshotter.go
└── windows

而 aufs 存储驱动由一个单独的 github 项目管理aufs , 在 containerd 内通过 vendor 包依赖导入,在cmd/containerd/builtins_linux.go 编译时导入 import (_ github.com/containerd/aufs)。

Aufs 存储驱动

Aufs存储驱动为 Snapshotter 的一个实现版本,前面也描述了还有其它存储驱动的实现,从代码架构逻辑上来看基本上大同小异,最主要是底层的文件系统层挂载需要代码逻辑去按协议不同构造同一套接口实现。aufs Snapshotter 需要实现的构建联合文件系统,实现多个不同目录挂载在同一个目录下,而如果是 devmapper 存储驱动底层则创建多个虚拟设备来存放在同一个数据卷进行管理。

Aufs 底层文件系统挂载时需要指定特有的选项如:

  • br 指定需要挂载的文件夹
  • ro/rw 指定文件的权限只读和可读写
  • dio 开启直接 I/O 机制
  • xino 使用外部 inode 数字位图和转换表

初始化 aufs 插件注册

!FILENAME vendor/github.com/containerd/aufs/aufs.go:43

func init() {
    plugin.Register(&plugin.Registration{
        Type: plugin.SnapshotPlugin,
        ID:   "aufs",
        InitFn: func(ic *plugin.InitContext) (interface{}, error) {
            ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
            ic.Meta.Exports["root"] = ic.Root   //  $root/io.containerd.snapshotter.v1.aufs
            return New(ic.Root)                 // +创建 aufs Snapshotter
        },
    })
}

aufs snapshotter 实现结构体定义

type snapshotter struct {
    root string
    ms   *storage.MetaStore
}

创建 aufs 驱动的 snapshotter

!FILENAME vendor/github.com/containerd/aufs/aufs.go:66

func New(root string) (snapshots.Snapshotter, error) {
    if err := supported(); err != nil {
        return nil, errors.Wrap(plugin.ErrSkipPlugin, err.Error())
    }
  // 创建根目录
    if err := os.MkdirAll(root, 0700); err != nil {
        return nil, err
    }
  // 元数据库储
    ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
    if err != nil {
        return nil, err
    }
  // 创建快照目录
    if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err)     
  {
        return nil, err
    }
    return &snapshotter{
        root: root,
        ms:   ms,
    }, nil
}

快照操作实现代码

从 snapshotter 接口定义操作来看包含:Stat 、Update、Mounts、Prepare、View、Commit、Remove、Walk、Close 九个方法需要 aufs.snapshotter 类实现,下面我们主要分析最常用的三个方法 Prepare 、Mounts 和 Commit的实现代码逻辑。

Prepare

aufs snapshotter.prepare() 创建一个工作态快照,返回 aufs mount 挂载对象可用于后续挂载相关操作。

!FILENAME vendor/github.com/containerd/aufs/aufs.go:147

func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
    return o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)// +调用 createSnapshot
}

!FILENAME vendor/github.com/containerd/aufs/aufs.go:256

func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
    var (
        path        string
        snapshotDir = filepath.Join(o.root, "snapshots")   //  $root/snapshots/ 目录 
    )
  
  // 创建临时目录 "./new-"
    td, err := ioutil.TempDir(snapshotDir, "new-")
    if err != nil {
        return nil, errors.Wrap(err, "failed to create temp dir")
    } 
  //...
  
  // 创建 fs 目录
    fs := filepath.Join(td, "fs")
    if err = os.MkdirAll(fs, 0755); err != nil {
        return nil, err
    }

  // 创建元数据事务上下文
    ctx, t, err := o.ms.TransactionContext(ctx, true)
    if err != nil {
        return nil, err
    }
  
  // +基于指定的配置项,新建快照(参看后述)
    s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
    if err != nil {
        if rerr := t.Rollback(); rerr != nil {
            log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
        }
        return nil, errors.Wrap(err, "failed to create active")
    }

    if len(s.ParentIDs) > 0 {
        st, err := os.Stat(filepath.Join(o.upperPath(s.ParentIDs[0])))
      //...
    
    // chown 修改 fs 文件的 uid 和 gid 与 "父" 一致 
        if err := os.Lchown(fs, int(stat.Uid), int(stat.Gid)); err != nil {
            if rerr := t.Rollback(); rerr != nil {
                log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
            }
            return nil, errors.Wrap(err, "failed to chown")
        }
    }

  //  为新生成快照准备文件目录
  //  重命名路径:
  //  td   ==>  $root/snapshots/new-/fs
  //  path ==>  $root/snapshots/$id
    path = filepath.Join(snapshotDir, s.ID)
    if err = os.Rename(td, path); err != nil {
   //...
    }
    td = ""

  // 事务提交
    if err = t.Commit(); err != nil {
        return nil, errors.Wrap(err, "commit failed")
    }

  // +构建"新快照s"挂载对象 (参看后述)
  return o.mounts(s), nil     
}

storage.CreateSnapshot() 为 active 或 view 快照所提供的"父"配置插入一条记录至元数据库,返回新建快照的元信息 Snapshot{Kind,ID,ParentIDs} 的实例

!FILENAME snapshots/storage/bolt.go:206

func CreateSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) (s Snapshot, err error) {
  // 支持检测 active 和 view 类型
    switch kind {
    case snapshots.KindActive, snapshots.KindView:
    default:
        return Snapshot{}, errors.Wrapf(errdefs.ErrInvalidArgument, "snapshot type %v invalid; only snapshots of type Active or View can be created", kind)
    }
    var base snapshots.Info
  // 加载选项 opts
    for _, opt := range opts {
        if err := opt(&base); err != nil {
            return Snapshot{}, err
        }
    }

    err = createBucketIfNotExists(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
        var (
            spbkt *bolt.Bucket
        )
    // 获取指定的 "父" bucket
        if parent != "" {
            spbkt = bkt.Bucket([]byte(parent))
            if spbkt == nil {
                return errors.Wrapf(errdefs.ErrNotFound, "missing parent %q bucket", parent)
            }

      // 检验为只读(Committed类型)
            if readKind(spbkt) != snapshots.KindCommitted {
                return errors.Wrapf(errdefs.ErrInvalidArgument, "parent %q is not committed snapshot", parent)
            }
        }
    // 基于指定的 key 作为引用名称,创建新的元数据库 bucket 
        sbkt, err := bkt.CreateBucket([]byte(key))
        if err != nil {
            if err == bolt.ErrBucketExists {
                err = errors.Wrapf(errdefs.ErrAlreadyExists, "snapshot %v", key)
            }
            return err
        }

    // 为 snapshot 新生成 id
        id, err := bkt.NextSequence()
        if err != nil {
            return errors.Wrapf(err, "unable to get identifier for snapshot %q", key)
        }

        t := time.Now().UTC()
        si := snapshots.Info{     // 新建的 snapshots 元信息
            Parent:  parent,
            Kind:    kind,
            Labels:  base.Labels,
            Created: t,
            Updated: t,
        }
    // 存储新建的 Snapshot 元信息到元数据库 
        if err := putSnapshot(sbkt, id, si); err != nil {
            return err
        }

        if spbkt != nil {
            pid := readID(spbkt)

      // 存储从 key 与"父" 反链信息 
            if err := pbkt.Put(parentKey(pid, id), []byte(key)); err != nil {
                return errors.Wrapf(err, "failed to write parent link for snapshot %q", key)
            }

      //为新创的 snapshot 设置"父"链的 IDs
            s.ParentIDs, err = parents(bkt, spbkt, pid)  
            if err != nil {
                return errors.Wrapf(err, "failed to get parent chain for snapshot %q", key)
            }
        }

        s.ID = fmt.Sprintf("%d", id)   // 新创的snapshot ID
        s.Kind = kind                  // 新创的snapshot 类型
        return nil
    })
    if err != nil {
        return Snapshot{}, err
    }

    return
}

!FILENAME snapshots/storage/metastore.go:53

type Snapshot struct {
    Kind        snapshots.Kind 
    ID        string
    ParentIDs    []string
}
Mounts

aufs snapshotter.mounts() 依据指定“父”配置,生成aufs 文件系统挂载选项,返回所构建 aufs 类型 mount 对象。

!FILENAME vendor/github.com/containerd/aufs/aufs.go:334

func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount {
  //  无“父”层则返回自身 bind 类型 mount
    if len(s.ParentIDs) == 0 {
        // if we only have one layer/no parents then just return a bind mount
        roFlag := "rw"
        if s.Kind == snapshots.KindView {
            roFlag = "ro"
        }

        return []mount.Mount{
            {
                Source: o.upperPath(s.ID),
                Type:   "bind",
                Options: []string{
                    roFlag,
                    "rbind",
                },
            },
        }
    }

    aufsOptions := []string{
        "br",
    }

    if s.Kind == snapshots.KindActive {
        aufsOptions = append(aufsOptions,           //aufs 文件系统挂载选项
            fmt.Sprintf("%s=rw", o.upperPath(s.ID)),
        )
    } else if len(s.ParentIDs) == 1 {             // 单层“父”
        return []mount.Mount{
            {
                Source: o.upperPath(s.ParentIDs[0]),    
                Type:   "bind",
                Options: []string{
                    "ro",
                    "rbind",
                },
            },
        }
    }

    for i := range s.ParentIDs {                
        aufsOptions = append(aufsOptions, fmt.Sprintf("%s=ro+wh", o.upperPath(s.ParentIDs[i])))                  //多层“父” aufs 文件系统挂载选项
    }
    options := []string{
        "dio",
        "xino=/dev/shm/aufs.xino",
    }
    if useDirperm() {
        options = append(options, "dirperm1")
    }

    options = append(options, strings.Join(aufsOptions, ":"))
    return []mount.Mount{
        {
            Type:    "aufs",
            Source:  "none",
            Options: options,
        },
    }

}
Commit

aufs snapshotter.Commit() 将可读写的工作态快照提交生成只读的已提交态快照,已提交态的快照可作为"父"角色存在。

!FILENAME vendor/github.com/containerd/aufs/aufs.go:172

func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
  // 创建metadata元数据库的事务上下文
    ctx, t, err := o.ms.TransactionContext(ctx, true)
    if err != nil {
        return err
    }

  // 在发生错误时,事务Rollback
    defer func() {
        if err != nil 
            if rerr := t.Rollback(); rerr != nil {
                log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
            }
        }
    }()

  // 获取存在的工作态快照 ID
    id, _, _, err := storage.GetInfo(ctx, key)
    if err != nil {
        return err
    }

  // 基于 ID 获取磁盘占用情况
    usage, err := fs.DiskUsage(ctx, o.upperPath(id))
    if err != nil {
        return err
    }
  
  // +提交 Active 工作态快照成为"已提交态"快照
    if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
        return errors.Wrap(err, "failed to commit snapshot")
    }

 // 元数据存储事务提交   
 return t.Commit()
}

storage.CommitActive() 将"key"指向的"工作态"快照事务(存储事务)重命名为"name"指向的提交态快照。生成的快照将被提交并只读。"key"指向将不再可用于查找或删除。提交的快照返回的字符串标识符与原始工作态快照的标识符相同。

snapshots/storage/bolt.go:339

func CommitActive(ctx context.Context, key, name string, usage snapshots.Usage, opts ...snapshots.Opt) (string, error) {
    var (
        id   uint64
        base snapshots.Info
    )
  // 快照选项加载
    for _, opt := range opts {
        if err := opt(&base); err != nil {
            return "", err
        }
    }

    if err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
        //基于指定的 name 创建"目标" Bucket 
    dbkt, err := bkt.CreateBucket([]byte(name))
        if err != nil {
            if err == bolt.ErrBucketExists {
                err = errdefs.ErrAlreadyExists
            }
            return errors.Wrapf(err, "committed snapshot %v", name)
        }
    
    //基于指定的 key 获取"源" Bucket 
        sbkt := bkt.Bucket([]byte(key))
        if sbkt == nil {
            return errors.Wrapf(errdefs.ErrNotFound, "failed to get active snapshot %q", key)
        }
    
    // snapshots.Info 更新
        var si snapshots.Info
        if err := readSnapshot(sbkt, &id, &si); err != nil {
            return errors.Wrapf(err, "failed to read active snapshot %q", key)
        }
    // 仅针对类型为 Active 的快照判断
        if si.Kind != snapshots.KindActive {
            return errors.Wrapf(errdefs.ErrFailedPrecondition, "snapshot %q is not active", key)
        }
    si.Kind = snapshots.KindCommitted  // info: Committed 类型
    si.Created = time.Now().UTC()      // info: 创建时间
    si.Updated = si.Created            // info: 更新时间

        // Replace labels, do not inherit
    si.Labels = base.Labels           //  info: 更新标签项

    // 保存快照元数据至目标 bucket
        if err := putSnapshot(dbkt, id, si); err != nil {
            return err
        }
    // 保存快照磁盘空间使用元信息至目标 bucket
        if err := putUsage(dbkt, usage); err != nil {
            return err
        }
    // 删除"源" bucket 的元数据记录项
        if err := bkt.DeleteBucket([]byte(key)); err != nil {
            return errors.Wrapf(err, "failed to delete active snapshot %q", key)
        }
        if si.Parent != "" {
      // 获取"父"信息
            spbkt := bkt.Bucket([]byte(si.Parent))
            if spbkt == nil {
                return errors.Wrapf(errdefs.ErrNotFound, "missing parent %q of snapshot %q", si.Parent, key)
            }
            pid := readID(spbkt)

            // Updates parent back link to use new key
      // 更新"父"对新快照名的反链元信息(parent id,new-snapshot id,new-snapshot name)
            if err := pbkt.Put(parentKey(pid, id), []byte(name)); err != nil {
                return errors.Wrapf(err, "failed to update parent link %q from %q to %q", pid, key, name)
            }
        }

        return nil
    }); err != nil {
        return "", err
    }

  // 返回 new-snapshot id ,已提交态快照ID
    return fmt.Sprintf("%d", id), nil   
}

〜〜 本文 END 〜〜

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容