关于javascript:Nerdctl-原生支持-Nydus-加速镜像

45次阅读

共计 10244 个字符,预计需要花费 26 分钟才能阅读完成。

文|李楠(GitHub ID : @loheagn)

北京航空航天大学 21 级研究生

云原生底层零碎的开发和摸索工作。

本文 6369 字 浏览 16 分钟

OSPP 开源之夏是由中科院软件研究所“开源软件供应链点亮打算”发动并长期反对的一项暑期开源流动。旨在激励在校学生积极参与开源软件的开发保护、促成优良开源软件社区的蓬勃发展、造就和挖掘更多优良的开发者。

这是去年(2022)的开源流动中,李楠同学加入 Nydus 容器开源生态集成课题的相干工作。

| 作者有话说 |

大家好!我是来自北京航空航天大学的 2021 级研究生李楠,对云原生技术很感兴趣,GitHub ID 是 @loheagn。在往年上半年,我加入了 Linux 基金会的秋季实习,实现了 CNCF – Kubevela: Management of Terraform state 我的项目的工作,并因而造就了对开源工作的趣味。在开源之夏 2022 凋谢题目后,我理解到了 Nydus 我的项目,感觉这是一个很酷的我的项目,因为我之前对容器和镜像的底层技术并不是很相熟,感觉这是个很不错的学习机会。于是便尝试投递了简历申请,最终很幸运地通过了筛选,并在严松老师的帮忙下顺利完成了题目。

PART. 1 我的项目背景

Nerdctl

Nerdctl 是一个对标 Docker CLI 和 Docker Compose 的、用于与 Containerd (当下最风行的容器运行时,Docker 的后端也是调用的 Containerd,通常作为守护过程呈现) 交互的命令行工具。

用户能够像应用 Docker CLI 一样应用 Nerdctl 与 Containerd 进行交互,比方应用 nerdctl pull <image_name> 来拉取镜像、应用 nerdctl run <image_name> 来运行容器等等。

相比于 Containerd 自身提供的 CTR 工具,Nerdctl 默认提供了更敌对的用户体验,并尽量放弃其应用形式与 Docker 统一。对于从 Docker 迁徙到 Containerd 的用户,往往只须要 alias docker=nerdctl 就能够与之前取得统一的应用体验。

OCI 镜像格局

OCI 镜像格局是 OCI (Open Container Initiative,凋谢容器打算) 的重要组成部分。它给出了一个厂商无关的镜像格局标准,即一个镜像应该蕴含哪些局部、每个局部的数据结构是如何的、这些各个局部应该以怎么的形式进行组织等等。

OCI 镜像格局脱胎于 Docker 镜像格局,它与 Docker 镜像格局有着十分相似的构造;但它比 Docker 镜像格局有更好的兼容性,并失去了各个厂商的广泛认同。

因而,在这里次要介绍一下 OCI 镜像格局的次要内容。

通常所说的镜像文件其实指的是一个蕴含了多个文件的“包”,“包”中的这些文件提供了启动一个容器所须要的所有须要信息,其中包含但不限于,容器所应用的文件系统等数据文件,镜像所实用的平台、数据完整性校验信息等配置文件。当咱们应用 docker pull 或 nerdctl pull 从镜像核心拉取镜像时,其实就是在顺次拉取该镜像所蕴含的这些文件。

例如,当咱们应用 nerdctl pull 拉取一个 OCI 镜像时:

从 Log 中能够清晰地看到,Nerdctl 顺次拉取了一个 index 文件、一个 manifest 文件、一个 config 文件和若干个 layer 数据文件。实际上,一个规范的 OCI 镜像通常就是由这几局部形成的。

其中,layer 文件个别是 tar 包或者压缩后的 tar 包,其蕴含着镜像具体的数据文件。这些 layer 文件会独特组成一个残缺的文件系统( 也就是从该镜像启动容器后,进入容器中看到的文件系统)

config 文件是一个 JSON 文件。其中蕴含镜像的一些配置信息,比方镜像工夫、批改记录、环境变量、镜像的启动命令等等。

manifest 文件也是一个 JSON 文件。它能够看作是镜像文件的清单,即阐明了该镜像蕴含了哪些 layer 文件和哪个 config 文件。

上面是一个 manifest 文件的典型例子:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:0584b370e957bf9d09e10f424859a02ab0fda255103f75b3f8c7d410a4e96ed5",
    "size": 7636
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:214ca5fb90323fe769c63a12af092f2572bf1c6b300263e09883909fc865d260",
      "size": 31379476
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:50836501937ff210a4ee8eedcb17b49b3b7627c5b7104397b2a6198c569d9231",
      "size": 25338790
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:d838e0361e8efc1fb3ec2b7aed16ba935ee9b62b6631c304256b0326c048a330",
      "size": 600
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:fcc7a415e354b2e1a2fcf80005278d0439a2f87556e683bb98891414339f9bee",
      "size": 893
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:dc73b4533047ea21262e7d35b3b2598e3d2c00b6d63426f47698fe2adac5b1d6",
      "size": 664
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:e8750203e98541223fb970b2b04058aae5ca11833a93b9f3df26bd835f66d223",
      "size": 1394
    }
  ]
}

index 文件也是一个 JSON 文件。它是可选的,能够被认为是 manifest 的 manifest。试想一下,一个 tag 标识的镜像,比方 docker.io/library/nginx:1.20,会针对不同的架构平台 (比方 linux/amd、linux/arm64 等等) 有不同的镜像文件,每个不同平台的镜像文件都有一个 manifest 文件来形容,那么咱们就须要有个更高层级的文件来索引这多个 manifest 文件。

比方,docker.io/library/nginx:1.20 的 index 文件就蕴含一个 manifests 数组,其中记录了多个不同平台的 manifest 的根本信息:

{
  "manifests": [
    {
      "digest": "sha256:a76df3b4f1478766631c794de7ff466aca466f995fd5bb216bb9643a3dd2a6bb",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      },
      "size": 1570
    },
    {
      "digest": "sha256:f46bffd1049ef89d01841ba45bb02880addbbe6d1587726b9979dbe2f6b556a4",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm",
        "os": "linux",
        "variant": "v5"
      },
      "size": 1570
    },
    {
      "digest": "sha256:d9a32c8a3049313fb16427b6e64a4a1f12b60a4a240bf4fbf9502013fcdf621c",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm",
        "os": "linux",
        "variant": "v7"
      },
      "size": 1570
    },
    {
      "digest": "sha256:acd1b78ac05eedcef5f205406468616e83a6a712f76d068a45cf76803d821d0b",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm64",
        "os": "linux",
        "variant": "v8"
      },
      "size": 1570
    },
    {
      "digest": "sha256:d972eee4f12250a62a8dc076560acc1903fc463ee9cb84f9762b50deed855ed6",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "386",
        "os": "linux"
      },
      "size": 1570
    },
    {
      "digest": "sha256:b187079b65b3eff95d1ea02acbc0abed172ba8e1433190b97d0acfddd5477640",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "mips64le",
        "os": "linux"
      },
      "size": 1570
    },
    {
      "digest": "sha256:ae93c7f72dc47dbd984348240c02484b95650b8b328464c62559ef173b64ce0d",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      },
      "size": 1570
    },
    {
      "digest": "sha256:51f45f5871a8d25b65cecf570c6b079995a16c7aef559261d7fd949e32d44822",
      "mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "s390x",
        "os": "linux"
      },
      "size": 1570
    }
  ],
  "mediaType": "application\/vnd.docker.distribution.manifest.list.v2+json",
  "schemaVersion": 2
}

综上,组成镜像的各个文件相互之间造成了一个树状构造,树的下层节点持有对上层节点的援用。从最上层的 index 文件或 manifest 文件开始,就能够“顺藤摸瓜”地索引到镜像的所有文件。

须要留神的是,OCI 镜像标准是一个凋谢的格局,它只规定了文件的组织模式,但没规定数据文件的具体内容。咱们齐全能够将一些其余类型的文件打包成一个 OCI 镜像的格局,当做 OCI 镜像进行散发,从而充分利用 DockerHub 等镜像注册核心的能力。

PART. 2 对于 Nydus

Nydus 是 CNCF 孵化我的项目 Dragonfly 的子项目,它提供了容器镜像、代码包、数据分析按需加载的能力,无需期待整个数据下载实现便可开始服务。

Nydus 在生产环境曾经撑持了每日百万级别的减速镜像容器创立,在启动性能、镜像空间优化、网络带宽效率、端到端数据一致性等方面相比 OCI v1 格局有着微小劣势,并可扩大至例如 NPM 包懒加载等数据散发场景。

目前 Nydus 由蚂蚁团体、阿里云、字节跳动联合开发,Containerd、Podman 社区承受了 Nydus 运行时作为其社区子项目,也是 Kata Containers 以及 Linux v5.19 内核态原生反对的镜像减速计划。

Nydus 镜像格局

Nydus 镜像格局是对下一代 OCI 镜像格局的摸索。它的呈现是基于以下的事实:在用户启动容器时,容器运行时会首先从近程 Registry 中下载残缺的镜像文件 (通常这一过程是容器启动时最耗时的局部),而后能力对镜像的文件系统进行解包和挂载,最初实现容器的启动。

但实际上,用户在运行容器过程中,并不会用到文件系统中的全副文件,数据使用率通常只有 6% 左右;也就是说,破费大量工夫拉取的镜像文件,却大概率最终不会用到。

因而,如果能在容器运行时,不提前拉取残缺镜像,而只是在须要拜访某些文件再动静拉取,将大大提高容器的启动效率,并带来网络带宽效率、镜像空间优化等更多益处。

下图是雷同内容的 Nydus 镜像和 OCI 镜像在创立容器时的耗时比照:

Nydus 镜像格局并没有对 OCI 镜像格局在架构上进行批改,而次要优化了其中的 layer 数据层的数据结构。Nydus 将本来对立寄存在 layer 层的文件数据和元数据 (文件系统的目录构造、文件元数据等) 离开,别离寄存在“Blob layer”和“Bootstrap layer”中。并对 Blob layer 中寄存的文件数据进行分块 (chunk),以便于懒加载 (在须要拜访某个文件时,只须要拉取对应的 chunk 即可,不须要拉取整个 Blob layer)

同时,这些分块信息,包含每个 chunk 在 Blob layer 的地位信息等也被寄存在 Bootstrap layer 这个元数据层中。这样,容器启动时,仅需拉取 Bootstrap layer 层,当容器具体拜访到某个文件时,再依据 Bootstrap layer 中的元信息拉取对应 Blob layer 中的对应的 chunk 即可。

从更下层的视角上,Nydus 镜像格局相比于 OCI 镜像格局的变动:

能够看到,Nydus 镜像对外仍旧能够体现出 OCI 镜像格局的组织模式,因而,Nydus 镜像能够充分利用原有的 Docker 镜像和 OCI 镜像的存储散发的生态。

Nydus Daemon

从前文的探讨中能够看出,从 Nydus 镜像生成的容器,当其拜访文件系统中的文件时,实际上拜访的不是文件系统,而是一个“网络文件系统”——理论拜访的是这个“网络文件系统”在本地的缓存或 Registry 等存储后端中的数据。

Nydus 中作为这个“网络文件系统”中的“本地客户端”的工具是 Nydus Daemon (简称 Nydusd)

下图中的 Nydus Framework 中起次要作用的就是 Nydusd。

Nydusd 是一个用户态过程,它能够通过 FUSE、VirtioFS 或 EROFS 等形式将网络文件系统挂载到容器的 Rootfs 上。

下图是应用 FUSE 的状况:

Nydus Snapshotter

Nydus 镜像格局尽管在架构上与 OCI 格局保持一致,但对数据的解析、具体的 layer 文件的 MediaType 等方面与 OCI 格局有很大区别。现有的容器运行时 (典型的如 Containerd、Podman、CRI-O 等) 及其配套的工具并不能间接与拉取和运行 Nydus 镜像。

例如,对于 Containerd:

在 Containerd 拉取镜像时,会依据镜像的 manifest 中的形容将所有的层文件都拉取下来。这首先就失去了 Nydus 按需加载的意义。

Containerd 在实现镜像每个层文件的拉取后,会调用 Snapshot 服务将每一层解包,以读取其中的文件。但 Nydus 镜像的 Blob layer 应用了自定义的 MediaType,Containerd 在解决时会间接报错。

Containerd 在运行容器时,会将镜像所属的各个解包后的 Snapshot 目录作为 OverlayFS 的 Lower Dir,挂载到容器的 Rootfs 上。Nydus 必须可能 hack 这一过程,将 Nydusd 提供的网络文件系统作为 OverlayFS 的 Lower Dir 挂载。

侥幸的是,Containerd 在设计之初就思考到了对多种文件系统的反对,反对用户自定义 Snapshot 插件,并在拉取和运行镜像时指定应用对应的 Snapshot 插件,以实现用户所冀望的性能。Nydus 所提供的这样的 Snapshot 插件就是 Nydus Snapshotter。

PART. 3

我的工作

Nerdctl 反对运行 Nydus 镜像

在开源之夏的题目公布时,当须要应用 Nerdctl 来运行 Nydus 镜像时,不能像一般的 OCI 镜像或 Docker 镜像一样,间接应用 nerdctl run 等来运行 (执行 nerdctl run 会间接报错),必须首先应用 Nydus 本人提供的工具 ctr-remote 拉取镜像,而后能力进一步应用 nerdctl run 运行。

通过浏览和调试 Nerdctl 的代码发现,当本地没有要运行的镜像时,Nerdctl 会执行 pull 命令从 Registry 中拉取镜像,而间接应用 nerdctl run 引发的报错正是在这个 pull 阶段产生的。因而,问题转化为解决 nerdctl pull 的报错。同时,想到 ctr-remote 其实次要就是在做 pull 镜像的性能。于是,进一步浏览 ctr-remote 的代码能够发现,ctr-remote 通过在镜像的拉取过程中,对 manifest 蕴含的各个 layer 增加相应的 annotation,使 Nydus Snapshotter 能够正确处理拉取的镜像。因而,只须要将 ctr-remote 中的这部分逻辑抽离进去,并增加到 nerdctl pull 的工作流程中即可。

Nerdctl 反对转换 Nydus 镜像

相比于 Docker CLI,Nerdctl 原生反对一些更多样化的命令,比方 nerdctl image convert。顾名思义,该命令的作用是将一种格局的镜像转换为另一种格局。其最根底的用法是应用 nerdctl image convert --oci <src_image_tag> <target_image_tag> 将一个常见的 Docker v2 格局的镜像 (也就是大家平时用的镜像格局) 转换为一个规范的 OCI 格局的镜像。

除了 Docker v2 和 OCI,截止到开源之夏题目公布时,nerdctl image convert 还反对将镜像转换到 eStargz 格局。因而,“nerdctl image convert 反对 Nydus”这个题目的含意就是,拓展 nerdctl image convert 命令,使其反对将常见的 Docker V2 格局和 OCI 格局的镜像转换为 Nydus 格局的镜像。这个题目是本次我的项目最重要的一部分工作。

nerdctl image convert 的实现次要是借助  Containerd 自身对外开放的 API 中的 Convert 能力实现的。Containerd 对镜像转换的解决流程是,依照镜像组织的树形构造,从根底的 layer 和 config 开始,到 index 层完结,一层层进行转换,从而最终生成一个新的镜像;其中,调用者能够自定义数据层的转换逻辑,nerdctl image convert 已有的对 eStargz 格局 的反对就是通过这种形式实现的。

但目前相似 eStargz 格局的这种实现其实是默认转换后的镜像和转换前的镜像的各层之间一一对应,而 Nydus 镜像除了有与转换前的 OCI 镜像中的数据层一一对应的 Blob layer 之外,还有一个 Bootstrap layer。幸好 Containerd 的解决流程中,在实现了每一层的转换后,会调用一个回调函数,给调用者机会做进一步的解决。因而,能够利用 Containerd 对 manifest 层解决完的回调,在该回调中,额定生成一个 Bootstrap layer,并相应地批改 manifest 层和 config 层中的内容,从而最终构建出一个非法的 Nydus 镜像。

在开发完根本逻辑后,测试过程中发现了转换后的 Nydus 镜像文件生成后又被意外删除的景象。甚至在一步步调试时,在函数返回前,转换后的 Nydus 镜像文件仍旧存在,但函数返回后文件奇观般的隐没了。

对此,我顺次尝试了以下排查思路:

Nydus 镜像文件的删除是在另一个协程中进行的,因而我在以后协程的断点没有调试到删除操作。但屡次调试后发现,删除动作肯定会产生在函数返回前,这与协程的不可预测性不符。

Nydus 镜像文件触发了 Containerd 守护过程的某种 GC 操作。我应用 Inotify 监控镜像文件的创立和删除操作对应的过程,发现的确是 Containerd 守护过程的操作。

但问题是,Nerdctl 执行的代码也会与 Containerd 进行 RPC 通信,这一操作是 Containerd 过程本人的内置逻辑呢,还是 Nerdctl 告诉 Containerd 做的呢?不得而知。

在破费了一周工夫排查 bug 后,发现是函数返回前执行了函数体前局部的 Defer 操作触发了 Nydus 镜像文件的删除操作,而在 Defer 的函数体中没有设置断点,因而没有调试到。最终,通过剖析 Defer 函数体中的逻辑,问题得以解决。

总结下来,还是具体的编程经验不足,没有在一开始就想到所有可能得方面,导致绕了很大的弯路。

小结

上述两项工作的 PR 都已合入了 Nerdctl 的主分支 (>=0.22),根本实现了 Nerdctl 原生反对 Nydus 镜像减速的能力。

大家能够移步文档作进一步理解:https://github.com/containerd/nerdctl/blob/master/docs/nydus.md

PART. 4 我的项目总结与瞻望

Containerd 是以后最风行的容器运行时之一,Nerdctl 作为 Containerd 的社区中的外围我的项目提供了欠缺的应用体验。它们都是容器畛域中十分重要的根底我的项目。

通过本次我的项目的开发工作,我逐步理解了 OCI 镜像的组成部分,每局部的作用和根本格局;晓得了以及 Nydus 镜像和 OCI 镜像之间的差别,并且了解了 Nydus 镜像中 Blob layer 和 Bootstrap layer 之间的区别;可能通过查看本地镜像相干文件排查一些简略的程序 bug。

在我的项目进行的过程中,我浏览了 Nerdctl 和 Containerd 的代码,学到了一些实用的编程技巧,并最终向 Nerdctl 提交并胜利合入了两个 PR。在向 Nerdctl 提交和批改 PR 的过程中,Nerdctl 的 Maintainer 们看待代码的谨严态度让我大受裨益———他们甚至会 review go.sum 每一行改变!

不仅如此,这些的开源工作经验为我揭开了“顶级开源我的项目的神秘的面纱”,加强了我的信念,让我更有自信和趣味进一步参加到云原生我的项目的相干工作中。

本次我的项目的实现,将使得用户能十分不便地应用 Nerdctl 和 Containerd 来结构、拉取、运行 Nydus 镜像,这无疑会对 Nydus 镜像格局的遍及和进一步倒退起到十分好的推动作用。

| 致谢 |

非常感谢我的项目组织老师赵新、本题目 Mentor 严松老师和我的项目领导助理姚胤楠同学在本次我的项目进行过程领导和帮忙,特地是严松老师细致入微的解答和领导,每次我的一个看起来很简略甚至很愚昧的问题都能失去严松老师具体的解答,并且言辞中常常蕴含着必定和激励,让人如沐春风。

在后续的学习和工作中,我心愿能继续参加到 Nydus 相干的开发工作中,持续为社区奉献 issue 和代码。

理解更多 …

Nydus Star 一下✨:

https://github.com/dragonflyoss/image-service

更多福利 …

《SOFAStack 社区 2022 年报》 已收回

扫描下方二维码填写问卷

有机会取得 SOFAStack  新年限定周边 哦!

本周举荐浏览

Nydus 镜像扫描减速

Nydus 镜像减速插件迁入 Containerd 旗下

Nydus | 容器镜像根底

Dragonfly 和 Nydus Mirror 模式集成实际

正文完
 0