探究OCI镜像与Docker镜像的联系

探究OCI镜像与Docker镜像的联系

从熟知的Docker出发

目前,容器化技术已经广泛普及了,而谈到容器,我想大多数人的第一反应就是Docker了。

Docker给我们带来了很多好处,优化了CI/CD流程,可以实现云原生,纳入容器编排等等。

回想下Docker中的概念,我们是如何使用Docker的?一般开发人员会编写Dockerfile,用来定义一个镜像,然后将Dockerfile生成的镜像推送到远程仓库,最终根据需要通过pull镜像生成容器运行起来。如果是云原生应用,那这个pull的动作可能是k8s做的。同时,k8s集群的node环境中需要有Docker守护进程。

可以说,Docker的功能十分强大,包裹了整个云原生应用的开发生命周期,似乎我们如果要使用容器技术就离不开它,可事实真的是这样么?

最近,容器编排界的事实标准k8s发布了这样一则公告:自 v1.20 起放弃对 Docker 的支持,届时用户将收到 Docker 弃用警告,并需要改用其他容器运行时。

Docker support in the kubelet is now deprecated and will be removed in a future release. The kubelet uses a module called “dockershim” which implements CRI support for Docker and it has seen maintenance issues in the Kubernetes community. We encourage you to evaluate moving to a container runtime that is a full-fledged implementation of CRI (v1alpha1 or v1 compliant) as they become available. (#94624, @dims) [SIG Node]

引用自K8S官方文档

k8s放弃了kubelet中对Docker的支持,鼓励我们替换为个完整实现CRI的容器运行时。虽然只是放弃了“Docker的容器运行时”,但显而易见,Docker并非不可取代的。

容器运行时只是整个云原生应用生命周期依赖的一个点而已,如果以后整个生命周期都弃用了Docker,那原先构建的镜像还能使用么?如何能继续运行呢?

这就需要容器技术将每个功能点剥离,并形成行业规范,镜像应该怎么描述、容器运行时应该如何解析镜像与运行、网络应该如何构建,都需要一个统一的接口,

早在好几年前,业界就意识到了这个问题,从而诞生了OCI。

OCI简介

https://opencontainers.org/faq/

OCI全称Open Container Initiative,翻译过来就是开放容器标准。为了减少项目的摩擦而导致的内耗,以及用户渴求一个容器技术不强绑定于一个具体厂商,且能跨越各种硬件、操作系统、CPU架构、公有云,2015年Docker和当时其他“友商”进行了讨论,大家开发容器技术,都得按照同一个模子来,就像软件开发时定一个接口,具体实现交由开发人员一样。Docker构建的镜像,也可以发布到其他容器厂商的镜像仓库,并run在其他厂商的运行时,得益于大家都是先了这个接口。

OCI包含了以下两部分:

  • 镜像规范(image-spec)

    规定应该以何种格式存储、分发镜像。

    也就是,被打成的镜像内容具体需要长什么样。目录结构,结构信息元文件、版本等等,应该如何去定义,在哪定义。

  • 运行时规范(runtime-spec)

    规定了如何运行在磁盘上解压的“文件系统包”。

    什么是文件系统包?比如,将linux整个系统,用tar工具打成了一个tar.gz包。这个包就是“文件系统包”。它将包含在镜像当中,当解压出来可被OCI Runtime运行了,就被称为“OCI Runtime Bundle”。

内容可寻址文件存储

在讲镜像规范前,需要学习一个知识点:内容可寻址文件存储,简称“CAS”

我们平时使用的系统,如windows,linux,均是通过路径来确定一个文件内容,比如D:/temp/temp.txt。访问文件的方式也是路径加上文件名。这样会有一个问题,就是无法识别这个文件是否被修改了,它还是最初的它么?当然,文件右键可以看到修改时间,但这并非存储本省的寻址提供的能力,如果我没有权限查看它的修改时间呢?是不是就不能知道了。于是,存在这样一种存储方式,我们通过路径就可以确定一个文件,且这个文件一定是我们需要的,未被修改的文件,就是内容可寻址文件存储

CAS如何实现

如何唯一标识一个文件?一般会想到使用文件摘要算法。常见的算法如SHA256,SHA512。

1
2
[root@skywater ~]# sha256sum temp.sh 
f6484a310090a07d701b33b9a3df060dc157a510408a5e933b86c6a40aec0cf2 temp.sh

CAS就是使用了文件摘要算法,如果我们将系统中所有文件的摘要作为key,而文件内容作为value,就可以形成一张hash表。对整个系统目录进行打包后再hash,作为一个tag,那么这个tag将会保证内容是不变的。

而换句话来说,如果一个系统经常发生变化,那么就不适合使用这样的存储方式,应为变动会引起hash变化,导致重新计算的负担。

容器技术中的镜像文件就恰恰有这样一个需求,那就是保证不可变性。我今天使用了这个镜像作为基础建立了另一个镜像,明天这个基础居然被人改了??导致我的应用出现了完全不可预知的问题,这是绝对不允许的。所以,CAS很适合作为镜像的存储方式。

image-spec(镜像标准)

OCI image-spec最早就是由当时的容器领头羊Docker贡献出来的,所以在后来的演化过程中,Docker镜像本身也是符合OCI规范的。

整体来看OCI image-spec,需要包含以下内容:

  • Image Manifest

    描述构成容器镜像的组件的清单文件

  • Image Index

    图像清单的注释索引

  • Image Layout

    表示图像内容的文件系统布局

  • Filesystem Layer

    描述容器文件系统的变更集

  • Image Configuration

    定义了运行时bundle中的layer顺序以及配置信息

  • Conversion

    将定义标签转换成实际的运行时字段以及指令,比如image中的Config.WorkingDir转换为运行时的process.cwd

  • Descriptor

    元数据描述对象,使用key-value形式表示。

    必须存在的属性包括:mediaTypedigestsize

    可选属性包括urlsannotationsdata

当然,作为具体的OCI image-spec实现,Docker也会有一些不一样的点。

这里将OCI标准与Docker镜像进行对比,把OCI image-spec中的标识符一一对应到Docker镜像,这样可以更好的理解OCI。

媒体类型(mediaType)

媒体类型常见于http请求的hearder中,用来定义所指向的资源类型。通过mediaType,我们就可以使用正确的方式去解析数据。如果是gzip类型,那就需要用tar工具解压,如果是excel类型,就需要用Office、WPS之类的软件打开。镜像文件、以及镜像文件中包含的各种内容都有它们对应的mediaType。

后续涉及到容器内容各个点时再明确每个内容对应的媒体类型。

各媒体类型间的依赖关系如下:

img

镜像结构

img

该图引用自https://guide.daocloud.io/dcs/docker-9153976.html

Docker镜像由Dockerfile生成,通常会选择一个镜像作为基础镜像,如图上的ubuntu:14.04,随后根据我们的指令,向上依次堆叠层(layer)。最顶层为Read-write Layer,即读写层,可以挂在到宿主机。

或许这么讲还是有些抽象,我们直接把Docker的image下载到本地来分析。

构建使用的Dockerfile如下。

1
2
3
4
5
6
7
8
FROM openjdk:8-jdk-alpine
MAINTAINER "skywater"
LABEL description="testImage"
RUN mkdir /tmp/logs
WORKDIR app
COPY ./target/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["/bin/sh", "-c", "java", "-jar", "app.jar"]

执行

1
2
3
[root@skywater docker]# docker save 106.15.92.100:5000/lts/springboot-demo:v2 | gzip > springboot-demo.tar.gz
[root@skywater docker]# mkdir springboot-demo && tar -zxvf springboot-demo.tar.gz -C springboot-demo
[root@skywater docker]# tree springboot-demo

解压后目录结构

image-20220307212727000

可以看到,镜像内容中包含了一个manifest,以及多个包含了layer.tar的文件夹。这里每个layer.tar就对应了图上的每一层,排列顺序,以及层的配置信息,就在manifest.json以及每个文件夹中的json当中。

了解了镜像文件的大概结构,我们再对照OCI规范深入去看一下每一个文件的作用。

Image Layout

这时一个整体结构的定义,OCI定义中,image内容结构需要包含以下内容:

index.json文件,oci-layout文件,以及blobs目录。

结构类似于:

1
2
3
4
5
blobs
|--- 987fads67897gadfas
|--- 8908a80g7b65cvb223
oci-layout
index.json

然而,从解压后的镜像内容来看,Docker目前没有适配这块规范,所以并没有这些文件与目录。

虽然Docker镜像并不完全符合OCI规范,譬如这里的image-layout,但并不意味着与OCI不兼容,OCI需要的信息,会以其他的结构展现在Docker镜像结构中。

如果需要获取的话,可以通过工具将docker镜像转换为oci镜像,使用skopeo工具:

1
$ skopeo copy docker://springboot-demo:v1 oci:springboot-demo

查看本地目录springboot-demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@skywater tmp]# tree springboot-demo/ --du
springboot-demo/
├── [ 75877939] blobs
│   └── [ 75877919] sha256
│   ├── [ 19413] 099e5f0c6733b339ec37c98f725fae41b872ebc9064619f7f44ad93cf939f5ea
│   ├── [ 249] 0e735f96c2df7edc67964a683c77805c0285371c0e71bacd491f38011b29829b
│   ├── [ 73006326] 4b2f0c876f4462376e74633289dbbb6e32d5c542ef698bd9f6bf24e97ed22aaf
│   ├── [ 2842856] 74f1fcf320dc694abfb36b0c3a7383c09e15fc4edfe76102e71a3224f8ff1ed8
│   ├── [ 161] 782e025922b72ccca53e4a386c1f7b784e602294434d4bcc84786dccfc622e40
│   ├── [ 3556] 9e63d81810f866d774e2a5b8e6be8d4c283c462214d75be2eede7019d4b03303
│   ├── [ 136] db69bb6d4d2aee35e87908bdafe2428b144513ff8c3b1d1f3744272d11717343
│   └── [ 1126] e6c11218c3533a6c1314bfa3f55acf940d11830ce6e148f4c1004f2a6d508901
├── [ 187] index.json
└── [ 31] oci-layout

75878212 bytes used in 2 directories, 10 files
  • index.json

    image-20220308112923586

    其中将包含一个Image index对象,后续再详解Image Index。目前只需要知道,通过index.json就可以找到Image Index的hash位置即可。

  • oci-layout

    1
    2
    [root@skywater springboot-demo]# cat oci-layout
    {"imageLayoutVersion": "1.0.0"}

    内容很简单,只有一个版本号,这个版本为整个OCI image文件目录布局结构的规划版本,目前使用的是1.0.0。

  • blobs

    包含了使用hash进行查找的镜像配置文件、层内容等信息

通过index.sjon,可以定位到各平台对应的manifest的hash位置。再通过manifest,可以找到image-config,而image-config中,又将包含各个layer的具体配置信息,这样就可以将所有内容串联起来,runtime也就知道了容器运行需要的所有基本信息。

Image Index

镜像索引,这个内容是可选的,一个镜像可能适用于多个平台架构,单个manifest并不能满足要求,所以我们可以提供多个manifest组成一个Image Index,来实现跨平台。

OCI对Image 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
{
// 必选
"schemaVersion": 2,
// 应该被使用
"mediaType": "application/vnd.oci.image.index.v1+json",
// 必选
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7143,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
],
"annotations": {
"com.example.key1": "value1",
"com.example.key2": "value2"
}
}

虽然Docker没有符合Image-Layout规范,但仍然可以找到Image-Layout中指向的Image Index。

在我们上面springboot-demo的例子中体现不出来,没有做跨平台,我们可以换一个dockerhub中的镜像看下其中的内容。

1
[root@skywater ~]# docker manifest inspect nginx

以Nginx镜像为例,可以看到一个媒体类型为application/vnd.docker.distribution.manifest.list.v2+json的manifest列表,其实就是对应了Image index,其中属性可以一一对应上。

image-20220308092044955

image-20220308094504456

  • mediaType

    Docker OCI
    application/vnd.docker.distribution.manifest.list.v2+json application/vnd.oci.image.index.v1+json

    表示该文件是镜像清单文件。

  • manifests

    manifest列表,对应特定平台的manifest,其中会包含platform信息,不同平台架构、系统需要有不同的镜像清单。

    • mediaType

      即为上文提到的Image Manifest媒体类型

    • platform

      描述镜像运行的环境要求,类似于定义一个软件必须是windows 10 再能用的一样。

Image Manifest

通过Image Index,我们可以找到该镜像在当前平台系统对应的manifest。

Image Index是为了跨平台,而相反的,manifest的主要目的,是为特定架构和操作系统单个容器映像提供配置和镜像层集

OCI对manifest的定义概览如下:

  • 必须属性

    schemaVersionmediaType ,**config** ,**layers** ,layers下的mediaType

  • 可选属性

    annotations

OCI Image 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
30
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"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"
}
}

我们来找一找,Docker中的manifest.json是否包含了这些内容。

读取save到本地的Docker image中的manifest.json内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@skywater docker]# cat springboot-demo/manifest.json | jq
[
{
"Config": "4a030f9340dfd2456fe7ddaa23dd65571eb2987c8c038e1482adf46fd48b3aa4.json",
"RepoTags": [
"springboot-demo:v1"
],
"Layers": [
"1a58e6937db044ef6f2e2962a0dc7bef16a6c33fdfc5a0318c39092612a1bd1a/layer.tar",
"c12f86d2a60fc27a1d93d555944262fda4ed66e3a3172ac45cd861151a0dc6c1/layer.tar",
"98867178f60349f16652222772d086159a6d087fcd50bc32b9d75c23cd01ed8d/layer.tar",
"91b1ccb4946286a411dc7e11717ad45e71b9235a6a18f6ec183f79ae3a3693bc/layer.tar",
"cc2781c6b5a3376fcfae998304d99c071d5faa6630b13f50e85acfab759d4167/layer.tar",
"4409a35858a53e68fd42b6ac63ab0c199ba220817ddc0436559467686f862042/layer.tar"
]
}
]

可以看到,配置文件以及layer目录均为hash值,也对应了之前所说的“内容可寻址文件存储”

或者,可以使用docker的manifest命令:

1
[root@skywater springboot-demo]# docker manifest inspect --insecure 106.15.92.100:5000/lts/springboot-demo:v1

image-20220307223018252

  • schemaVersion

    代表为v2版本的manifest。ps:v1版本已废弃。

  • mediaType

    第二个标识为上一章节提到的mediaType,为application/vnd.docker.distribution.manifest.v2+json

    而在OCI标准中,manifest的mediaType标准值为application/vnd.oci.image.manifest.v1+json

    略有差别,但OCI兼容Docker的这种mediaType。

  • config

    对应OCI标准中的Image Configuration(镜像配置文件)。

    这里镜像配置文件的格式为application/vnd.docker.container.image.v1+json

    对应OCI标准中的镜像格式为application/vnd.oci.image.index.v1+json

    OCI兼容Docker镜像配置文件格式。

    通过manifest,可以定位到image config文件hash位置。这里获取到的hash为4a030f9340dfd2456fe7ddaa23dd65571eb2987c8c038e1482adf46fd48b3aa4.json,可以在镜像内容中找到该文件。后续再进行细节分析。

  • layers

    一个Descriptor Array(描述符数组),描述了文件系统layer的布局,且必须符合堆栈顺序。

    可以发现,下载下来的manifest.json中的layer的hash,和使用docker manifest inspect命令解析出来的不一样,原因是下载下来的是tar格式的bundle,而inspect的是tar.gzip格式。

    • mediaType

      可以看到,其mediaType为application/vnd.docker.image.rootfs.diff.tar.gzip。根据其提供的hash,可以在下载下来的镜像内容目录中,按顺序找到每个对应的layer.tar文件。

  • annotations

    这项在Docker的manifest中没有体现,可以理解为自己给manifest打上一些标记,内容自行定义,但需要符合key-value格式,以及annotation rules规范。

回顾image manifest(镜像清单)的内容,其中定义了这个镜像清单的版本,镜像配置文件的地址,以及每个layer的地址、排列顺序。是一个镜像的整体概览文件。其中每个文件都有他们的mediaType,由于是docker镜像,所以mediaType中都含有docker的标识符(xxx.docker.xxx)。

Image Configuration

根据manifest内容,我们可以查找到该镜像的Image configuration文件。

OCI中,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
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
{
// 可选,表示创建镜像的组合日期和时间
"created": "2015-10-31T22:22:56.015925234Z",
// 可选,表示创建镜像的作者
"author": "Alyssa P. Hacker <alyspdev@example.com>",
// 必填,表示此镜像运行需要的CPU架构
"architecture": "amd64",
// 必填,表示此镜像运行的系统
"os": "linux",
// 可选,将定义容器endpoint相关参数。
"config": {
"User": "alice",
"ExposedPorts": {
"8080/tcp": {}
},
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"FOO=oci_is_a",
"BAR=well_written_spec"
],
"Entrypoint": [
"/bin/my-app-binary"
],
"Cmd": [
"--foreground",
"--config",
"/etc/my-app.d/default.cfg"
],
"Volumes": {
"/var/job-result-data": {},
"/var/log/my-app-logs": {}
},
"WorkingDir": "/home/alice",
"Labels": {
"com.example.project.git.url": "https://example.com/project.git",
"com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
}
},
// 必填,定义image需要用到的layer的地址
"rootfs": {
"diff_ids": [
// layer归档的hash,顺序为layer从底到顶的顺序,即diff_ids[0]对应的layer处于image最底层
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
],
// 必填,且必须是layers,验证时如果出现未知值将会抛出错误
"type": "layers"
},
// 可选的,用来描述层的历史信息
"history": [
{
"created": "2015-10-31T22:22:54.690851953Z",
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
},
{
"created": "2015-10-31T22:22:55.613815829Z",
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
"empty_layer": true
},
{
"created": "2015-10-31T22:22:56.329850019Z",
"created_by": "/bin/sh -c apk add curl"
}
]
}

大体了解的OCI中的Image Configuration,我们来看下Docker中的有何区别。

首先需要找到Docker中的Image Configuration在哪,根据Manifest中的描述,其位置在sha256:4a030f9340dfd2456fe7ddaa23dd65571eb2987c8c038e1482adf46fd48b3aa4

1
2
[root@skywater springboot-demo]# ls -l|grep 4a030f9340d
-rw-r--r-- 1 root root 4695 Feb 14 17:01 4a030f9340dfd2456fe7ddaa23dd65571eb2987c8c038e1482adf46fd48b3aa4.json

可以看到,springboot-demo目录下确实有一个这个文件。

查看其内容:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
{
"architecture": "amd64",
"author": "\"skywater\"",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin",
"LANG=C.UTF-8",
"JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk",
"JAVA_VERSION=8u212",
"JAVA_ALPINE_VERSION=8.212.04-r0"
],
"Cmd": null,
"Image": "sha256:2b6a6388a9a8cc96d4b34abcf222b981a9abd86e999b33697ad63ffbe3eadafb",
"Volumes": null,
"WorkingDir": "/app",
"Entrypoint": [
"java",
"-jar",
"app.jar"
],
"OnBuild": null,
"Labels": {
"description": "testImage"
}
},
"container": "6dc97c1a3b1f527df5fd18460ce766e1960a0d7a4f07a013b884e74d4a5c79be",
"container_config": {
"Hostname": "6dc97c1a3b1f",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin",
"LANG=C.UTF-8",
"JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk",
"JAVA_VERSION=8u212",
"JAVA_ALPINE_VERSION=8.212.04-r0"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"ENTRYPOINT [\"java\" \"-jar\" \"app.jar\"]"
],
"Image": "sha256:2b6a6388a9a8cc96d4b34abcf222b981a9abd86e999b33697ad63ffbe3eadafb",
"Volumes": null,
"WorkingDir": "/app",
"Entrypoint": [
"java",
"-jar",
"app.jar"
],
"OnBuild": null,
"Labels": {
"description": "testImage"
}
},
"created": "2022-02-14T09:01:30.341153211Z",
"docker_version": "20.10.7",
"history": [
{
"created": "2019-05-11T00:07:03.358250803Z",
"created_by": "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / "
},
{
"created": "2019-05-11T00:07:03.510395965Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:11.0323736Z",
"created_by": "/bin/sh -c #(nop) ENV LANG=C.UTF-8",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:12.044405061Z",
"created_by": "/bin/sh -c { \t\techo '#!/bin/sh'; \t\techo 'set -e'; \t\techo; \t\techo 'dirname \"$(dirname \"$(readlink -f \"$(which javac || which java)\")\")\"'; \t} > /usr/local/bin/docker-java-home \t&& chmod +x /usr/local/bin/docker-java-home"
},
{
"created": "2019-05-11T01:32:12.271831312Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:12.506789049Z",
"created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:12.755877628Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_VERSION=8u212",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:13.051281176Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8.212.04-r0",
"empty_layer": true
},
{
"created": "2019-05-11T01:32:17.777332452Z",
"created_by": "/bin/sh -c set -x \t&& apk add --no-cache \t\topenjdk8=\"$JAVA_ALPINE_VERSION\" \t&& [ \"$JAVA_HOME\" = \"$(docker-java-home)\" ]"
},
{
"created": "2022-02-14T08:58:24.52137399Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) MAINTAINER \"skywater\"",
"empty_layer": true
},
{
"created": "2022-02-14T08:58:24.635972282Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) LABEL description=testImage",
"empty_layer": true
},
{
"created": "2022-02-14T08:58:25.287249679Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c mkdir /tmp/logs"
},
{
"created": "2022-02-14T08:58:25.411811261Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) WORKDIR /app"
},
{
"created": "2022-02-14T09:01:30.110467033Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) COPY file:6b7274fe9bd12de07992236f81d0b51d17356a0cc3df9a153584843d4f73c3cf in /app/app.jar "
},
{
"created": "2022-02-14T09:01:30.226144552Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) EXPOSE 8080",
"empty_layer": true
},
{
"created": "2022-02-14T09:01:30.341153211Z",
"author": "\"skywater\"",
"created_by": "/bin/sh -c #(nop) ENTRYPOINT [\"java\" \"-jar\" \"app.jar\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81",
"sha256:9b9b7f3d56a01e3d9076874990c62e7a516cc4032f784f421574d06b18ef9aa4",
"sha256:ceaf9e1ebef5f9eaa707a838848a3c13800fcf32d7757be10d4b08fb85f1bc8a",
"sha256:611c936fd113e79ca783c390c3abe2877ed041f70b9cf034749a269692b5acdc",
"sha256:265cbbb1847e96bd58716d5faebea214c613098ddca73253dd2d7229ddf80865",
"sha256:50522ab76e98bd51cedd8d7442cb7b74c7e455f6e6d6efc2158a78f3185ca5e8"
]
}
}

可以发现其与OCI中的Image Configuration定义基本一致。

关注其中几个属性:

  • config

    参照我们最开始的Dockerfile,config里面对应了容器暴露的端口、Endpoint配置。

  • history

    empty_layer如果为true,则代表只是修改了配置信息,而未变动文件内容,所以并不需要生成新的layer以及对应hash,只要在这里记录配置操作即可。

    首先,Dockerfile中定义了From openjdk:8-jdk-alpine,所以会有相关jdk1.8的一些指令。

    我们每定义一次RUN、COPY等引起layer内容发生变化的操作时,就会生成新的layer,empty_layer属性也将会是默认的false。

    从这里可以看出,Dockerfile指令与Docker Image内容形成了完整的联系。

  • rootfs.diff_ids

    如果仔细观察diff_ids中的hash,发现无论是image manifest中的layers,还是说本地解压镜像文件的blobs中,都从未出现过这些ID。

    其实,这些ID是layer归档文件的ID,而非layer压缩归档的ID。我们只要计算下layer.tar的hash值,就可以和这里对应上了。

    譬如diff_ids[0] = “sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81”

    1
    2
    [root@skywater springboot-demo]# sha256sum 1a58e6937db044ef6f2e2962a0dc7bef16a6c33fdfc5a0318c39092612a1bd1a/layer.tar
    f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81 1a58e6937db044ef6f2e2962a0dc7bef16a6c33fdfc5a0318c39092612a1bd1a/layer.tar

    可以看到该layer.tar的hash值就是f1b5933...

    实际在docker本地环境中的存储位置为/var/lib/docker/image/overlay2/layerdb/sha256/f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81

    镜像是由layer构成的,layer文件以hash摘要寻址存放,所以,不同的image可以复用相同的layer。

Filesystem Layer

Image Manefest中除了Config信息,还有一块内容就是对Layers的描述。

OCI对Layer的mediaType定义与Docker中略有不同。

OCI Docker
application/vnd.oci.image.layer.v1.tar
application/vnd.oci.image.layer.v1.tar+gzip
application/vnd.oci.image.layer.v1.tar+zstd
application/vnd.oci.image.layer.nondistributable.v1.tar
application/vnd.oci.image.layer.nondistributable.v1.tar+gzip
application/vnd.oci.image.layer.nondistributable.v1.tar+zstd
application/vnd.docker.image.rootfs.diff.tar.gzip

可以通过解压layer.tar查看每一层的具体内容。

Docker容器实质上就是层与层的叠加,上方层可以通过覆盖的方式实现对下方层内容的增删改。最上方的层为读写层,会在从镜像转变为容器时生成,用于与宿主机频繁交互。其余下方的层只有只读权限,无法进行修改。

最后,需要有一个联合挂载系统(Docker一般使用overlay),将各个层联合起来,是指成为一个整体,用户视图将只存在一个。

总结

Docker作为OCI最初的贡献者,随着时间演化,其具体实现与OCI标准略有不同,但互相兼容。均通过manifest list来查找合适的镜像文件,并采用分层结构,使用manifest来定义层的顺序与位置,并指向层配置文件。通过读取层配置文件,就可以获取到所有具体的层配置信息了。从而构建了完整的镜像内容架构,提供给具体镜像实现工具一个统一的流程规范。