本文内容节选自 《containerd 原理剖析与实战》,本书正参加限时优惠内购,点击阅读原文,限时 69.9 元购买。
上一篇文章《一文了解 containerd 中的 snapshot》中,介绍了containerd 的 snapshot
机制,了解到 containerd 通过内置的 snapshotter 比如aufs
、btrfs
、devmapper
、native
、overlayfs
、zfs
等,来完成 snapshot
生命周期的管理。
接下来我们从最简单的 native
snapshotter 开始,带领大家了解 snapshotter 的实现。
native snapshotter
native
snapshotter 是 containerd 中最早实现的 snapshotter,native
snapshotter 使用的是原生的文件系统保存 snapshot
,假如一个镜像有四层 layer,每层镜像 layer 有 10 MB
的未压缩文件,那么 snapshotter 将会创建四个 snapshot,分别是 10MB
、20MB
、30MB
、40MB
,总共有 100MB
大小。
换句话说,我们的镜像有 40MB
,却占用了 100MB
的存储空间,存储效率确实有点低。不过对于其他 snapshotter (如 overlay
,devmapper
等)来说,将会通过使用不同的策略来消除这种存储效率低下的问题。
下面通过一个镜像示例介绍 native
snapshotter 原理,首先基于下面的 Dockerfile
构建一个镜像,代码如下。
# alpine image 占用存储空间比较小
FROM alpine:latest
# 每层分别创建 10MB 大小的文件
RUN dd if=/dev/zero of=file_a bs=1024 count=10240
RUN dd if=/dev/zero of=file_b bs=1024 count=10240
RUN dd if=/dev/zero of=file_c bs=1024 count=10240
基于 nerdctl 构建镜像,代码如下。
[root@zjz ~]# nerdctl build -t zhaojizhuang66/snapshots-test .
推送镜像,代码如下。
[root@zjz ~]# nerdctl push zhaojizhuang66/snapshots-test
通过 nerdctl 指定 native snapshotter 拉取镜像,代码如下。
[root@zjz ~/containerd]# nerdctl --snapshotter native pull zhaojizhuang66/testsnapshotter
进入 native
snapshots
对应的路径查看,代码如下。
[root@zjz ~/containerd]# cd /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots
[root@zjz ~/containerd]# ls
1 2 3 4
总共有 4 个 snapshot
,查看每个 snapshot
的大小,可以看到每个 snapshot
的大小依次增加 10MB
左右。
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ls -lh 1 |head -n 1
total 68K
# 第 2 个 snapshots 为 alpine + 10MB
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ls -lh 2 |head -n 1
total 11M
# 第 3 个 snapshots 为 alpine + 10MB + 10MB
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ls -lh 3 |head -n 1
total 21M
# 第 4 个 snapshots 为 alpine + 10MB + 10MB + 10MB
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ls -lh 4 |head -n 1
total 31M
接下来查看每个 snapshot
中的内容。
第 1 个 snapshot
,代码如下。
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ll 1
total 76
drwxr-xr-x 19 root root 4096 Mar 7 14:56 .
drwx------ 6 root root 4096 Mar 7 14:56 ..
drwxr-xr-x 2 root root 4096 Feb 11 00:45 bin
drwxr-xr-x 2 root root 4096 Feb 11 00:45 dev
drwxr-xr-x 17 root root 4096 Feb 11 00:45 etc
... 省略 ...
第 2 个 snapshot
,代码如下。
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ll 2
total 10316
drwxr-xr-x 19 root root 4096 Mar 7 14:56 .
drwx------ 6 root root 4096 Mar 7 14:56 ..
drwxr-xr-x 2 root root 4096 Mar 7 14:56 bin
drwxr-xr-x 2 root root 4096 Feb 11 00:45 dev
drwxr-xr-x 17 root root 4096 Mar 7 14:56 etc
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_a
... 省略 ...
第 3 个 snapshot
,代码如下。
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ll 3
total 20556
drwxr-xr-x 19 root root 4096 Mar 7 14:56 .
drwx------ 6 root root 4096 Mar 7 14:56 ..
drwxr-xr-x 2 root root 4096 Mar 7 14:56 bin
drwxr-xr-x 2 root root 4096 Feb 11 00:45 dev
drwxr-xr-x 17 root root 4096 Mar 7 14:56 etc
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_a
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_b
... 省略 ...
第 4 个 snapshot
,代码如下。
[root@zjz /var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots]# ll 4
total 30796
drwxr-xr-x 19 root root 4096 Mar 7 14:56 .
drwx------ 6 root root 4096 Mar 7 14:56 ..
drwxr-xr-x 2 root root 4096 Mar 7 14:56 bin
drwxr-xr-x 2 root root 4096 Feb 11 00:45 dev
drwxr-xr-x 17 root root 4096 Mar 7 14:56 etc
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_a
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_b
-rw-r--r-- 1 root root 10485760 Mar 7 14:47 file_c
... 省略 ...
以上就是 naitve
snapshotter 准备容器 rootfs
的过程。可以看到, 对于 native
snapshotter 来说,多层 snapshotter 对于镜像存储来说又有些浪费的,总共 30MB
的镜像,经过 native
snapshotter 解压之后,总共占用了 60MB
的存储空间。
下面看 native
snapshotter 的源码可以具体实现,代码如下。
// 版本 v1.7.0
// containerd/snapshots/native/native.go
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)
}
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
// 1. 获取 parent snapshot 的目录
parent := o.getSnapshotDir(s.ParentIDs[0])
// 2. 直接 copy parent snapshot 目录中的内容到新的 snapshot 目录
s.CopyDir(dst-snapshot-path, parent, ...);
// 3. 返回的挂载信息为
return []mount.Mount{
{
Source: dst-snapshot-path,
Type: "bind",
Options: []string{"rbind","ro"},
},
}
}
查看 snapshot
对应的挂载信息,代码如下。
# 启动容器,创建 active 状态的 snapshot
root@zjz:~# ctr run --snapshotter native -d docker.io/zhaojizhuang66/testsnapshotter:latest zjz
# 看到多了一层 名为 zjz 的 active 的 snapshot
root@zjz:~# ctr snapshot --snapshotter native ls
KEY PARENT KIND
sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39 Committed
sha256:db7e45c34c1fd60255055131918550259be8d7a83e0ac953df15d9410dc07b07 sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39 Committed
sha256:a937f098cfdf05ea5f262cbba031de305649a102fabc47014d2b062428573d42 sha256:db7e45c34c1fd60255055131918550259be8d7a83e0ac953df15d9410dc07b07 Committed
sha256:77297b225cd30d2ace7f5591a7e9208263428b291fd44aac95af92f7337b342a sha256:a937f098cfdf05ea5f262cbba031de305649a102fabc47014d2b062428573d42 Committed
zjz sha256:77297b225cd30d2ace7f5591a7e9208263428b291fd44aac95af92f7337b342a Active
# 查看该 snapshot 的挂载信息
root@zjz:~# ctr snapshot --snapshotter native mount /tmp zjz
mount -t bind /data00/lib/containerd/io.containerd.snapshotter.v1.native/snapshots/20 /tmp -o rbind,rw
可以看到 native
snapshotter 只是通过简单的 Copy 调用,将父 snapshot
中的内容拷贝到子 snapshot
中。
native
snapshotter 对于相同的内容进行了多重保存,还是有些浪费的,那么有没有其他更高效的存储方式呢?答案是肯定的。
同时也欢迎同学们留言,可以通过利用哪些技术,能够有效解决镜像层重复占用存储空间的问题。后续的文章将继续介绍社区是怎么进行高效存储的。
以上内容节选自新书 《containerd 原理剖析与实战》
最后,附上本书的购买链接,新书刚刚上架原价 109,限时优惠内购 69.9 元,