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 againif 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.Digestunpacked bool)// 获取snapshottersnapshotterName, 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 againlayers = nilcontinue} else if errdefs.IsAlreadyExists(err) {// Try a different keycontinue}// Already exists should have the caller retryreturn 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或者nativess map[string]snapshots.Snapshottersnapshotsapi.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/fsmounts, 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.mzph.cn/news/163883.shtml

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

相关文章

《人件》读书笔记

文章目录 一、书名和作者二、书籍概览2.1 主要论点和结构2.2 目标读者和应用场景 三、核心观点与主题3.1 管理团队主题3.2 改善工作环境主题3.3 正确的人主题3.4 团队项目管理主题 四、亮点与启发4.1 最有影响的观点4.2 对个人专业发展的启示 五、批评与局限性5.1 可能存在争议…

leetcode (力扣) 97. 交错字符串(动态规划)

文章目录 题目描述思路分析完整代码 题目描述 给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。 两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串: s s1 s2 … sn t …

数据库——查询连续的月份

一、GP或PGSQL with recursive t(n) as (select date(2023-01-01) union all select n1 from t where n < now()) select to_char(n, yyyy-mm) as ny from t group by ny order by ny 二、Hive select add_months(FROM_UNIXTIME(unix_timestamp(SUBSTR(start_date, 1, 7…

rdf-file:组件内置协议(SP、DE、FUND、FUND_INDEX)

Rdf-File根据协议布局模板和数据定义模板,来进行文件的解析与生成。通过协议布局和数据定义模板&#xff0c;能够明确计算出头尾占用的行数&#xff0c;这样可以更精确的分离出head&#xff0c;body&#xff0c;tail。 目前组件实现的协议布局模板可以分为如下两大类&#xff…

【深度学习实验】图像处理(一):Python Imaging Library(PIL)库:图像读取、写入、复制、粘贴、几何变换、图像增强、图像滤波

文章目录 一、实验介绍二、实验环境1. 配置虚拟环境2. 库版本介绍 三、实验内容0. 安装 PIL 库1. 图像读取和写入a. 图像读取b. 图像写入c. 构建新图像 2. 图像复制粘贴a. 图像复制b. 图像局部复制c. 图像粘贴 3. 几何变换a. 图像调整大小b. 图像旋转c. 图像翻转 4. 图像增强a.…

cocos creator中AStar算法实例

引擎版本 —— cocos creator2.3.4

高端猫罐头有哪些?精选的5款优质的猫罐头推荐!

很多铲屎官看猫猫吃猫粮吃腻了&#xff0c;或者猫猫平时不喜欢喝水&#xff0c;又或者看猫猫太瘦了就想入手几款猫罐头但是又愁于不会选择&#xff0c;而且现在猫罐头风这么大不知道选什么好~ 作为一个开宠物店7年的店长&#xff0c;对于猫咪的饮食健康我一直都很重视&#xff…

数据结构与算法编程题16

已知长度为n的线性表A&#xff0c;请写一时间复杂度为O(n)、空间复杂度为O(1)的算法&#xff0c;该算法删除线性表中所有值为item的数据元素。 item 3 数组下标 i 0 1 2 3 4 5 6 7 8 顺序表&#xff1a; 1 2 3 4 3 3 5 3 7 #include <iostream> using namespace std;typ…

Linux常用基础命令及重要目录,配置文件功能介绍

目录 一&#xff0c;Linux常用必备基础命令 1&#xff0c;网络类命令 2&#xff0c;文件目录类命令 3&#xff0c;操作类命令 4&#xff0c;关机重启命令 5&#xff0c;帮助命令 6&#xff0c;查看显示类命令 7&#xff0c;命令常用快捷键 二&#xff0c;Linux重要目录…

文献阅读的六个步骤

对于科研人员来说&#xff0c;文献阅读是一项重要的任务&#xff0c;当然要找对方法和步骤&#xff0c;可以按照这些步骤来哦。 1、确定研究问题和目标&#xff1a;在开始阅读外文文献之前&#xff0c;明确您的研究问题和目标。这将帮助您更好地理解作者所说的话以及他们提出的…

windows版本的grafana如何离线安装插件

本文以安装clickhouse的插件为例&#xff0c;记录下如何离线安装插件 1 下载插件 ClickHouse plugin for Grafana | Grafana Labs 2 找到grafana的配置文件 打开编辑&#xff0c;搜索plugin关键字&#xff0c;修改plugin的加载目录 目录不存在&#xff0c;手动创建&#xff0…

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题 文章目录 【Python】Vscode解决Python中制表符和空格混用导致的缩进问题1. 问题来源2. 解决Reference 1. 问题来源 在python中使用缩进来进行代码块的分区&#xff0c;通常来说python的一个缩进包含4个空格&#…

软件测试岗位与职业发展

一、软件测试岗位有哪些&#xff1f; 在企业中&#xff0c;软件测试领域的几个典型的职位有&#xff1a;功能测试工程师&#xff08;也叫手工测试&#xff09;、自动化测试工程师、性能测试工程师、测试开发等。 1、功能测试&#xff08;手工测试&#xff09;工程师 主要工作内…

2023年“福建省工业互联网+智能制造创新大赛”开启报名

11月22日&#xff0c;由福建省总工会、福建省大数据集团有限公司共同举办的2023年“福建省工业互联网智能制造创新大赛”启动报名。 大赛积极响应《福建省总工会等八部门关于广泛深入开展劳动和技能竞赛为新发展阶段新福建建设建工立业的意见》&#xff08;闽工〔2022〕70号&am…

mongodb数据库的常用操作语句

说在前面的话 本文所有的操作示例&#xff0c;都以集合“HistoryTaskBase”为例。 一、查询 1、时间区间 查询“通知时间”介于2019-09-01到2019-10-01之间的数据。 db.getCollection(HistoryTaskBase).find({notifyTime:{$gte:ISODate(2019-09-01T00:00:00.000Z),$lte:ISOD…

基于STM32的电影院安全系统的设计与实现(论文+源码)

1.系统设计 本次基于STM32F4的电影院安全系统的设计与实现&#xff0c;以STM32F4单片机为核心控制器&#xff0c;配合人体红外传感器&#xff0c;烟雾传感器&#xff0c;甲醛传感器等硬件设施&#xff0c;实现了对电影院内环境的检测&#xff0c;当出现异常则会通过蜂鸣器和LE…

实现了一个简易的计算器

计算器的界面如下&#xff1a; 实现过程&#xff1a; 通过html和css编写这样一个界面JavaScript实现功能 在通过JavaScript实现计算器功能的过程中&#xff0c;其实使用的都是一些基本指数。主要包括以下几点&#xff1a; If/else 分支.For 循环JavaScript 函数箭头函数&…

日志分析对决:揭示 ELK 与 GrayLog 的优势和差异

&#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 日志分析对决&#xff1a;揭示 ELK 与 GrayLog 的优势和差异 前言第一&#xff1a;ELK Stack简介第二&#xff1a;GrayLog简介架构&#xff1a;主要特性&#xff1a; 第三&am…

腾讯待办关停,怎么在手机上记录待办清单并设置提醒?

如果你之前一直在腾讯待办这款微信小程序中记录待办事项&#xff0c;那么你就会发现小程序中弹窗的“业务关停通知”弹窗&#xff0c;查看其中的内容可知&#xff0c;由于业务方向调整&#xff0c;腾讯待办将于2023年12月20日全面停止运营并下架&#xff0c;这就意味着我们无法…

面试:MyBatis问题

文章目录 什么是MyBatis?MyBatis的核心组件有哪些&#xff1f;能说说MyBatis的工作原理吗&#xff1f;MyBatis的工作流程是怎样的&#xff1f;Mybaits 的优点 & 缺点MyBatis 与 JPA 有哪些不同&#xff1f;MyBatis一二级缓存的区别&#xff1f;MyBatis如何处理延迟加载&am…