OCI Image Format定义了content addressable与location addressable结合的分层树状结构。
基本组成 Image manifest :用于对镜像的内容寻址。Image index :指向多个manifest的更高级别manifest,一般用于区分多平台。Filesystem layers :用于描述容器文件系统内容变化。Configuration :用于记录镜像配置及运行时信息等元信息。引用go-containerregistry 项目中结构图来宏观描述一下上述组成的关系:
镜像层级结构 OCI Image Spec 中有更细化的描述:
Content Descriptor Content descriptor用于描述对象内容的位置,组件内的descriptor可以描述当前组件对其他组件的引用关系,其应包含如下核心元素:
属性描述 属性 类型 作用 mediaTypestring对象内容的类型 digeststring对象内容的唯一标识,常使用sha256算法加密 sizeint64对象内容的字节数 urls[]string对象可以被下载的url列表(optional) annotationsmap[string]string携带额外信息的键值对集合
mediaType的作用
mediaType用于唯一地标识当前blob的类型,通过此类型可以标准化对blob的处理。以containerd的image.ChildrenHandler 获取当前descriptor所有直接子引用为例。可以看出对不同blob的处理依据就是mediaType。
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
41
// Children returns the immediate children of content described by the descriptor.
func Children ( ctx context . Context , provider content . Provider , desc ocispec . Descriptor ) ([] ocispec . Descriptor , error ) {
var descs [] ocispec . Descriptor
switch desc . MediaType {
case MediaTypeDockerSchema2Manifest , ocispec . MediaTypeImageManifest :
p , err := content . ReadBlob ( ctx , provider , desc )
if err != nil {
return nil , err
}
// TODO(stevvooe): We just assume oci manifest, for now. There may be
// subtle differences from the docker version.
var manifest ocispec . Manifest
if err := json . Unmarshal ( p , & manifest ); err != nil {
return nil , err
}
descs = append ( descs , manifest . Config )
descs = append ( descs , manifest . Layers ... )
case MediaTypeDockerSchema2ManifestList , ocispec . MediaTypeImageIndex :
p , err := content . ReadBlob ( ctx , provider , desc )
if err != nil {
return nil , err
}
var index ocispec . Index
if err := json . Unmarshal ( p , & index ); err != nil {
return nil , err
}
descs = append ( descs , index . Manifests ... )
default :
if IsLayerType ( desc . MediaType ) || IsKnownConfig ( desc . MediaType ) {
// childless data types.
return nil , nil
}
log . G ( ctx ). Debugf ( "encountered unknown type %v; children may not be fetched" , desc . MediaType )
}
return descs , nil
}
Image Manifest Manifest用于定位镜像内容,可以认为是一个镜像的实际入口,包含一个特定platform下image所需的全部信息:
application/vnd.oci.image.manifest.v1+json OCI Image Format Specapplication/vnd.docker.distribution.manifest.v2+json 兼容Docker Image Format Spec属性描述 属性 类型 作用 schemaVersionint指定manifest schema,为确保与旧版本docker兼容,此Spec下固定值为2 mediaTypestring内容的类型 configdescriptor与容器运行时相关的配置信息 layers[]descriptor用于构建镜像内文件系统布局,其中layers[0]描述base layer annotationsmap[string]string携带额外信息的键值对集合
实践探索 以linux/amd64下的ubuntu:21.04为例,我们看一下其manifest:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"schemaVersion" : 2 ,
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"config" : {
"mediaType" : "application/vnd.docker.container.image.v1+json" ,
"size" : 1462 ,
"digest" : "sha256:bf70ebd2c444440ae068c5ccea80e2087906a825ff1019a9f6d6cbb229e33481"
},
"layers" : [
{
"mediaType" : "application/vnd.docker.image.rootfs.diff.tar.gzip" ,
"size" : 31837572 ,
"digest" : "sha256:4451f5c7eb7af74432585f5ebfbeb01bbfc87ec4a74dc93703bdd89330559cd1"
}
]
}
可以看到,其mediaType为application/vnd.docker.distribution.manifest.v2+json,包含一个config blob与一个layer blob。
Image Index Index又被称为fat manifest,manifest可以视为layer的索引,而index是在manifest上又加了一层的索引。有了index,这种两层树状结构变成了多层,提供了多描述符入口点。
在docker image中,index的主要作用是区分多平台(OS/ORCH)。
application/vnd.oci.image.index.v1+json OCI Image Format Specapplication/vnd.docker.distribution.manifest.list.v2+json 兼容Docker Image Format Spec属性描述 属性 类型 作用 schemaVersionint指定manifest schema,为确保与旧版本docker兼容,此spec下固定值为2 mediaTypestring内容的类型 manifests[]object描述运行时要求的最小集,主要是操作系统/架构 等平台相关,列表中有多个manifest,提供平台相关的属性用以进行filter。 annotationsmap[string]string携带额外信息的键值对集合
实践探索 我们看一下dockerhub上ubuntu:21.04的index。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
"schemaVersion" : 2 ,
"mediaType" : "application/vnd.docker.distribution.manifest.list.v2+json" ,
"manifests" : [
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:ef8ee90cfa9cfc7c218586dea9daa6a8d1d191b3c73be143f4120fe140dae3d0" ,
"platform" : {
"architecture" : "amd64" ,
"os" : "linux"
}
},
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:b7de3b708ddbdb5ca7d0a6a81f6d9df450276fc4794174a7b7a3441b00281a61" ,
"platform" : {
"architecture" : "arm" ,
"os" : "linux" ,
"variant" : "v7"
}
},
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:ca763e1a382a5b23f91abaf1c36a84be33da2d657f45746112f28ae010571041" ,
"platform" : {
"architecture" : "arm64" ,
"os" : "linux" ,
"variant" : "v8"
}
},
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:54b3fc49fc1949bcedbafbf1f18393920545ba934331cf72176cb14087962879" ,
"platform" : {
"architecture" : "ppc64le" ,
"os" : "linux"
}
},
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:9c389f10c2b192dd01e87188c7cf1591dc830370046085190dd3ecfdaa1f2cfb" ,
"platform" : {
"architecture" : "riscv64" ,
"os" : "linux"
}
},
{
"mediaType" : "application/vnd.docker.distribution.manifest.v2+json" ,
"size" : 529 ,
"digest" : "sha256:13532df2f7a272c2c973268db5264059be5ba9882962d30db3d86ca38db3a737" ,
"platform" : {
"architecture" : "s390x" ,
"os" : "linux"
}
}
]
}
从中不难看出,该镜像提供了六种CPU架构下编译的ubuntu镜像,当某个client发出docker pull命令时,registry会index到对应的架构平台,找到合适的manifest。
Filesystem layer Layer是镜像内文件系统的组成成分,每一层都在描述一系列文件系统变化。
application/vnd.oci.image.layer.v1.tar+gzip OCI Image Format Specapplication/vnd.docker.image.rootfs.diff.tar.gzip 兼容Docker Image Format Spec实践探索 我们copy出ubuntu:21.04的layer并解压,看一下base image的样式,tar内文件太多,仅列出前10行。
1
2
3
4
5
6
7
8
9
10
11
12
13
$ cp blobs/sha256/44/4451f5c7eb7af74432585f5ebfbeb01bbfc87ec4a74dc93703bdd89330559cd1/data ~/ubuntu.tar.gz && gzip -d ~/ubuntu.tar.gz -c | tar tv | head -10
# output
lrwxrwxrwx 0/0 0 2021-07-24 01:47 bin -> usr/bin
drwxr-xr-x 0/0 0 2021-04-19 15:26 boot/
drwxr-xr-x 0/0 0 2021-07-24 01:50 dev/
drwxr-xr-x 0/0 0 2021-07-24 01:50 etc/
-rw------- 0/0 0 2021-07-24 01:47 etc/.pwd.lock
-rw-r--r-- 0/0 3028 2021-07-24 01:47 etc/adduser.conf
drwxr-xr-x 0/0 0 2021-07-24 01:50 etc/alternatives/
-rw-r--r-- 0/0 100 2021-04-14 18:32 etc/alternatives/README
lrwxrwxrwx 0/0 0 2021-07-24 01:50 etc/alternatives/awk -> /usr/bin/mawk
lrwxrwxrwx 0/0 0 2021-07-24 01:50 etc/alternatives/nawk -> /usr/bin/mawk
技巧
可以通过... | awk '{print $6}' | awk -F/ '{print $1}'| sort | uniq 对上述输出结果进行聚合获取第一层目录,结果可以看到就是标准的ubuntu root filesystem。
再探索一下filesystem changeset的内容,新创建一个镜像,修改镜像内的文件系统
1
2
3
FROM ubuntu:21.04
RUN echo "hello world" > /tmp/hello.txt
COPY ccc .
分别将第二层与第三层的内容拷贝出来并解压
1
2
3
4
5
6
7
8
9
10
# 略去拷贝过程
######Layer2######
$ tar zxvf layer2.tar.gz
tmp/
tmp/hello.txt
######Layer3######
$ tar zxvf layer3.tar.gz
ccc
上述结果验证了之前踩过的一个坑:写Dockerfile构建镜像时,使用COPY将宿主机上的文件复制到镜像里时,如果源文件变化了,docker缓存会失效。之前误以为一个dockerfile中的一条语句对应一个layer,只要语句不变,layer就不变,就可以使用cache。此例清晰地描述出layer会与文件系统的changeset密切相关。
Configuration 用于描述镜像的一些元信息及容器运行时所需的信息。
application/vnd.oci.image.config.v1+json OCI Image Format Specapplication/vnd.docker.container.image.v1+json 兼容Docker Image Format Spec属性描述 属性 类型 作用 createdstring描述镜像创建日期 authorstring描述镜像创建的作者 architecturestring描述编译镜像中二进制包的节点CPU架构 osstring描述构建镜像的节点的操作系统 configobject容器运行时所需要的执行参数(docker run中所能指定的参数),如Volumes,Env,ExposedPort等 rootfsobject描述image各层DiffID historyobject描述每一层的历史信息
上述大部分属性可以通过docker inspect [IMAGE]获取到。
DiffID
DiffID是layer未压缩时的tar包hash后的digest,可用于解压后内容验证。
由于DiffID仅能描述某个layer的信息,无法描述整个layer布局的信息,因此又引入ChainID来校验image的布局,主要思想是引入与之前layer的相关性来生成对应layer的ID。从定义上看,第一层base layer的DiffID与ChainID一致。
实践探索 展示一下ubuntu:21.04的configuration,部分与container相关的字段超出此spec范围。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{
"architecture" : "amd64" ,
"config" : {
"Hostname" : "" ,
"Domainname" : "" ,
"User" : "" ,
"AttachStdin" : false ,
"AttachStdout" : false ,
"AttachStderr" : false ,
"Tty" : false ,
"OpenStdin" : false ,
"StdinOnce" : false ,
"Env" : [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd" : [
"bash"
],
"Image" : "sha256:2a1126c0612fcbe61f0acaa6b1f2caf3a156b31684219de8bbb763ee3e99940c" ,
"Volumes" : null ,
"WorkingDir" : "" ,
"Entrypoint" : null ,
"OnBuild" : null ,
"Labels" : null
},
"container" : "acac01451c096428e536623ecd3887aa7c79f8377ac8a94885b6ceae8971dfcf" ,
"container_config" : {
"Hostname" : "acac01451c09" ,
"Domainname" : "" ,
"User" : "" ,
"AttachStdin" : false ,
"AttachStdout" : false ,
"AttachStderr" : false ,
"Tty" : false ,
"OpenStdin" : false ,
"StdinOnce" : false ,
"Env" : [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd" : [
"/bin/sh" ,
"-c" ,
"#(nop) " ,
"CMD [\"bash\"]"
],
"Image" : "sha256:2a1126c0612fcbe61f0acaa6b1f2caf3a156b31684219de8bbb763ee3e99940c" ,
"Volumes" : null ,
"WorkingDir" : "" ,
"Entrypoint" : null ,
"OnBuild" : null ,
"Labels" : {}
},
"created" : "2021-07-26T21:21:54.791192114Z" ,
"docker_version" : "20.10.7" ,
"history" : [
{
"created" : "2021-07-26T21:21:54.424131139Z" ,
"created_by" : "/bin/sh -c #(nop) ADD file:6ae44786caae9af1c6b70dc9cc244e7d4e06fffc0696f68877527d69aa3fc735 in / "
},
{
"created" : "2021-07-26T21:21:54.791192114Z" ,
"created_by" : "/bin/sh -c #(nop) CMD [\"bash\"]" ,
"empty_layer" : true
}
],
"os" : "linux" ,
"rootfs" : {
"type" : "layers" ,
"diff_ids" : [
"sha256:ce91b7d7ac5b2c288515e8eee3a83720d6855e7f1cf8dfa6e9b524453956175f"
]
}
}
Image Layout 用于描述OCI内容寻址(content-addressable )blob与位置寻址(location-addressable )reference的目录结构。
content-addressable vs location-addressable
在location-addressed 存储中,每个数据元素存储在特定的物理媒介中,并且它的(物理媒介)location 会被记录下来以供后续访问。当想要访问到对应数据内容时,只需要在request中使用这个location 即可。位置寻址不关心存储的具体内容是什么,只关心内容存储在什么位置,内容的大小多少(与盘空间占用相关),location所标识的内容可以被灵活地修改/覆盖/删除。
与之相比,content-addressed 存储通过与内容相关的唯一ID来定位,通过存储系统找到对应的内容。内容一旦发生变化,这个标识ID也会发生变化,即寻址地址也会发生变化。由于这个特点,一般的content-addressed 系统不允许修改原有的内容,并且删除操作也会通过严格的策略进行控制。
参考wikipedia: Content-addressed vs. location-addressed
结构组成 blobs目录 :包含内容寻址的blob。子目录是hash算法名称 内容索引形式为blobs/<alg>/<encoded> oci-layout 文件 :声明OCI image-layout的版本。Index.json文件 :image-layout中reference的入口点。一般使用org.opencontainers.image.ref.name annotation声明引用。 组织形式与image index形式十分相似。 示例 比较完善的示例是helm对OCI支持 ,chart的发布支持OCI规范,实现上依赖于本地的OCI Image Layout与OCI registry交互完成chart的发布。
helm支持OCI的交互方式 使用save cmd保存两个版本的chart。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ helm chart save mychart-0.1.0 myregistry:5000/mychart:v0.1.0
ref: myregistry:5000/mychart:v0.1.0
digest: dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337
size: 3.7 KiB
name: mychart
version: 0.1.0
v0.1.0: saved
$ helm chart save mychart-0.2.0 myregistry:5000/mychart:v0.2.0
ref: myregistry:5000/mychart:v0.2.0
digest: fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24
size: 3.7 KiB
name: mychart
version: 0.2.0
v0.2.0: saved
local OCI image layout的结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree ~/.cache/helm/registry/cache
├── blobs
│ └── sha256
│ ├── 573f8b72a735d3f6e5919acb325d365aeddf69edee1e4840c59a5d741179da97
│ ├── 65a07b841ece031e6d0ec5eb948eacb17aa6d7294cdeb01d5348e86242951487
│ ├── 98ddb183b4658761a6e431fbbde4c6c15863b0c3597b74b519c67776830de282
│ ├── dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337
│ ├── e76837ca35eb2e8f22ce8a78f14a1275511eafed58b76955b2ac7ddd0211c965
│ └── fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24
├── index.json
├── ingest
└── oci-layout
查看一下index.json中的内容cat ~/.cache/helm/registry/cache/index.json | jq .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"schemaVersion" : 2 ,
"manifests" : [
{
"mediaType" : "application/vnd.oci.image.manifest.v1+json" ,
"digest" : "sha256:dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337" ,
"size" : 322 ,
"annotations" : {
"org.opencontainers.image.ref.name" : "myregistry:5000/mychart:v0.1.0"
}
},
{
"mediaType" : "application/vnd.oci.image.manifest.v1+json" ,
"digest" : "sha256:fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24" ,
"size" : 322 ,
"annotations" : {
"org.opencontainers.image.ref.name" : "myregistry:5000/mychart:v0.2.0"
}
}
]
}
从index.json中很容易看出来通过descriptor中的digest与org.opencontainers.image.ref.name annotion将location(chart reference)与content(OCI manifest)关联了起来。
总结与延伸 本文介绍了OCI Image Format Spec的组成,其对mediaType做了兼容,可以说是Docker Image Format Spec的一个超集。但OCI Image Format Spec要更通用些,体现在layer content可以更加多样,并且index不局限于一层。在此通用规范下可以做一些更cool的事情,比如OCI Artifacts 。
此外,OCI Image Format Spec与OCI Runtime Spec 及OCI Distribution Spec 密切相关,比如image config如何转换成runtime bundle;image如何存储到registry。可见OCI Image Format Spec是OCI 规范中关键纽带。
后续会写一篇文章来描述一个私有化交付下的应用打包方案,从中可以看到OCI Artifacts,OCI Image Format Spec与OCI Distribution结合起来释放的强大力量。