Nydus Acceld 去 Containerd 服务化

ba62c947c3b1bc3d8eb5b787794a70d4.gif

丁亚东同学为大家带来他在 GitLink 编程夏令营中参加 Nydus 加速镜像的统一转换框架所做的相关工作分享。

大家好!我是来自大连理工大学的 2023 级研究生丁亚东,GitHub ID 是 @Desiki-high。硕士期间主要研究方向是边缘计算,并且十分热爱云原生技术。

Nydus GitHub:

https://github.com/dragonflyoss/nydus

01

课题背景

1.1 OCI 镜像

39d8d51dd51b866c37a46857f2fb7380.png

OCI v1 镜像格式

OCI V1 镜像格式是目前容器镜像的标准规范。每一个 OCI 镜像由一个 index 文件作为镜像的索引文件,包含一至多个 manifest 文件和 config 文件用于描述不同平台镜像的配置以及使用 layer 的索引等信息,最后是多个 layer 文件包含镜像的具体文件信息。这样的分层结构便于不同镜像间 layer 复用以及增量构建,但同时也导致不同 layer 之间存在重复数据。并且 OCI 镜像规范要求所有 layer 全部拉取到本地后开始创建容器,这在镜像体积较大时会导致拉取镜像时间过长,拖慢容器启动速度,参考 Fast'16 论文[1]。

1.2  Nydus 加速镜像

b34a37fdc799bc422e8a74bf187d167a.png

Nydus 镜像格式

Nydus 是 CNCF 孵化项目 Dragonfly 的子项目,它提供了一种按需加载的容器镜像格式,这使得创建Nydus 容器时无需等待全部数据下载完成便可开始服务。Nydus 基于高性能只读文件系统格式 EROFS 扩展出了 RAFS 文件系统格式,从而将镜像层转换为 RAFS 数据和元数据两大部分。

9c9f40c776e95805e6ace6bed56048f2.png

Nydus 基于 FUSE 的按需加载

Nydus 提供了基于 FUSE 的容器按需加载能力。创建容器时,首先由 Containerd 配合 Nydus Snapshotter 组件拉取 Nydus 镜像的 Metadata 层。之后 Nydus Snapshotter 组件启动 Nydus Daemon 进程,创建 FUSE 挂载点作为容器 RootFS 目录,此时容器已经可以正常启动。Nydus 镜像格式显著提高了容器端到端的性能,更为具体内容可以参考 Nydus 社区[2]。

1.3 Harbor 镜像中心

Harbor 是企业级镜像中心,同时也是 CNCF 毕业项目[3]。它提供了通过策略和基于角色的访问控制以及镜像漏洞扫描等特性。目前 Harbor 支持存储标准 OCI V1 镜像以及符合规范的加速镜像例如 Nydus、eStargz 等。Harbor 提供丰富的 API 接口和 Webhook 设计,可以根据业务需求进行自定义扩展。

1.4 Acceleration-Service 通用转换框架

ed430d64c0a2fb6eff46b988e67d1893.png

Acceld, Dragonfly, Nydus 工作流

Acceleration-Service 是 Harbor 的一个子项目[4],它是一个 Harbor 的通用镜像转换框架[5],由 Accelctl 和 Acceld 组成,其中 Acceld 是一个通过 Harbor Webhook 扩展的服务。在 Harbor 仓库集成 Acceld  之后,当用户推送镜像时可以触发 Webhook 请求,从而通过 Acceld 已集成的 Nydus 转换驱动完成镜像的自动转换。但是目前 Acceld 强依赖本地 Containerd 服务的镜像存储层,需要解决部署繁琐,并发限制等问题,同时需要移除 Containerd 服务依赖并实现镜像存储以及 GC 机制。

02

移除 Containerd 服务依赖

2.1 问题分析

Acceld 的工作流程为:从镜像中心拉取原始镜像到本地,使用转换驱动构建加速镜像,从本地将加速镜像推送到镜像中心。目前 Acceld 借助 Containerd 服务进行镜像的拉取、构建、推送, 具体来说通过  Containerd Client 连接本地服务,调用 Containerd 的服务接口实现对镜像的管理。这使得 Acceld 高度依赖 Containerd 服务,存在部署繁琐和扩展困难的限制。

2.2 解决方案

Acceld 在转换镜像过程中需要借助 Containerd 服务的 Image Service 来管理镜像。Image Service 核心实现是 Content Store(负责未解压的 Blob 管理) 与 Snapshotter(负责提供解压层的挂载方式)。由于镜像转换可以不涉及镜像解压,我们在移除 Containerd 服务之后只需要借助 Containerd Pkg 实现 Content Store 管理 Blob 的能力。下面简单介绍 Containerd 如何对镜像进行管理。Containerd 将容器镜像抽象为 Image 格式如下。

type Image struct {    Name string    Labels map[string]string    Target ocispec.Descriptor    CreatedAt, UpdatedAt time.Time}

Acceld 可以借助 Image 结构以及相关的操作 pkg 将镜像的索引信息保存在内存中,并借助 bolt database[6] 将镜像数据进行持久化。对于具体的本地镜像数据文件的读写操作需要借助 Content.Store interface[7] 实现对镜像的管理,抽象接口如下。

type Store interface {  Ingester  IngestManager  Provider    Manager}

在 Containerd Store 中,Ingester 在 Store 写入 Blob 前,暂存写入中的数据,IngestManager 负责管理Ingester 的活动,例如 list 、about 等。在 Ingester 写入数据之后,Provider 提供读 Store 的方法,例如 io.ReaderAt 和 io.Closer 等。Manager 用于管理之前 Commit 到本地的数据文件,提供 Update 和 Delete 等方法。

2.3 方案实现

设计方案如下图所示,Acceld 接受 HTTP 请求,其中 task 为一次镜像转换任务。

eca15a19c07fd3bbf6dcee91ff3c1912.png

利用 Containerd 提供的 Content Store 拉取、转换和推送镜像的能力来管理本地镜像文件,同时借助 bolt database 记录本地镜像索引信息并持久化。我们基于 Containerd pkg 在上层进行封装初步构造了满足我们功能需求的 Content 结构。

import (  ctrcontent "github.com/containerd/containerd/content"  "github.com/containerd/containerd/metadata")type Content struct {    // db is the bolt database of content  db *metadata.DB  // store is the local content store wrapped inner db  store ctrcontent.Store}

利用 Conetnt.db 管理本地数据库,借助 Containerd 二次封装的 metadata.DB ,我们实现的 Content.db 和 Content.store 是相互关联的,所有拉取、转换和推送的镜像信息都会同步至本地数据库文件。至此我们成功移除了 Containerd 服务依赖并实现了镜像存储。

03

合理的 GC 机制

3.1 问题分析

借助 Containerd pkg 实现了镜像的存储功能,但目前阶段 Acceld 的 GC 使用原生 Containerd 提供的 GC 模式[8] 但是由于 Acceld 只拉取镜像而不运行容器,所以每次调用 content.garbageCollect 时会清空本地所有的镜像文件。这并不符合 Acceld 的需求,Acceld 需要优先删除不常用的 Upper 镜像层数据而保留经常使用的 Base 镜像层。我们需要实现符合 Acceld 需求的 GC 逻辑,即按照 LFRU 的(先 LFU 再 LRU)顺序清理缓存。

3.2 解决方案

首先需要实现对镜像按 Blob 粒度进行清理的能力,然后在此基础上根据 Blob 索引记录历史使用信息,包括最后一次使用时间以及使用次数信息。当本地存储镜像文件大小超过所设置的阈值时,按照先 LFU (使用次数) 后按照 LRU (使用时间) 来进行容器镜像层的清理。为了保证清理的健壮性,我们需要在每次镜像转换任务结束后检测存储大小是否超过阈值,同时设置定时任务来定时监测。

为了实现按 Blob 粒度清理本地缓存,我们需要借鉴 Containerd 中的 GC 管理机制。Containerd 使用 lease[8] 来实现对容器的所有文件的 GC 管理,包括容器镜像。具体来说,Containerd 在 GC 时会避免回收被 lease 管理的容器以及其使用的镜像文件。为了实现 LFRU 的机制,我们需要在 LRU 的基础上进行二次封装来引入镜像使用次数保护 Upper 镜像层数据。

3.3 方案实现

在实现自身容器镜像管理机制的基础上,我们在 bolt 数据库中建立了单独的 bucket 来记录内存中所使用的 lease 以及最后一次使用时间等相关信息。使用 bolt 数据库可以便于持久化信息,在 Acceld 进程故障重启后仍然可以恢复正常的 lease 信息。为了提高 Acceld 的 GC 性能,我们实现了 leaseCache 将需要的 lease 索引信息同步到内存中。

type leaseCache struct {  caches      map[string]*lru.Cache[string, any]  cachesIndex []int  size        int}

具体来说,我们利用自身实现的 Content.Store 接口,在 Commit 等保存镜像过程中对所有的 Blob 设置一个永不过期的 lease 同时记录到 bolt 数据库和 leaseCache 中。在每次 Acceld 读取 Blob 时更新数据库中记录的 usedAtLabel 和 usedCountLabel 信息,并更新 leaseCache 中 caches 中 lease 的顺序。我们将所有内容均封装到 Acceld 的 Content 结构中,最终 Content 结构如下所示。

type Content struct {  // db is the bolt database of content  db *metadata.DB  // lm is lease manager for managing leases using the provided database transaction.  lm leases.Manager  // gcSingleflight help to resolve concurrent gc  gcSingleflight *singleflight.Group  // GcMutex works between gc and convert  GcMutex *sync.RWMutex  // lc cache the used count and reference order of lease  lc *leaseCache  // store is the local content store wrapped inner db  store ctrcontent.Store  // hosts provides remote registry access methods.  hosts remote.HostFunc  // Threshold is the maximum capacity of the local caches storage  Threshold int64}

当程序发现本地存储容量达到所设置阈值的80%时,Acceld 首先会根据 leaseCache 中记录的清理顺序,对 lease 进行清理直到本地存储容量降到到阈值的80%以下。清理 lease 之后, Acceld 使用 db 中封装的 GC 方法实现最终的清理。具体方法实现如下所示,在实际实现过程中需要借助 Singleflight 以及 RWMutex 来实现在高并发场景下的 GC 能力。

func (content *Content) GC(ctx context.Context, threshold int64) {  size, err := content.Size()  if err != nil {    logrus.Error(errors.Wrap(err, "gc get content size"))    return  }  if size > (threshold*gcPercent)/100 {    content.gcSingleflight.Do(accelerationServiceNamespace, func() (interface{}, error) {      content.GcMutex.Lock()      defer content.GcMutex.Unlock()      size, err := content.Size()      if err != nil {        logrus.Error(errors.Wrap(err, "gc get content size"))        return nil, nil      }      if err := content.garbageCollect(ctx, size-(threshold*gcPercent)/100); err != nil {        logrus.Error(errors.Wrap(err, "gc"))      }      return nil, nil    })  }}

04

项目总结与展望

4.1 为何参加活动  

非常荣幸,我可以参加本次 GitLink 编程夏令营。在今年上半年,我参与了 Nydus 项目关于容器文件预取的部分工作,积累了一些开源工作的经验。在与严松老师的沟通过程中了解到 GitLink 编程夏令营,我认为这是一个很好的学习机会。于是便投递了 Acceld 项目的简历申请,最终在严松老师的帮助下顺利完成了题目。

4.2 遇到的困难与挑战  

在项目开发过程中,遇到的最大的困难是如何实现合理的 GC 机制。我需要借鉴 Containerd GC 来实现 Acceld 的 GC ,并在此基础上实现 LFRU 的缓存清理策略。而 Containerd 项目十分复杂,它的 GC 机制非常健壮,我需要深入阅读 Containerd 项目源码来理解关于 label 和 lease 之间的管理机制。在确定实现思路之后,如何实现也是一个很大的挑战。例如在初步实现 GC 之后,我发现在清理过程中并不会按照我设计的 Blob 粒度清理,而是一次性清理一个镜像中的所有 Blob。经过继续验证之后,发现 Containerd 默认在使用 Content Store 保存本地镜像文件时会写入一些影响 GC 清理顺序的 label。实际上这些 label 是服务于容器管理,所以我们决定在 Content 写入镜像层时对 label 进行过滤,最终实现了按 Blob 粒度的缓存清理。

4.3 社区与导师的帮助  

在本次 GitLink 编程夏令营过程中,社区 maintainer 对我提出的疑问以及遇到的问题进行了仔细的解答。在设计阶段,在我的设计基础上提出建议,帮助我一步一步完善架构设计。在编码阶段,导师对我的 PR 进行了多次的 Code Review,帮助我提高代码质量。

4.4 收获与展望  

在本次编程夏令营中,我不仅进行了核心功能的开发工作,同时也参与了社区 CI 和 文档的维护工作。在PR 的 Review 中提高了自身的代码能力和思考能力,在这里我也要再次向导师以及社区表示衷心的感谢。这是我第一次深入地接触开源社区,社区对开源新手非常友好,会仔细指出我的各种错误,并给予详细的帮助和修改意见。今后,我也会继续参与到社区的开源工作之中,在社区中继续提高自身能力。GitLink 编程夏令营是一个适合新人的平台,如果你对社区中任何项目感兴趣,欢迎加入。

参考

[1].Fast'16

https://www.usenix.org/node/194431

[2].Nydus

https://github.com/dragonflyoss/nydus

[3].Harbor

https://goharbor.io/

[4].Acceld

https://github.com/goharbor/acceleration-service

[5].Harbor 构建高效的镜像加速工作流

https://mp.weixin.qq.com/s/pc_jL77kR5SFzruL1njynw

[6].Bolt Database

https://github.com/etcd-io/bbolt

[7].Containerd Content

https://github.com/containerd/containerd/blob/main/content/content.go

[8].Containerd GC

https://github.com/containerd/containerd/blob/main/docs/garbage-collection.md

GitLink 编程夏令营是在 CCF 中国计算机学会指导下,由 CCF 开源发展委员会(CCF ODC)举办的面向全国高校学生的暑期编程活动。这是 2023 的夏令营活动中,丁亚东同学参加 Nydus 开源项目的总结,主要介绍为支持 Nydus 等加速镜像的统一转换框架所做的相关工作。

推荐阅读

3ae848c728addcbf5630f7e16a7d9fcb.png

Dragonfly 发布 v2.1.0 版本!

b83a544f0bac5362f41a9b7c7e1ef428.jpeg

火山引擎基于 Dragonfly 加速实践

e9258ca48adc012080c186085d1314a1.jpeg

Dragonfly 基于 P2P 的文件和镜像分发系统

780aafccf68e130baae52e181feb6137.jpeg

Dragonfly 和 Nydus Mirror 模式集成实践

25495a2d17b9bfe6833fc7aa5d14fe26.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值