containerd Snapshots功能解析

news2024/11/16 12:52:06

containerd Snapshots功能解析

snapshot是containerd的一个核心功能,用于创建和管理容器的文件系统。
本篇containerd版本为v1.7.9

本文以 ctr i pull命令为例,分析containerd的snapshot “创建” 相关的功能。

在这里插入图片描述

ctr命令

ctr image相关命令的实现在cmd/ctr/commands/images目录中。

查看文件cmd/ctr/commands/images/pull.go

// 查看Action中注册的函数
func(context *cli.Context) error {
		// 省略内容....
        // 获取grpc客户端
		client, ctx, cancel, err := commands.NewClient(context)
		if err != nil {
			return err
		}
		defer cancel()
        // 这里的功能是将pull动作,通过grpc调用完全交给远端实现。
        // 当前的代码版本, 此块代码永远不会执行。
		if !context.BoolT("local") {
            // grpc调用
			return client.Transfer(ctx, reg, is, transfer.WithProgress(pf))
		}

		// 省略内容....
        // fetch阶段
		img, err := content.Fetch(ctx, client, ref, config)
		if err != nil {
			return err
		}

		// 省略内容....
        // unpack阶段
        // 根据平台信息,解压镜像,创建快照等
		start := time.Now()
		for _, platform := range p {
			fmt.Printf("unpacking %s %s...\n", platforms.Format(platform), img.Target.Digest)
			i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform))
			err = i.Unpack(ctx, context.String("snapshotter"))
			if err != nil {
				return err
			}
			if context.Bool("print-chainid") {
				diffIDs, err := i.RootFS(ctx)
				if err != nil {
					return err
				}
				chainID := identity.ChainID(diffIDs).String()
				fmt.Printf("image chain ID: %s\n", chainID)
			}
		}
		fmt.Printf("done: %s\t\n", time.Since(start))
		return nil
	}

fetch阶段

fetch阶段分为两步:

  1. 下载镜像
  2. 在数据库中添加镜像记录

查看文件client/client.go

func (c *Client) Fetch(ctx context.Context, ref string, opts ...RemoteOpt) (images.Image, error) {
    // 省略内容....
	ctx, done, err := c.WithLease(ctx)
	if err != nil {
		return images.Image{}, err
	}
	defer done(ctx)
    // 下载镜像
	img, err := c.fetch(ctx, fetchCtx, ref, 0)
	if err != nil {
		return images.Image{}, err
	}
    // 在数据库中添加镜像记录
	return c.createNewImage(ctx, img)
}

下载镜像

c.fetch(ctx, fetchCtx, ref, 0)

下载镜像没什么好说的,需要注意的是,当前的代码版本,下载的功能是在ctr中实现的,而不是调用grpc接口实现的。

createNewImage

// c.createNewImage(ctx, img)
func (c *Client) createNewImage(ctx context.Context, img images.Image) (images.Image, error) {
    // 省略内容....
	is := c.ImageService()
	for {
		if created, err := is.Create(ctx, img); err != nil {
			if !errdefs.IsAlreadyExists(err) {
				return images.Image{}, err
			}
             // 如果镜像已经存在,则更新镜像
			updated, err := is.Update(ctx, img)
			if err != nil {
				// if image was removed, try create again
				if errdefs.IsNotFound(err) {
					continue
				}
				return images.Image{}, err
			}

			img = updated
		} else {
			img = created
		}

		return img, nil
	}
}

最终会以grpc方式调用containerdImageService接口。

containerd中的接口,均是以plugin的方式注册实现的。plugin的实现我们后面再分析。

// image service注册
// services/images/service.go
func init() {
	plugin.Register(&plugin.Registration{
		Type: plugin.GRPCPlugin,
		ID:   "images",
		Requires: []plugin.Type{
			plugin.ServicePlugin,
		},
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			// 省略内容....
		},
	})
}

images.create没有太多逻辑,主要是在bbolt中添加一条数据记录。
bbolt是一个key-value数据库,containerd中的大部分数据都是存储在bbolt中的。
https://pkg.go.dev/go.etcd.io/bbolt#section-readme

func (l *local) Create(ctx context.Context, req *imagesapi.CreateImageRequest, _ ...grpc.CallOption) (*imagesapi.CreateImageResponse, error) {
	// 省略内容....
    // 在bbolt中添加一条数据记录
	created, err := l.store.Create(ctx, image)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}

	resp.Image = imageToProto(&created)
    // 发布事件
    // 事件发布是containerd中的一个重要功能,后面会详细分析。
	if err := l.publisher.Publish(ctx, "/images/create", &eventstypes.ImageCreate{
		Name:   resp.Image.Name,
		Labels: resp.Image.Labels,
	}); err != nil {
		return nil, err
	}

	l.emitSchema1DeprecationWarning(ctx, &image)
	return &resp, nil

}

images.update对比create,多了一些逻辑,主要是更新镜像的某些字段。

func (l *local) Update(ctx context.Context, req *imagesapi.UpdateImageRequest, _ ...grpc.CallOption) (*imagesapi.UpdateImageResponse, error) {
    // 省略内容....
    // 更新镜像的某些字段
	if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
		fieldpaths = append(fieldpaths, req.UpdateMask.Paths...)
	}

	if req.SourceDateEpoch != nil {
		tm := req.SourceDateEpoch.AsTime()
		ctx = epoch.WithSourceDateEpoch(ctx, &tm)
	}
    // 在bbolt中更新一条数据记录
    // fieldpaths 为需要更新的字段
	updated, err := l.store.Update(ctx, image, fieldpaths...)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}
    // 省略内容....
    // 发布事件....
	return &resp, nil
}

unpack阶段

fetch阶段下载的镜像,可以理解为压缩包,unpack阶段就是解压镜像,创建快照等操作。

解压镜像好理解,创建快照是什么意思呢?

镜像的文件系统是只读的,容器的文件系统是可写的,容器的文件系统是基于镜像的文件系统创建的,这个过程就是创建快照。
在containerd中, 每个容器都有一个自己的快照,利用这个特性,可以实现容器的快速创建和销毁。

containerd实现有两种Snapshotter,一种是通过overlayfs实现,一种是通过native实现。

overlayfslinux内核的一个功能,nativecontainerd自己实现的一种快照方式。

native实现中,所有的快照都将是完全copy,所以native的快照方式,会占用更多的磁盘空间。

以下代码为ctr部分实现。

// unpack主要代码
// i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform))
// err = i.Unpack(ctx, context.String("snapshotter"))
// image.go
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
    // 省略内容....

	manifest, err := i.getManifest(ctx, i.platform)
	if err != nil {
		return err
	}
    // 获取镜像的所有层
	layers, err := i.getLayers(ctx, i.platform, manifest)
	if err != nil {
		return err
	}

	var (
        // 用于对比镜像的层和快照的层,如果镜像的层和快照的层一致,则不需要创建快照
		a  = i.client.DiffService()
        // 用于更新数据
		cs = i.client.ContentStore()

		chain    []digest.Digest
		unpacked bool
	)
    // 获取snapshotter
	snapshotterName, err = i.client.resolveSnapshotterName(ctx, snapshotterName)
	if err != nil {
		return err
	}
	sn, err := i.client.getSnapshotter(ctx, snapshotterName)
	if err != nil {
		return err
	}
	// 省略内容...

	for _, layer := range layers {
        // 获取镜像的层的数据、创建快照
		unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)
		if err != nil {
			return err
		}

		if unpacked {
			// Set the uncompressed label after the uncompressed
			// digest has been verified through apply.
			cinfo := content.Info{
				Digest: layer.Blob.Digest,
				Labels: map[string]string{
					labels.LabelUncompressed: layer.Diff.Digest.String(),
				},
			}
            // 更新数据库
			if _, err := cs.Update(ctx, cinfo, "labels."+labels.LabelUncompressed); err != nil {
				return err
			}
		}

		chain = append(chain, layer.Diff.Digest)
	}

	// 省略内容....
    // 更新数据库
	_, err = cs.Update(ctx, cinfo, fmt.Sprintf("labels.containerd.io/gc.ref.snapshot.%s", snapshotterName))
	return err
}
// rootts/apply.go
func ApplyLayerWithOpts(ctx context.Context, layer Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) (bool, error) {
    // 省略内容....
    // 以grpc方式获取快照状态,判断是否存在
	if _, err := sn.Stat(ctx, chainID); err != nil {
        // 省略内容....
        // 对比差异, 同步数据
		if err := applyLayers(ctx, []Layer{layer}, append(chain, layer.Diff.Digest), sn, a, opts, applyOpts); err != nil {
			if !errdefs.IsAlreadyExists(err) {
				return false, err
			}
		} else {
			applied = true
		}
	}
	return applied, nil
}

func applyLayers(ctx context.Context, layers []Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) error {
	// 省略内容....

	for {
		key = fmt.Sprintf(snapshots.UnpackKeyFormat, uniquePart(), chainID)

		// Prepare snapshot with from parent, label as root
        // 以grpc方式调用,准备快照
		mounts, err = sn.Prepare(ctx, key, parent.String(), opts...)
		if err != nil {
			if errdefs.IsNotFound(err) && len(layers) > 1 {
                // 递归调用
				if err := applyLayers(ctx, layers[:len(layers)-1], chain[:len(chain)-1], sn, a, opts, applyOpts); err != nil {
					if !errdefs.IsAlreadyExists(err) {
						return err
					}
				}
				// Do no try applying layers again
				layers = nil
				continue
			} else if errdefs.IsAlreadyExists(err) {
				// Try a different key
				continue
			}

			// Already exists should have the caller retry
			return fmt.Errorf("failed to prepare extraction snapshot %q: %w", key, err)

		}
		break
	}
	defer func() {
        // 失败回滚操作
		if err != nil {
			if !errdefs.IsAlreadyExists(err) {
				log.G(ctx).WithError(err).WithField("key", key).Infof("apply failure, attempting cleanup")
			}
            // 以grpc方式调用,删除快照
			if rerr := sn.Remove(ctx, key); rerr != nil {
				log.G(ctx).WithError(rerr).WithField("key", key).Warnf("extraction snapshot removal failed")
			}
		}
	}()
    // 以grpc方式调用,对比,提取数据
	diff, err = a.Apply(ctx, layer.Blob, mounts, applyOpts...)
	if err != nil {
		err = fmt.Errorf("failed to extract layer %s: %w", layer.Diff.Digest, err)
		return err
	}
	if diff.Digest != layer.Diff.Digest {
		err = fmt.Errorf("wrong diff id calculated on extraction %q", diff.Digest)
		return err
	}
    // 以grpc方式调用,提交快照,更新数据库
	if err = sn.Commit(ctx, chainID.String(), key, opts...); err != nil {
		err = fmt.Errorf("failed to commit snapshot %s: %w", key, err)
		return err
	}

	return nil
}

a.Apply,sn.Prepare,sn.Commit等接口,均在congainerd实现。以plugin的方式注册,grpc调用。

diff接口实现。

// services/diff/local.go
type local struct {
    // 用于存储处理函数
	differs []differ
}
// 将快照数据(差异数据)同步到指定位置
func (l *local) Apply(ctx context.Context, er *diffapi.ApplyRequest, _ ...grpc.CallOption) (*diffapi.ApplyResponse, error) {
    // 省略内容....
	for _, differ := range l.differs {
        // 执行同步操作
        // 这里不展开分析
		ocidesc, err = differ.Apply(ctx, desc, mounts, opts...)
		if !errdefs.IsNotImplemented(err) {
			break
		}
	}
    // 省略内容....
	return &diffapi.ApplyResponse{
		Applied: fromDescriptor(ocidesc),
	}, nil
}
// 快照数据比对差异
func (l *local) Diff(ctx context.Context, dr *diffapi.DiffRequest, _ ...grpc.CallOption) (*diffapi.DiffResponse, error) {
    // 省略内容....
	for _, d := range l.differs {
        // 执行对比操作
        // 提供已经存在的挂载(快照),和新的镜像层进行差异比对
        // 这里不展开分析
		ocidesc, err = d.Compare(ctx, aMounts, bMounts, opts...)
		if !errdefs.IsNotImplemented(err) {
			break
		}
	}
	// 省略内容....
	return &diffapi.DiffResponse{
		Diff: fromDescriptor(ocidesc),
	}, nil
}

snapshotter接口实现。

// services/snapshots/service.go
type service struct {
    // Snapshotter的具体实现,上文提到的overlayfs或者native
	ss map[string]snapshots.Snapshotter
	snapshotsapi.UnimplementedSnapshotsServer
}
// 准备快照
// 准备好的快照会交给diff接口,进行数据同步
func (s *service) Prepare(ctx context.Context, pr *snapshotsapi.PrepareSnapshotRequest) (*snapshotsapi.PrepareSnapshotResponse, error) {
	// 省略内容....
    // 返回快照挂载位置,以及当前快照的父快照
    // 默认挂载位置/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/自增数字id/fs
	mounts, err := sn.Prepare(ctx, pr.Key, pr.Parent, opts...)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}

	return &snapshotsapi.PrepareSnapshotResponse{
		Mounts: fromMounts(mounts),
	}, nil
}
// 提交快照
// 提交的快照可以进行使用
func (s *service) Commit(ctx context.Context, cr *snapshotsapi.CommitSnapshotRequest) (*ptypes.Empty, error) {
    // 省略内容....
    // 提交快照
    // 提交后快照将变为active状态
	if err := sn.Commit(ctx, cr.Name, cr.Key, opts...); err != nil {
		return nil, errdefs.ToGRPC(err)
	}

	return empty, nil
}

总结

ctr image pull命令执行可以大致分为两步:

  1. fetch阶段,下载镜像,创建镜像记录
  2. unpack阶段,解压镜像,创建快照

快照的创建中包含差异对比,可以大大减少磁盘空间的占用。

当获取系统中第一个镜像时, 每一个layer都会创建一个快照。
当获取系统中第二个镜像时, 如果第二个镜像的layer和第一个镜像的layer一致,则不会创建快照,只会创建一条镜像记录。

快照创建流程梳理, 以单一layer为例。

准备快照
对比差异
同步数据
提交快照标记可用
image layer
snapshots.Prepare
diff.Diff
diff.Applay
snapshots.Commit

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1242980.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenAI“宫斗”新进展!Sam Altman将重返OpenAI担任首席执行官 董事会成员改动

在经过激烈的五天讨论和辩论之后,高调人工智能初创公司OpenAI宣布,其联合创始人之一Sam Altman将回归担任首席执行官。这一决定是对上周Altman突然被解雇的回应,该决定引起了极大的关注和讨论。 OpenAI表示,他们已经达成了与Altm…

低代码平台推荐:五大低代码厂商谁的模式更“合适”

随着数字化时代的到来,低代码开发平台作为提高数字生产力的工具正受到越来越多企业的关注,市面上的低代码产品和厂商更是“乱花渐欲迷人眼”。 各家产品不仅功能各有不同,甚至商机都有区别的情况,如何做好产品选型已然成了采购企…

2023年国内好用的企业网盘推荐

2023年企业网盘俨然已经成为了各个企业团队的标配了,那么2023年国内有什么好用的企业网盘吗?2023国内哪个企业网盘好用? 国内哪个企业网盘好用? 由于不同行业企业对于企业网盘的具体需求点不同,因此我们无法从功能上评…

idea 2023 安装配置 Gradle8.4

官网:https://gradle.org 下载 Gradle8.4 https://gradle.org/releases/ 解压到本地,到 gradle-8.4\init.d 目录下新建文件:init.gradle 这里有个坑,编译报http协议安全的问题,解决办法,加入&#xff1…

【EI会议征稿】第十一届先进制造技术与材料工程国际学术会议 (AMTME 2024)

JPCS独立出版/高录用快检索/院士杰青云集 第十一届先进制造技术与材料工程国际学术会议 (AMTME 2024) 2024 11th International Conference on Advanced Manufacturing Technology and Materials Engineering 第十一届先进制造技术与材料工程国际学术会议 (AMTME 2024) 定于…

前端处理返回数据为数组对象且对象嵌套数组并重名的数据,合并名称并叠加数据

前端处理返回数据为数组对象且对象嵌套数组并重名的数据,合并名称并叠加数据 var newList[]; var table{}; var dataObj{}; var finalList[]; var tableData[{brName:营业部,dateStr:2023-11-23,tacheArr:[{dealCnt:20,tacheName:奔驰}]},{brName:营业部,dateStr:2…

软件开发团队如何确保团队成员与项目进度一致?

在软件开发团队中,确保团队成员的利益与项目进度保持一致,可以采取以下措施: 建立基础流程:建立几个最主要的流程,如任务跟踪、开发效率提升、任务完成等,可以帮助小团队从无序逐步进入有序。 关注员工反馈…

【逆向】NahamCon CTF -Click Me Writeup(BridaFrida Hook 拿Flag)

文章目录 前言一、样式与功能点二、反编译三、Frida Get Flag四、BurpSuite插件-Brida总结 前言 第一次做外国CTF比赛的mobile challenge 题,题目就是一个安卓的apk安装包。顺便记录一下代码分析过程以及分享两种不同的解题思路。 提示:以下是本篇文章正…

选对软件公司,助力小程序商城腾飞

选择一家合适的软件公司对于小程序商城的开发和运营至关重要。在众多的软件公司中,如何找到最适合自己的合作伙伴呢?本文将从实际需求、公司实力、案例展示、服务态度和价格等五个方面,为您解析如何选择合适的软件公司。 一、明确实际需求 在…

用「超舒适」突围,星纪元 ES 加量不加价

在 2023 广州车展上,星途星纪元 ES 正式开启预售,新车预售区间 24.8-35.8 万元,共推出五款车型配置。 外观方面,星纪元 ES 前脸采用了封闭式的设计,前大灯组采用了流行的贯穿式设计搭配矩阵大灯,使得整个灯…

cefsharp119.4.30(cef119.4.3Chromium119.0.6045.159)版本升级体验支持x86_h264及其他多个h264版本

Cefsharp119.4.30,cef119.4.3,Chromium119.0.6045.159 此更新包括一个高优先级安全更新 This update includes a high priority security update. 说明:此版本119.4.3支持x86_H264视频播放(需要联系我),其他版本。.NETFramework 4.6.2 NuGet Gallery | CefSharp.WinForms …

Selenium(12):层级定位_通过父元素找到子元素

层级定位 在实际的项目测试中,经常会遇到无法直接定位到需要选取的元素,但是其父元素比较容易定位,通过定位父元素再遍历其子元素选择需要的目标元素,或者需要定位某个元素下所有的子元素。 层级定位的思想是先定位父对象&#xf…

外包干了4年,技术算是没了...

先说一下自己的情况。大专生,19年通过校招进入湖南某软件公司,干了接近3年的测试,今年年上旬,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了三年&#xff0c…

企业邮箱即时提醒服务推荐

现在用企业邮箱比较多,但是不能即时提醒,总是误事,什么邮箱可以即时提醒呢?随着工作和生活节奏的加快,传统的电子邮件系统由于不能即时提醒,往往会导致重要邮件的漏接,从而引发一系列的麻烦和误…

java--继承快速入门

1.什么是继承 java中提供了一个关键字extends,用这个关键字,可以让一个类和另一个类建立其父子关系。 2.继承的特点 子类能继承父类的非私有成员(成员变量,成员方法)。 3.继承后对象的创建 子类的对象是由子类、父类共同完成的。 4.继承的…

贝锐向日葵:独家算法如何支撑不断地产品创新?

作为国民级的专业远程控制品牌,贝锐旗下的“向日葵远程控制”一直在远程控制领域不断积累创新。 近年来,远程控制软件本身开始成为企业的主流远程业务工具以及个人场景下的日常必备软件,向日葵远程控制也在不断地完善并创新产品。 比如在前…

全网最详细的安装pytorch GPU方法,一次安装成功!!包括安装失败后的处理方法!

文章目录 前提---查看是否有NVIDIV英伟达显卡【笑哭】一、查看电脑的显卡驱动版本方法一:在cmd命令窗口中输入nvidia-smi,可以发现版本为12.2方法2:点击NVIDIA控制面板→系统信息 二、安装CUDA方法1: 在pytorch官网https://pytorc…

Vue3+Ts实现聊天机器人(chatBot-附代码)

一:项目介绍 本次实验主要涉及到的技术是 Vue3 Ts,当然其中也有部分是 Vue2 格式的代码以及 json 和 CSS 布局等。本来是想仿照 文心一言 来开发的一个聊天机器人案例。结果由于时间不足,可能只是做出来了一个半成品。不过核心功能是有的。由…