containerd Snapshots功能解析

2023-11-25 00:01

本文主要是介绍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

这篇关于containerd Snapshots功能解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/422766

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Spring框架5 - 容器的扩展功能 (ApplicationContext)

private static ApplicationContext applicationContext;static {applicationContext = new ClassPathXmlApplicationContext("bean.xml");} BeanFactory的功能扩展类ApplicationContext进行深度的分析。ApplicationConext与 BeanF

JavaFX应用更新检测功能(在线自动更新方案)

JavaFX开发的桌面应用属于C端,一般来说需要版本检测和自动更新功能,这里记录一下一种版本检测和自动更新的方法。 1. 整体方案 JavaFX.应用版本检测、自动更新主要涉及一下步骤: 读取本地应用版本拉取远程版本并比较两个版本如果需要升级,那么拉取更新历史弹出升级控制窗口用户选择升级时,拉取升级包解压,重启应用用户选择忽略时,本地版本标志为忽略版本用户选择取消时,隐藏升级控制窗口 2.

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第二题解析 未完

一、题目阅读 (最大值之和)给定整数序列 a0,⋯,an−1,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤105 和 1≤ai≤108。 一个序列的非空连续子序列可以用两个下标 ll 和 rr(其中0≤l≤r<n0≤l≤r<n)表示,对应的序列为 al,al+1,⋯,ar​。两个非空连续子序列不同,当且仅当下标不同。 例如,当原序列为 [1,2,1,2] 时,要计算子序列 [