pouch commit实现
我们借助pouch commit的实现来看一下,如何使用containerd的diff和snapshot服务。 首先要明白containerd是不支持build/commit功能的,那么pouch是如何实现的commit命令呢?
docker commit
要明白pouch commit如何实现,我们首先看一下docker commit是如何实现的,在功能上基本相关,只不过前者依赖containerd提供的grpc服务。
深入学习docker commit 的原理前,我不妨先来看看一下 docker help 中关于 commit 命令的阐述: commit Create a new image from a container's changes 结合上图与命令docker commit 的描述,我们可以发现有三个关键字Image、Container 与Changes 。如何理解这三个关键字,我们可以从以下三个步骤入手:
Docker Daemon 会通过一个 Docker 镜像运行一个 Docker 容器,Docker 通过层级文件系统为 Docker 容器提供文件系统视角,最上层的是可读可写层(Read-Write Layer)。
Docker 容器初始的可读可写层内容均为空,Docker 容器对文件系统的内容更新将全部更新于可读可写层(Read-Write Layer)。
实现 docker commit 操作时,Docker 仅仅是将可读可写层(Read-Write Layer)中的更新内容,打包为一个全新的镜像。
简言之,所谓的commit功能就是将容器最上层的R/W层重新打包成一个新的镜像,至于说如何构造镜像要依赖不同的实现。
pouch commit实现
有了上边的介绍,我们对commit的功能有了一个基本的了解,如果让我们在pouch里边去实现commit的话,无非依赖于以下几个操作,Diff操作来提取出R/W读写层,Content服务写入tar包,然后unpack到snapshot中去,我们实际看一下pouch commit的实现看是不是这样做的。
path: alibaba/pouch/ctrd/image_commit.go
func (c *Client) Commit(ctx context.Context, config *CommitConfig) (_ digest.Digest, err0 error) {
// get a containerd client
wrapperCli, err := c.Get(ctx)
if err != nil {
return "", fmt.Errorf("failed to get a containerd grpc client: %v", err)
}
client := wrapperCli.client
var (
sn = client.SnapshotService(defaultSnapshotterName)
cs = client.ContentStore()
differ = client.DiffService()
)
// export new layer
snapshot, err := c.GetSnapshot(ctx, config.ContainerID)
if err != nil {
return "", fmt.Errorf("failed to get snapshot: %s", err)
}
// 调用diff服务,返回描述这一层的descriptor
layer, diffIDStr, err := exportLayer(ctx, snapshot.Name, sn, cs, differ)
if err != nil {
return "", err
}
// create child image
diffIDDigest, err := digest.Parse(diffIDStr)
if err != nil {
return "", err
}
//结合parent image构建一个符合oci spec的img描述体
childImg, err := newChildImage(ctx, config, diffIDDigest)
if err != nil {
return "", err
}
// create new snapshot for new layer
//产生新镜像chainId作为镜像的snapshotKey
snapshotKey := identity.ChainID(childImg.RootFS.DiffIDs).String()
//还是按照pull镜像时候的3步走 先prepare -> apply -> commit
if err = newSnapshot(ctx, config.Image, sn, differ, layer, snapshotKey, diffIDStr); err != nil {
return "", err
}
defer func() {
if err0 != nil {
logrus.Warnf("remove snapshot %s cause commit image failed", snapshotKey)
client.SnapshotService(defaultSnapshotterName).Remove(ctx, snapshotKey)
}
}()
imgJSON, err := json.Marshal(childImg)
if err != nil {
return "", err
}
// 以下分别构建新镜像config descriptor的layer descriptor
// new config descriptor
configDesc := ocispec.Descriptor{
MediaType: configType,
Digest: digest.FromBytes(imgJSON),
Size: int64(len(imgJSON)),
}
// get parent image layer descriptor
pmfst, err := images.Manifest(ctx, cs, config.CImage.Target(), platforms.Default())
if err != nil {
return "", err
}
// new layer descriptor
layers := append(pmfst.Layers, layer)
labels := map[string]string{
"containerd.io/gc.ref.content.0": configDesc.Digest.String(),
}
for i, l := range layers {
labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String()
}
// new manifest descriptor
mfst := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Config: configDesc,
Layers: layers,
}
mfstJSON, err := json.MarshalIndent(mfst, "", " ")
if err != nil {
return "", errors.Wrap(err, "failed to marshal manifest")
}
mfstDigest := digest.FromBytes(mfstJSON)
mfstDesc := ocispec.Descriptor{
Digest: mfstDigest,
Size: int64(len(mfstJSON)),
}
desc := ocispec.Descriptor{
MediaType: manifestType,
Digest: mfstDigest,
Size: int64(len(mfstJSON)),
}
// image create
img := images.Image{
Name: config.Reference,
Target: desc,
CreatedAt: time.Now(),
}
// register containerd image metadata.
// 向containerd的images注册镜像的元数据
if _, err := client.ImageService().Update(ctx, img); err != nil {
if !errdefs.IsNotFound(err) {
return "", fmt.Errorf("failed to cover exist image %s", err)
}
if _, err := client.ImageService().Create(ctx, img); err != nil {
return "", fmt.Errorf("failed to create new image %s", err)
}
}
// write manifest content
//镜像为单位的,在unpack那个环节有介绍
if err := content.WriteBlob(ctx, cs, mfstDigest.String(), bytes.NewReader(mfstJSON), mfstDesc.Size, mfstDesc.Digest, content.WithLabels(labels)); err != nil {
return "", errors.Wrapf(err, "error writing manifest blob %s", mfstDigest)
}
// write config content
// 实际上就是前边那个oci spec img
if err := content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(imgJSON), configDesc.Size, configDesc.Digest); err != nil {
return "", errors.Wrap(err, "error writing config blob")
}
// pouch record config descriptor digest as image id.
return configDesc.Digest, nil
}
总结
通过观察commit的实现来说,我们可以发现diff服务和snapshot服务之间的职责分工,diff服务会操作tar包来完成r/w的打包,applyDiff这些操作,而snapshot服务来说完成类似登记的功能,以mounts挂载点来和其他服务连接起来。
参考
https://github.com/alibaba/pouch/pull/2125 http://guide.daocloud.io/dcs/docker-commit-9153991.html