containerd Snapshots功能解析

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《微信小程序开发从入门到实战》学习二十五

3.3 开发创建投票页面 3.3.13 使用页面路径参数 写了很多重复代码,现在想办法将多选和单选投票页面合二为一。 将单选页面改造作为单选多选共同页面。 修改index.js中的代码,将路径都跳转到第一个单选页面,带上单选或多选的标志&#xff…

常见的四种需求分析方法

需求分析是软件开发项目中非常重要的一环,而适当的需求分析方法可以帮助开发团队更好地理解用户需求,准确定义系统的功能和性能要求。通过使用这些方法,开发团队可以更好地规划和管理项目,减少需求变更和返工的风险。 如果缺乏适当…

云贝教育 |【喜报】同学们年末冲刺考试了!恭喜本月MySQL和oracle的考试同学 同一天顺利下证

恭喜MySQL的kang同学和oracle的wang同学本月考试通关,都顺利下证 悄悄的说 : 这月有MySQL优惠试卷,培训考试也有特惠价~ Oracle培训考试年末特别价 保证惊喜!! 最后祝同学们都顺利过关!早日下证&#xf…

【攻防世界-misc】pure_color

1.方法一:用画图工具打开图片,将图片拷贝至虚拟机win7桌面, 点“属性”,颜色设置为“黑白”, 出现flag值。 2.方法二:使用Stegsilve打开,分析图片 将图片打开,按左右键查找&#xff…

String 真的不可变吗?

为什么 String 类不可变 final修饰符: String类被声明为final,这意味着它不能被继承。因此,无法创建String的子类来修改其行为。私有字符数组(char[]): String类内部使用私有的字符数组来存储字符串的内容…

TypeError: Cannot read property ‘sendpost‘ of undefined

箭头函数指向问题,定义let that this 解决

分享5款工作和学习中,经常用到的软件

​ 如今,工作和学习都离不开电脑,所以电脑里的软件自然也是必不可少的,但是电脑软件那么多,不可能每个都装上吧,所以我们要装好用的、实用的,下面给大家分享5款好用到爆的软件,很多懂电脑的人都…

使用支付宝的沙箱环境在本地配置模拟支付并发布至公网调试

文章目录 前言1. 下载当面付demo2. 修改配置文件3. 打包成web服务4. 局域网测试5. 内网穿透6. 测试公网访问7. 配置二级子域名8. 测试使用固定二级子域名访问9. 结语 前言 在沙箱环境调试支付SDK的时候,往往沙箱环境部署在本地,局限性大,在沙…

java--ArrayList快速入门

1.什么是集合&#xff1f; 集合是一个容器&#xff0c;用来装数据的&#xff0c;类似于数组。 2.有数组&#xff0c;为啥还学习集合 ①数组定义完成并启动后&#xff0c;长度是固定了。 ②集合大小可变&#xff0c;开发中用的更多。 3.ArrayList<E> 是用的最多、最…

OpenStack-train版安装之基础组件安装

基础组件安装 安装MariaDB&#xff08;数据库&#xff09;安装RabbitMQ&#xff08;消息队列&#xff09;安装Memcached&#xff08;缓存&#xff09; 安装MariaDB&#xff08;数据库&#xff09; 安装 # yum install mariadb mariadb-server python2-PyMySQL -y数据库配置 …

公益众筹模式源码模式:水滴筹模式 实现社会价值的最大化 附带完整的搭建教程

随着社会的进步和互联网技术的发展&#xff0c;公益众筹作为一种有效的筹款方式&#xff0c;越来越受到人们的关注。其中&#xff0c;水滴筹模式以其独特的运营方式和强大的社交功能&#xff0c;逐渐成为了公益众筹领域的一种重要模式。该源码系统就是在这样的背景下应运而生&a…

洗地机哪个牌子好用?洗地机选购攻略

传统的清洁方式都是扫把拖把的结合&#xff0c;既繁琐也劳累&#xff0c;每次清洁完后还得累的腰酸背痛的&#xff0c;像厨房这种地方甚至会不容易清洁干净&#xff0c;总感觉地板灰蒙蒙的。洗地机的诞生就很好的解决了这些问题&#xff0c;不用一遍遍的重复扫地拖地擦地&#…

vue一个页面左边是el-table表格 当点击每条数据时可以在右边界面编辑表格参数,右边保存更新左边表格数据

实现思路&#xff1a; 1.点击当前行通过row拿到当前行数据。 2.将当前行数据传给子组件。 3.子组件监听父组件传过来的数据并映射在界面。 4.点击保存将修改的值传给父组件更新表格。 5.父组件收到修改过后的值&#xff0c;可以通过字段判断比如id&#xff0c;通过 findIn…

volatile 详解

目录 一. 前言 二. 可见性 2.1. 可见性概述 2.2. 内存屏障 2.3. 代码实例 三. 不保证原子性 3.1. 原子性概述 3.2. 如何解决 volatile 的原子性问题呢&#xff1f; 四. 禁止指令重排 4.1. volatile 的 happens-before 关系 4.2. 代码实例 五. volatile 应用场景 5…

JOSEF 漏电继电器 LLJ-100FG φ45 50-500mA 卡轨安装

系列型号&#xff1a; LLJ-10F(S)漏电继电器LLJ-15F(S)漏电继电器LLJ-16F(S)漏电继电器 LLJ-25F(S)漏电继电器LLJ-30F(S)漏电继电器LLJ-32F(S)漏电继电器 LLJ-60F(S)漏电继电器LLJ-63F(S)漏电继电器LLJ-80F(S)漏电继电器 LLJ-100F(S)漏电继电器LLJ-120F(S)漏电继电器LLJ-125F(S…

Linux基础命令4

find查找操作 1.文件名 上图中&#xff0c;一共有4个部分&#xff0c;分别是find&#xff0c;搜索路径&#xff0c;-name&#xff0c;文件名 find加上文件的路径&#xff08;也就是要查找的文件在根目录下的usr目录下的bin目录底下&#xff09; 加上 -name 加上文件名&a…

如何用网格交易做ETF套利

ETF套利是指利用ETF基金的交易机制&#xff0c;通过短期的买卖差价或组合投资来获取利润。 具体来说&#xff0c;ETF套利最常用的套利方法则是&#xff1a;价格套利和波动套利。 1. 价格套利&#xff1a;当ETF二级市场的价格与一级市场的净值出现偏差时&#xff0c;投资者可以通…

消息中间件——RabbitMQ(五)快速入门生产者与消费者,SpringBoot整合RabbitMQ!

前言 本章我们来一次快速入门RabbitMQ——生产者与消费者。需要构建一个生产端与消费端的模型。什么意思呢&#xff1f;我们的生产者发送一条消息&#xff0c;投递到RabbitMQ集群也就是Broker。 我们的消费端进行监听RabbitMQ&#xff0c;当发现队列中有消息后&#xff0c;就进…

CS2的到来会对csgo产生什么影响?

从左手持枪到教练观战位&#xff0c;周四更新的CS新版本缺乏CSGO里很多关键功能。社区服务器和创意工坊地图&#xff0c;目前最重要的功能缺失是创意工坊地图和社区服务器。这些社区制作的地图长期以来一直是玩家磨练技能的首选场所&#xff0c;从死斗服务器到用来练习瞄准、跑…

动态loading

项目中需要用到动图loading的地方可以下载 https://www.intogif.com/loading/ 高级点的还有css动画;692 Loaders: CSS & Tailwind 692 Loaders: CSS & Tailwind