导语
本文旨在通过OCI介绍镜像的全链路生态,帮助读者更好的理解镜像和其生态。
引言
镜像的生态覆盖面极广,为了介绍这一技术,不妨从本人的一个经历说起:在刚接触Docker源码时,代码里嵌套的许多概念让我摸不着头脑,例如Manifest
和Descriptor
等,一些容易混淆的结构体命名(例如v2版本的镜像配置在代码内的结构体名为Image
)也降低了代码的阅读效率,导致我常常处于”懂了,但又没有完全懂”的状态。后来我去社区查阅相关资料才发现,这些定义都是OCI(Open Container Initiative)制定的标准,如果没有很好的对这些概念进行了解,镜像相关的模块开发将会变的异常困难,为了学习镜像甚至容器技术,OCI的学习也是必须的。
OCI的全称是“Open Container Initiative”,它是一种容器的行业内标准,由Docker公司和容器行业的其他领导者于2015年6月建立。它定义了容器相关技术的规范,比如镜像的存储格式,镜像的分发标准,容器的启动流程甚至是各种组件的接口规范等。OCI分为Image Specification
,Runtime Specification
和Distribution Specification
,它们分别定义了镜像、容器运行时和镜像分发的标准。
1. OCI Image Specification
OCI Image Specification
是镜像格式的规范,本章将会对该规范进行详细的解读,并在最后一小节附带相关内容以供延伸阅读。
简而言之,一个标准的镜像文件由若干镜像索引(Image Index
)指向一个或多个镜像清单(Image Manifest
),其中每个清单都指向一个配置文件(Image Configuration
)和若干层文件(Image Layer
),如果没有镜像索引,也可以只包含一个镜像清单。读到这里可能你会对这几个定义一脸懵逼,不过不要着急,本章会对这几种类型进行详细介绍。
镜像的标准目前已经单独开源为github上的库,任何镜像的开发都可以通过导入这个库快速的引用各种定义好的结构体。
OCI镜像标准的整体拓扑结构如下:
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
的类型,例如Manifest
的MediaType
为application/vnd.oci.image.manifest.v1+json
,目前常见Artifact
的MediaType
如下,我们可以在这里查看详细。
- 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
储存镜像的运行平台信息,这个字段只有当Artifact
为Index
时不为空
有意思的是,如果Descriptor
本身被转化为JSON文件,这个文件本身也是一个Artifact
,因此它可以拥有一个Descriptor
,其MediaType
为application/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
展示了镜像的配置文件和层文件,不难注意到config
和layers
字段内储存的都是Descriptor
,annotations
字段储存的键值对和Descriptor
的annotations
字段规则和限制一致。
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
字段内的Descriptor
在MediaType
为application/vnd.oci.image.manifest.v1+json
时Platform
字段不为空。
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
和镜像层对应Descriptor
的digest
是不一致的,这里diff_id
是镜像层解压后的摘要,digest
是压缩格式下的镜像层摘要,且排列顺序为最底层到最顶层history
记录了每层的实际修改命令和相关信息
镜像配置文件对于镜像而言十分重要,从它的结构体名Image
我们也能察觉到这一点。当我们谈论Image ID
时,我们也是在指代镜像配置文件的构件摘要(即Configuration
对应的Descriptor
内digest
字段),毕竟该配置文件包含了镜像组成、启动和运行等配置参数,可以说完整的体现了镜像本身。
1.7. Image Layout
Image Layout
是镜像在本地文件系统下的布局,它包含了:
blobs
目录:储存Artifact
的根目录,镜像的Index
,Manifest
和Configuration
都在这个目录下,其文件名为它对应的摘要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. 相关文档
- Artifact: https://github.com/opencontainers/artifacts
- MediaType: https://github.com/opencontainers/image-spec/blob/main/media-types.md
- Descriptor: https://github.com/opencontainers/image-spec/blob/main/descriptor.md
- Manifest: https://github.com/opencontainers/image-spec/blob/main/manifest.md
- Index: https://github.com/opencontainers/image-spec/blob/main/image-index.md
- Configuration: https://github.com/opencontainers/image-spec/blob/main/config.md
- Layout: https://github.com/opencontainers/image-spec/blob/main/image-layout.md
- v1和v2: https://matrix.ai/blog/docker-image-specification-v1-vs-v2/
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: 容器的进程配置,根据平台的不同内部需要指明的字段也不同,具体的内容可以在这里查看
- 平台相关配置: 因为平台的特性,不同平台有专属于自己的配置,目前支持Linux,Windows,Solaris和VM
- Hooks: 即容器生命流程中的钩子,从容器生命流程来看分为Prestart,CreateRuntime,CreateContainer,StartContainer,Poststart和Poststop
我们可以在这里阅读到容器运行时配置的详细代码。
容器根目录由Docker或contaierd负责挂载,它们独立于容器运行时之外,根据镜像层的digest
从仓库拉取镜像层储存到对应的路径下(例如Docker的overlay格式镜像在/var/lib/docker/overlay2
),随后会用指定的UnionFS挂载,并在最顶层额外添加读写层以供用户操作。
2.2. Lifecycle
容器的运行时需要支持镜像有如下的生命流程。
容器的运行有如下几种状态:
- 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. 相关文档
- config.json: https://github.com/opencontainers/runtime-spec/blob/master/config.md
- Filesystem Bundle: https://github.com/opencontainers/runtime-spec/blob/master/bundle.md
- Lifecycle: https://github.com/opencontainers/runtime-spec/blob/master/runtime.md
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标准分为镜像标准、运行时标准和分发标准,它们分别定义了镜像的格式、镜像运行时的规范和镜像分发的规范。
镜像规范将每个镜像的元数据拆分为镜像索引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权威指南》