OCI的全链路生态

Posted by ilyee on September 30, 2021

导语

本文旨在通过OCI介绍镜像的全链路生态,帮助读者更好的理解镜像和其生态。

引言

镜像的生态覆盖面极广,为了介绍这一技术,不妨从本人的一个经历说起:在刚接触Docker源码时,代码里嵌套的许多概念让我摸不着头脑,例如ManifestDescriptor等,一些容易混淆的结构体命名(例如v2版本的镜像配置在代码内的结构体名为Image)也降低了代码的阅读效率,导致我常常处于”懂了,但又没有完全懂”的状态。后来我去社区查阅相关资料才发现,这些定义都是OCI(Open Container Initiative)制定的标准,如果没有很好的对这些概念进行了解,镜像相关的模块开发将会变的异常困难,为了学习镜像甚至容器技术,OCI的学习也是必须的。

OCI的全称是“Open Container Initiative”,它是一种容器的行业内标准,由Docker公司和容器行业的其他领导者于2015年6月建立。它定义了容器相关技术的规范,比如镜像的存储格式,镜像的分发标准,容器的启动流程甚至是各种组件的接口规范等。OCI分为Image SpecificationRuntime SpecificationDistribution Specification,它们分别定义了镜像、容器运行时和镜像分发的标准。

1. OCI Image Specification

OCI Image Specification是镜像格式的规范,本章将会对该规范进行详细的解读,并在最后一小节附带相关内容以供延伸阅读。

简而言之,一个标准的镜像文件由若干镜像索引(Image Index)指向一个或多个镜像清单(Image Manifest),其中每个清单都指向一个配置文件(Image Configuration)和若干层文件(Image Layer),如果没有镜像索引,也可以只包含一个镜像清单。读到这里可能你会对这几个定义一脸懵逼,不过不要着急,本章会对这几种类型进行详细介绍。

镜像的标准目前已经单独开源为github上的库,任何镜像的开发都可以通过导入这个库快速的引用各种定义好的结构体。

OCI镜像标准的整体拓扑结构如下:

image-spec-topo

1.1. Artifact

镜像的OCI标准并没有定义层文件中的内容,也就是说,通过将Helm Chart,CNAB等制品打包到镜像层内,我们也可以让它们成为符合OCI镜像标准的结构,允许它们用镜像的分发方式在Registry内储存并被下载。因此,为了和OCI镜像作区分,这种遵循OCI定义,能够通过OCI分发规范推送和拉取的内容,可以统称为OCI Artifact

举个例子,镜像的清单Manifest就是一种Artifact,同理,索引Image Index,配置Image Configuration和层文件Image Layer都是Artifact,每种Artifact都有自己的类型,在MediaType字段内定义(下一节会展开介绍),不同的Artifact必须要保证属于不同类型的MediaType

从结构组成上来看,OCI镜像只是OCI Artifact的一个实例,这种数据的封装将允许所有符合OCI distribution specification的镜像仓库服务都能实现不同类型数据的存储、权限、复制和分发等能力,而无须针对每种特定类型的数据设立或开发不同的仓库服务,使开发者能专注于新类型的Artifact的创新。开发者可以根据社区的文档来定义全新的Artifact,并通过已有的镜像仓库进行分发。

1.2. Media Type

MediaType定义了不同Artifact的类型,例如ManifestMediaTypeapplication/vnd.oci.image.manifest.v1+json,目前常见ArtifactMediaType如下,我们可以在这里查看详细。

  • application/vnd.oci.descriptor.v1+json: Content Descriptor
  • application/vnd.oci.layout.header.v1+json: OCI Layout
  • application/vnd.oci.image.index.v1+json: Image Index
  • application/vnd.oci.image.manifest.v1+json: Image manifest
  • application/vnd.oci.image.config.v1+json: Image config
  • application/vnd.oci.image.layer.v1.tar: “Layer”, as a tar archive
  • application/vnd.oci.image.layer.v1.tar+gzip: “Layer”, as a tar archive compressed with gzip
  • application/vnd.oci.image.layer.v1.tar+zstd: “Layer”, as a tar archive compressed with zstd
  • application/vnd.oci.image.layer.nondistributable.v1.tar: “Layer”, as a tar archive with distribution restrictions
  • application/vnd.oci.image.layer.nondistributable.v1.tar+gzip: “Layer”, as a tar archive with distribution restrictions compressed with gzip
  • application/vnd.oci.image.layer.nondistributable.v1.tar+zstd: “Layer”, as a tar archive with distribution restrictions compressed with zstd

从命名我们可以发现MediaType的命名有其规律,可以总结为:

1
2
3
4
5
6
# 镜像配置
[registration-tree].[org|company|entity].[objectType].[optional-subType].config.[version]+[optional-configFormat]
# 镜像清单
[registration-tree].[org|company|entity].[objectType].[optional-subType].config.[version]+[optional-configFormat]
# 镜像层文件
[registration-tree].[org|company|entity].[layerType].[optional-layerSubType].layer.[version].[fileFormat]+[optional-compressionFormat]

其中各个字段的对照如下表。

字段 说明
registration-tree IANA(Internet Assigned Numbers Authority,互联网号码分配机构)的注册类型
org/company/entity 开源组织、公司名称或其他实体
objectType 类型的简称
optional-subType 可选字段,对object Type的补充说明
version 类型的版本
optional-configFormat 可选的配置格式说明(json、yaml等)
optional-layerSubType 可选字段,对 layerType的补充说明
fileFormat 文件格式
optional-compressionFormat 可选的压缩格式说明(gzip、zstd等)

开发者可以根据这个规则创建属于自己的Artifact类型,例如Helm的镜像层的MediaType名为application/vnd.cncf.helm.chart.config.v1+json

1.3. Descriptor

Descriptor用来描述一个Artifact,我们可以把它理解为一个Artifact的元数据。它独立于Artifact之外,更多的是出现在代码中,其代码结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Descriptor struct {
    MediaType string `json:"mediaType,omitempty"`

    Digest digest.Digest `json:"digest"`

    Size int64 `json:"size"`

    URLs []string `json:"urls,omitempty"`

    Annotations map[string]string `json:"annotations,omitempty"`

    Platform *Platform `json:"platform,omitempty"`
}

其中:

  • MediaType代表这个Artifact的类型
  • Digest字段代表Artifact的摘要,目前摘要生成算法只支持SHA-256和SHA-512digest是唯一的,我们可以通过digest从镜像仓库中获得该构件,例如镜像层
  • Size字段代表Artifact的大小,如果是Manifest或者Config,则为其JSON文件的大小,如果是镜像层,则为其包的二进制的大小
  • URLs代表能获取到该Artifact的URL链接列表,这些URL一定是http或https协议
  • Annotations储存这个Artifact的元数据信息,其键值的命名必须要遵循一定的规律,具体可以参考这篇文档
  • Platform储存镜像的运行平台信息,这个字段只有当ArtifactIndex时不为空

有意思的是,如果Descriptor本身被转化为JSON文件,这个文件本身也是一个Artifact,因此它可以拥有一个Descriptor,其MediaTypeapplication/vnd.oci.descriptor.v1+json

1.4. Image Manifest

Image Manifest(镜像清单)可以认为是镜像最主要的元数据,它包含了镜像配置文件和镜像层文件的Descriptor,镜像仓库,Docker和containerd等组件可以通过镜像清单获得一个镜像的所有所需内容,从而将镜像转化为OCI运行时规范,并将镜像的后续运行工作交给runc完成。

如下是一个Manifest的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 7023,
    "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 32654,
      "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 16724,
      "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 73109,
      "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
    }
  ],
  "annotations": {
    "com.example.key1": "value1",
    "com.example.key2": "value2"
  }
}

这个Manifest展示了镜像的配置文件和层文件,不难注意到configlayers字段内储存的都是Descriptorannotations字段储存的键值对和Descriptorannotations字段规则和限制一致。

1.5. Image Index

Image Index(镜像索引)是建立在Manifest之上的概念,这是一个非必需的Artifact,它的主要作用是指向镜像在不同平台上的镜像清单,它在代码中的定义如下:

1
2
3
4
5
6
7
type Index struct {
    specs.Versioned

    Manifests []Descriptor `json:"manifests"`

    Annotations map[string]string `json:"annotations,omitempty"`
}

其中,Manifests字段内的DescriptorMediaTypeapplication/vnd.oci.image.manifest.v1+jsonPlatform字段不为空。

1.6. Image Configuration

Image Configuration(镜像配置)用于描述容器运行时的执行参数和镜像层的挂载顺序,它也包含了镜像运行所需的机器架构信息和不同镜像层的操作信息,如下为一个配置实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "created": "2015-10-31T22:22:56.015925234Z",
    "author": "Alyssa P. Hacker <alyspdev@example.com>",
    "architecture": "amd64",
    "os": "linux",
    "config": {
        "User": "alice",
        "ExposedPorts": {
            "8080/tcp": {}
        },
        "Env": [
            "BAR=well_written_spec"
        ],
        "Entrypoint": [
            "/bin/my-app-binary"
        ],
        "Cmd": [
            "/etc/my-app.d/default.cfg"
        ],
        "Volumes": {
            "/var/log/my-app-logs": {}
        },
        "WorkingDir": "/home/alice",
        "Labels": {
            "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
        }
    },
    "rootfs": {
      "diff_ids": [
        "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1"
      ],
      "type": "layers"
    },
    "history": [
      {
        "created": "2015-10-31T22:22:54.690851953Z",
        "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
      }
    ]
}

其中:

  • config字段里记录了镜像构件的容器所需的各种配置,如环境变量,workdir,挂载的目录,初始命令等
  • diff_ids记录了未压缩镜像的摘要,注意配置文件内镜像层的diff_id和镜像层对应Descriptordigest是不一致的,这里diff_id是镜像层解压后的摘要,digest是压缩格式下的镜像层摘要,且排列顺序为最底层到最顶层
  • history记录了每层的实际修改命令和相关信息

镜像配置文件对于镜像而言十分重要,从它的结构体名Image我们也能察觉到这一点。当我们谈论Image ID时,我们也是在指代镜像配置文件的构件摘要(即Configuration对应的Descriptordigest字段),毕竟该配置文件包含了镜像组成、启动和运行等配置参数,可以说完整的体现了镜像本身。

1.7. Image Layout

Image Layout是镜像在本地文件系统下的布局,它包含了:

  • blobs目录:储存Artifact的根目录,镜像的IndexManifestConfiguration都在这个目录下,其文件名为它对应的摘要
  • oci-layout文件:表明镜像layout版本的文件,json格式,示例:{"imageLayoutVersion": "1.0.0"}
  • index.json文件:Index文件,指向blobs目录下的所有文件

Layout被用来创建镜像的OCI Runtime Specification bundle,这个后文会提到,这里我们可以认为是将镜像的各种元数据组织放置到本地文件系统内,从而将后续的启动工作交接给runc等镜像运行时。

1.8. 版本控制

OCI的镜像规范也是会变化的,其大版本号为SchemaVersion,小版本号在MediaType内,例如之前介绍MediaType的小节下,示例的构件版本都为v2.1。

大版本的变化往往伴随着结构体的修改,例如,在v1版本的OCI中,每个镜像层都有对应的json文件用来储存其配置,但是在v2中,这些配置全部放在了配置文件中;在v1版本下,每个镜像层都有一个随机生成的256位ID,在v2版本下这个摘要的生成从随机改为摘要,从而防止因为层文件的变化导致的镜像无法拉起问题,增强镜像层的内容寻址能力。

小版本的变化都是可兼容的,它的兼容性可以在这里查找到。

1.9. 相关文档

2. OCI Runtime Specification

OCI Runtime Specification是镜像运行时的规范,它定义了利用镜像的Artifact在不同的平台上运行容器的标准流程。如果想详细的了解容器运行时,可以深入阅读一下runc代码

2.1. Filesystem Bundle

通过镜像规范我们可以得到描述一个镜像的所有元数据,但对于镜像运行时而言,它们还需要更多的数据来运行一个镜像,比如镜像层的二进制压缩文件。正交的镜像标准让运行时规范对镜像规范和镜像分发规范无感,因此对于运行时而言,它需要的不是镜像的元数据,而是已经在本地目录下组织好的文件系统包,即Filesystem Bundle,基于统一的标准,运行时便能直接通过本地数据成功拉起一个容器。

一个标准的Filesystem Bundle包含所有运行和加载容器的信息,它包含一个config.json文件和容器的根目录(通过指定的UnionFS挂载得到),且两个文件需要在同一个目录下。

我们可以通过runc spec命令(如果提示没有安装runc,可以在这里安装)很轻易的得到一个config.json文件,其中部分关键字段含义如下:

  • Root: 记录容器运行的根目录
  • Mounts: 容器内除了根目录外的额外挂载点(例如bind mount等),destination是容器内的挂载点,source是设备名或bind mount源路径,options是挂载选项,根据平台这里有多种选择
  • Process: 容器的进程配置,根据平台的不同内部需要指明的字段也不同,具体的内容可以在这里查看
  • 平台相关配置: 因为平台的特性,不同平台有专属于自己的配置,目前支持LinuxWindowsSolarisVM
  • Hooks: 即容器生命流程中的钩子,从容器生命流程来看分为Prestart,CreateRuntime,CreateContainer,StartContainer,Poststart和Poststop

我们可以在这里阅读到容器运行时配置的详细代码。

容器根目录由Docker或contaierd负责挂载,它们独立于容器运行时之外,根据镜像层的digest从仓库拉取镜像层储存到对应的路径下(例如Docker的overlay格式镜像在/var/lib/docker/overlay2),随后会用指定的UnionFS挂载,并在最顶层额外添加读写层以供用户操作。

2.2. Lifecycle

容器的运行时需要支持镜像有如下的生命流程。

container-state

容器的运行有如下几种状态:

  • init 状态:这个是我自己添加的状态,并不在标准中,表示没有容器存在的初始状态
  • creating: 运行时使用Create命令创建中的容器
  • created: 容器被成功创建,表示配置文件和相关镜像层都没有出错,但是尚未运行
  • running: 处于运行的容器
  • stopped: 容器运行完成或出错,或通过Stop命令暂停了容器,处于这个状态的容器依然有许多保留的文件,它还未完全销毁

同理,运行时也需要支持如下的命令:

  • Create: create <container-id> <path-to-bundle>,创建一个容器,容器立即进入creating状态,如果没有错误返回,创建出来的容器将会进入created状态
  • Start: start <container-id>,开始运行一个容器,如果成功运行则容器进入running状态,否则进入stopped状态
  • Kill: kill <container-id> <signal>,杀死一个容器使其进入stopped状态
  • Delete: delete <container-id>,删除一个容器,销毁其在本地的所有信息,注意只有stopped状态的容器可以被销毁

2.3. 相关文档

3. OCI Distribution Specification

如果是说Image Specification是镜像格式的规范,Runtime Specification是镜像运行的规范,那么OCI Distribution Specification就是镜像分发的规范。该分发标准于2018年4月开始制定,于2021年5月4日公布v1.0版本,我们可以在这里看到贡献者的名单。该规范用于标准化镜像的分发标准,使OCI的生态覆盖镜像的全生态链路,从而成为一种跨平台的容器镜像分发标准。例如,Docker的官方镜像仓库distribution就是一个符合分发规范的Registry,同理,腾讯软件源所使用的Harbor也是符合分发规范的仓库。

分发标准主要定义了如下几个方面的规则:

  • 需要实现的接口
  • 接口的具体实现,如HTTP method和返回code等
  • URI规范
  • 错误代码

具体的标准我们可以在这里看到,也可以通过阅读distribution的代码了解其具体实现,限于篇幅的原因这里不会对代码进行展开讨论,如果有机会后续会总结相关文档。

总结

oci-overview

OCI标准分为镜像标准、运行时标准和分发标准,它们分别定义了镜像的格式、镜像运行时的规范和镜像分发的规范。

镜像规范将每个镜像的元数据拆分为镜像索引Index,镜像清单Manifest,镜像配置Config和二进制镜像层,这类制品被称为Artifact,它们都拥有一个外部的Descriptor通过digest描述其摘要,通过摘要我们可以在Registry内获得这些文件。

镜像运行时则定义了镜像的启动流程和其容器生命周期。所有镜像层通过UnionFS挂载为rootfs,镜像的配置文件转化为符合运行时标准的config.json,它们在同一个根目录下共同形成Filesystem Bundle从而让运行时接管镜像的运行。

镜像的分发则定义了Registry需要实现的接口,接口的接受返回规范和错误代码等。我们可以通过阅读distribution的源码来更好的了解这一分发标准。

References

[1] https://github.com/opencontainers/artifacts

[2] https://github.com/opencontainers/image-spec/blob/main/media-types.md

[3] https://github.com/opencontainers/image-spec/blob/main/descriptor.md

[4] https://github.com/opencontainers/image-spec/blob/main/manifest.md

[5] https://github.com/opencontainers/image-spec/blob/main/image-index.md

[6] https://github.com/opencontainers/image-spec/blob/main/config.md

[7] https://github.com/opencontainers/image-spec/blob/main/image-layout.md

[8] https://matrix.ai/blog/docker-image-specification-v1-vs-v2/

[9] https://github.com/opencontainers/runtime-spec/blob/master/config.md

[10] https://github.com/opencontainers/runtime-spec/blob/master/bundle.md

[11] https://github.com/opencontainers/runtime-spec/blob/master/runtime.md

[12] 《Harbor权威指南》