1. 分层的镜像
在我们启动一个容器之前,通常需要下载这个容器对应的镜像,以这个镜像为基础启动容器。镜像中包含了对应的程序的二进制文件与其所依赖的文件,程序在启动后看到的rootfs只是这个镜像中存在的文件。这样,我们就可以为容器中的进程提供一个干净的文件系统。
创建一个镜像(image
)的最简单方法是使用Dockerfile。
FROM scratch
COPY hello /
CMD ["/hello"]
scratch
是docker为我们提供的一个空镜像,我们可以在此基础上构建任何我们想要的镜像。
在书写Dockerfile
时,想必你听说过这么一句话,不要在Dockerfile中创建太多层.
在Dockerfile中,每一个指令都会创建一个新的“层”,这里的层,指的是UnionFS中的一个文件目录。当我们创建了过多的层,会导致镜像体积变大,除此之外,Union FS 也会有最大层数限制。
因此对于如下的Dockerfile文件写法,应尽量避免:
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
可优化为如下写法
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps
这里的原理有点类似于Redis的AOF日志重写。
为什么要将镜像分为很多层,而不是像系统镜像那样打包为一个ISO文件。
不同的镜像,其所需的文件存在重复,如果对每个镜像都单独复制一份,会导致镜像过于臃肿,浪费磁盘空间,也会占用大量网络资源下载镜像文件。
例如我们有100个服务都需要依赖于ubuntu18.04环境,如果我们需要为这些服务制作镜像,显然不可能真的将ubuntu18.04的所有文件复制100份,最好的办法是机器上只存在一份这样的文件,每个容器都来复用它,这就需要使用UnionFS。
2. image分层的基础:UnionFS
UnionFS称:联合挂载。其最主要的功能是将多个目录挂载在同一个目录下。
前面说到,对于100个都需要Ubuntu18.04环境的服务,最好的办法是在机器上提供一份ubuntu18.04环境文件,所有的服务都来依赖这一份文件。
但这就产生了另一个问题:如果某一个服务需要对ubuntu18.04中的某个文件进行修改,或是在这之上产生新的文件,那么其他同样依赖于这个环境的服务可能会被影响。
这就需要用到UnionFS的特性。
目前docker使用的UnionFS实现是OverlayFS.下面通过一个小实验来掌握overlay的基本特性:
执行以下脚本
#!/bin/bash
umount ./merged
rm upper lower merged work -r
mkdir upper lower merged work
echo "I'm from lower!" > lower/in_lower.txt
echo "I'm from upper!" > upper/in_upper.txt
# `in_both` is in both directories
echo "I'm from lower!" > lower/in_both.txt
echo "I'm from upper!" > upper/in_both.txt
sudo mount -t overlay overlay \
-o lowerdir=./lower,upperdir=./upper,workdir=./work \
./merged
OverlayFS 的一个 mount 命令牵涉到四类目录,分别是 lower,upper,merged 和 work,
“lower/",也就是被 mount 两层目录中底下的这层(lowerdir)。在 OverlayFS 中,最底下这一层里的文件是不会被修改的,你可以认为它是只读的。OverlayFS 支持多个 lowerdir。
“upper/",它是被 mount 两层目录中上面的这层 (upperdir)。在 OverlayFS 中,如果有文件的创建,修改,删除操作,那么都会在这一层反映出来,它是可读写的。
“merged” ,是挂载点(mount point)目录,也是用户看到的目录,用户的实际文件操作在这里进行。
“work/",是一个存放临时文件的目录,OverlayFS 中如果有文件修改,就会在中间过程中临时存放文件到这里。并未表现在图中。
如果lower和upper中存在同名文件,那么不会显示lower中的文件。
对于以下三种文件操作:
新建文件。这个文件将会出现在upper目录中。
删除文件。如果我们删除"in_upper.txt”,那么这个文件会在 upper/ 目录中消失。如果删除"in_lower.txt”, 在 lower/ 目录里的"in_lower.txt"文件不会有变化,只是在 upper/ 目录中增加了一个特殊文件来告诉 OverlayFS,“in_lower.txt’这个文件不能出现在 merged/ 里了,这就表示它已经被删除了。
修改文件。如果修改"in_lower.txt”,那么就会在 upper/ 目录中新建一个"in_lower.txt"文件,包含更新的内容,而在 lower/ 中的原来的实际文件"in_lower.txt"不会改变。
所有的更改都不会反映到lowerdir上,因此容器的镜像文件中各层正好作为 OverlayFS 的 lowerdir 的目录,然后加上一个空的 upperdir 一起挂载好后,就组成了容器的文件系统。
同时,overlayfs还支持挂载多个lower目录,这样就实现了镜像的多个层。
以下是一个正在运行的容器他的UnionFS挂载情况:
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/94809845e9decf34968235fe45bc9d66780a6c2f1929e6aeace43c62ae8a8473-init/diff:/var/lib/docker/overlay2/c95ef099ca18c9b33ff36907bb94c28f3c81a9758058b1698ae438fc88552ac7/diff:/var/lib/docker/overlay2/84cc0780b885789c335f30a784fde4355bfc11b4b9c538da0d6be665d0e87f74/diff:/var/lib/docker/overlay2/94ab5f531aa05c31918599de4a947b6af18488a5bd9f62186060aa32a0938677/diff:/var/lib/docker/overlay2/4154cfa0480d3be6d22ecd4b52225798d8c7d2f349602d36a821fd310318f19e/diff",
"MergedDir": "/var/lib/docker/overlay2/94809845e9decf34968235fe45bc9d66780a6c2f1929e6aeace43c62ae8a8473/merged",
"UpperDir": "/var/lib/docker/overlay2/94809845e9decf34968235fe45bc9d66780a6c2f1929e6aeace43c62ae8a8473/diff",
"WorkDir": "/var/lib/docker/overlay2/94809845e9decf34968235fe45bc9d66780a6c2f1929e6aeace43c62ae8a8473/work"
},
"Name": "overlay2"
},
3. 镜像的组成
docker对镜像的管理是根据OCI标准来的,根据OCI标准,一个image可以分为以下四个部分:
Image Index
: image index是个json文件,用于使image能支持多个平台和多tag。Image Manifest
: manifest是一个json文件,这个文件包含了对filesystem layers和image config的描述。Image Config
: image config就是一个json文件,这个json文件包含了对这个image的描述。FileSystem Layer
: Filesystem Layer包含了文件系统的信息,即该image包含了哪些文件/目录,以及它们的属性和数据。
它们之间的关系如下:
3.1 Media Type
Media Type, 媒体类型。
在OCI标准中,不同的文件对应不同的media type。其对应关系如下:
Media Type | 说明 |
---|---|
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 | Image Layer 镜像层文件 |
application/vnd.oci.image.layer.v1.tar+gzip | Image Layer 镜像层文件gzip压缩 |
application/vnd.oci.image.layer.nondistributable.v1.tar | Image Layer 非内容寻址管理 |
application/vnd.oci.image.layer.nondistributable.v1.tar+gzip | Image Layer, gzip压缩 非内容寻址管理 |
3.2 FileSystem Layer
每个filesystem layer都包含了在上一个layer上的改动情况,主要包含三方面的内容:
变化类型:是增加、修改还是删除了文件
文件类型:每个变化发生在哪种文件类型上
文件属性:文件的修改时间、用户ID、组ID、RWX权限等
根据我们上面所介绍的UnionFS,很容易理解。
3.3 Image Config
Config包含了对镜像文件的描述,它是一个json文件,示例如下:
{
"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": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"FOO=oci_is_a",
"BAR=well_written_spec"
],
...
},
"rootfs": {
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
],
"type": "layers"
},
"history": [
...
]
}
其中大部分是可选参数,必须的参数有:
- architecture: CPU架构
- os: 镜像在什么操作系统上运行
- rootfs:指定了image所包含的filesystem layers,type的值必须是layers,diff_ids包含了layer的列表(顺序排列),每一个sha256就是每层layer对应tar包的sha256码
- type: type的值必须是layers
- diff_ids: 每层layer包的哈希值的列表
其他都是可选参数,见名知意
3.4 Image Manifest
Image Manifest为特定架构和操作系统的单个容器镜像提供配置和一系列的layer。
Manifest 的三大主要目标:
内容可寻址(通过 hash 算法为镜像和它的组件生成唯一ID)
支持多种平台的架构镜像(由一个更上层的 manifest 说明包含的镜像 manifests 其具体平台的版本情况)
能被解析成为 OCI 运行时规范
示例如下:
{
"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"
}
}
从上面的json文件中可以看出,manifest中包含对config的描述,包括其mediaType, size,digest等,这里的digest就是config文件的sha256值,可以看出manifest与config的一对一对应关系。而layer中则指明了这个manifest包含哪些层,manifest与layer是一对多关系。
3.5 Image Index
Image Index指向一个特定manifests的更高层manifest,用于支持多平台。index是可选的。
示例如下:
{
"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"
}
}
index文件包含了对image中所有manifest的描述,相当于一个manifest列表,包括每个manifest的media type,文件大小,sha256码,支持的平台以及平台特殊的配置。
比如ubuntu想让它的image支持amd64和arm64平台,于是它在两个平台上都编译好相应的包,然后将两个平台的layer都放到这个image的filesystem layers里面,然后写两个config文件和两个manifest文件,再加上这样一个描述不同平台manifest的index文件,就可以让这个image支持两个平台了,两个平台的用户可以使用同样的命令得到自己平台想要的那些layer。
3.6 镜像layout
镜像的布局包含以下内容:
- blobs:内容寻址的块文件,目录必须存在,但是可以为空
- oci-layout: 必须存在的json对象,必须包含
imageLayoutVersion
字段 - index.json: 必须存在的JSON格式,文件中必须包含镜像Index的基本属性
可以查看hello-world镜像的内容:
$ yay -S skopeo # 安装skopeo
$ skopeo copy docker://hello-world oci:hello-world # 利用skopeo下载hello-world镜像
$ tree -L 3 hello-world
hello-world
├── blobs
│ └── sha256
│ ├── 2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
│ ├── 75ab15a4973c91d13d02b8346763142ad26095e155ca756c79ee3a4aa792991f
│ └── 811f3caa888b1ee5310e2135cfd3fe36b42e233fe0d76d9798ebd324621238b9
├── index.json
└── oci-layout
index.json
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:75ab15a4973c91d13d02b8346763142ad26095e155ca756c79ee3a4aa792991f",
"size": 402
}
]
}
oci-layout
{
"imageLayoutVersion": "1.0.0"
}
4. 从远程获取镜像的过程
docker发送image的名称+tag(或者digest)给registry服务器,服务器根据收到的image的名称+tag(或者digest),找到相应image的manifest,然后将manifest返回给docker
docker得到manifest后,读取里面image配置文件的digest(sha256),这个sha256码就是image的ID
根据ID在本地找有没有存在同样ID的image,有的话就不用继续下载了
如果没有,那么会给registry服务器发请求(里面包含配置文件的sha256和media type),拿到image的配置文件(Image Config)
根据配置文件中的diff_ids(每个diffid对应一个layer tar包的sha256,tar包相当于layer的原始格式),在本地找对应的layer是否存在
如果layer不存在,则根据manifest里面layer的sha256和media type去服务器拿相应的layer(相当去拿压缩格式的包)。
拿到后进行解压,并检查解压后tar包的sha256能否和配置文件(Image Config)中的diff_id对的上,对不上说明有问题,下载失败
根据docker所用的后台文件系统类型,解压tar包并放到指定的目录
等所有的layer都下载完成后,整个image下载完成,就可以使用了
上面的过程涉及到两个接口:
GET /v2/<name>/manifests/<reference>
其中为镜像名称,可能包含tag或digest。
这个接口用于获取镜像的manifest。
GET /v2/<name>/blobs/<digest>
这个接口用于获取镜像的layer
5. 镜像在本地的存储
镜像默认在系统中的存储位置在
/var/lib/docker
这里我们单独选择一个镜像
golang:1.16.6
来看
5.1 镜像的digest
镜像的digest即为镜像manifest文件的sha256值,当镜像内容发生变化,即其中的layer发生了变化,则其layer的sha256值必然发生变化,而相应的包含layer哈希值的manifest必然也会发生变化,这样就保证了digest能唯一对应一个镜像。
5.2 repositories.json
repositories.json中记录了和本地image相关的repository信息,主要是name和image id的对应关系,当image从registry上被pull下来后,就会更新该文件,其位置在/var/lib/docker/image/overlay2/repositories.json
$ cat repositories.json | jq
{
"Repositories": {
"golang": {
"golang:1.16.6": "sha256:028d102f774acfd5d9a17d60dc321add40bea5cc6f06e0a84fd3aca1bb4c2b12",
"golang@sha256:4544ae57fc735d7e415603d194d9fb09589b8ad7acd4d66e928eabfb1ed85ff1": "sha256:028d102f774acfd5d9a17d60dc321add40bea5cc6f06e0a84fd3aca1bb4c2b12",
},
...
},
}
记住golang:1.16.6的digest为028d102f774acfd5d9a17d60dc321add40bea5cc6f06e0a84fd3aca1bb4c2b12
,记住前三个字母就可以了028
5.3 配置文件(image config)
在从服务器获取image时,会先获取manifest,然后从manifest中拿到config的hash,从而获取config,保存在/var/lib/docker/image/overlay2/imagedb/content/sha256
,文件名就是image id
$ cat 028d102f774acfd5d9a17d60dc321add40bea5cc6f06e0a84fd3aca1bb4c2b12 | jq
{
...
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:afa3e488a0ee76983343f8aa759e4b7b898db65b715eb90abc81c181388374e3",
"sha256:4b0edb23340c111e75557748161eed3ca159584871569ce7ec9b659e1db201b4",
"sha256:5a9a65095453efb15a9b378a3c1e7699e8004744ecd6dd519bdbabd0ca9d2efc",
"sha256:ad83f0aa5c0abe35b7711f226037a5557292d479c21635b3522e1e5a41e3ce23",
"sha256:d1c59e37fbfc7294184d6fbe4ff8e1690d9119b6233f91af5ad0a4b36e45dff7",
"sha256:e46b2fd4e4eadc0ee107417c66e968f417e0759fd7d625ab6f0537ba02c1c868",
"sha256:9672a02ff8cffb302a4c5cef60f8b36cbbe9709a5ba78b7d0ce56db3219c5e51"
]
}
}
这里要注意的时,如果image存在多个层,即diff_ids数组长度大于一,则diff_ids[0]
是UnionFS最底层,diff_ids[-1]
是UnionFS最高层。
5.4 layer的diff_id和digest的对应关系
layer的diff_id存在image的配置文件中,而layer的digest存在image的manifest中,他们的对应关系被存储在了/var/lib/docker/image/overlay2/distribution
目录下:
.
├── diffid-by-digest
│ └── sha256
└── v2metadata-by-diffid
└── sha256
diffid-by-digest: 存放digest到diffid的对应关系
v2metadata-by-diffid: 存放diffid到digest的对应关系
查看mysql最上层layer的digest:
$ cd /var/lib/docker/image/overlay2/distribution/v2metadata-by-diffid/sha256
$ cat 9672a02ff8cffb302a4c5cef60f8b36cbbe9709a5ba78b7d0ce56db3219c5e51 | jq # sha256:967...是golang:1.16.6的最上层layer
[
{
"Digest": "sha256:ff36ba4656980ea99a067c8a9b39f210a4da82badccaa2bae27317e711985668",
"SourceRepository": "docker.io/library/golang",
"HMAC": ""
}
]
5.5 layer元数据
从image config中我们可以得到diff_ids,从而得到image各个层的sha256值,这时我们可以到/var/lib/docker/image/overlay2/layerdb/sha256
去查看这些层,例如golang:1.16.6的最底层sha256为afa3e488a0ee76983343f8aa759e4b7b898db65b715eb90abc81c181388374e3
:
$ cat /var/lib/docker/image/overlay2/layerdb/sha256/afa3e488a0ee76983343f8aa759e4b7b898db65b715eb90abc81c181388374e3
$ tree
.
├── cache-id
├── diff
├── size
└── tar-split.json.gz
但在查看第二层的时候发现:
$ cd 4b0edb23340c111e75557748161eed3ca159584871569ce7ec9b659e1db201b4
bash: cd: 4b0edb23340c111e75557748161eed3ca159584871569ce7ec9b659e1db201b4: 没有那个文件或目录
这是因为docker使用了chainID的方式去保存这些layer,简单来说就是chainID=sha256sum(H(chainID), diffid)
,因此golang:1.16.6的第二层为:
$ echo -n "sha256:afa3e488a0ee76983343f8aa759e4b7b898db65b715eb90abc81c181388374e3 sha256:4b0edb23340c111e75557748161eed3ca159584871569ce7ec9b659e1db201b4" | sha256sum -
c21ff68b02e7caf277f5d356e8b323a95e8d3969dd1ab0d9f60e7c8b4a01c874 -
这时候查看c21ff68b02e7caf277f5d356e8b323a95e8d3969dd1ab0d9f60e7c8b4a01c874
:
$ cd c21ff68b02e7caf277f5d356e8b323a95e8d3969dd1ab0d9f60e7c8b4a01c874
$ tree
.
├── cache-id
├── diff
├── parent
├── size
└── tar-split.json.gz
以此类推,我们可以找到任意一层。
在每一层中,一般包含5个文件:
- cache-id是docker下载layer的时候在本地生成的一个随机uuid,指向真正存放layer文件的地方
- diff文件存放layer的diffid
- parent文件存放当前layer的父layer的diffid,而由于最底层layer没有parent,因此它没有这个文件
- size当前layer的大小,单位是字节
- tar-split.json.gz,layer压缩包的split文件,通过这个文件可以还原layer的tar包
打印出golang:1.16.6第二层的cache-id:
$ cat cache-id
40a11a2b4f594bcccbe2cbb6cd3e6fa4a3c2f8427817f43bbade4331173611d2
5.6 layer数据
layer数据在/var/lib/docker/overlay2/
通过我们刚才打印的golang:1.16.6第二层的cache-id,可以在这里找到golang:1.16.6第二层的文件:
$ cd /var/lib/docker/overlay2/40a11a2b4f594bcccbe2cbb6cd3e6fa4a3c2f8427817f43bbade4331173611d2
$ tree
.
├── committed
├── diff
├── link
├── lower
└── work
diff文件夹中就是这一层相对于下层修改的文件。
5.7 manifest文件
manifest里面包含的内容就是对config和layer的sha256 + media type描述,目的就是为了下载config和layer,等image下载完成后,manifest的使命就完成了,里面的信息对于image的本地管理来说没什么用,因此本地并没与单独对manifest进行存储。
References
https://cloud.tencent.com/developer/article/1769020
https://yeasy.gitbook.io/docker_practice/image/list
https://www.jianshu.com/p/3ba255463047
https://staight.github.io/2019/10/04/%E5%AE%B9%E5%99%A8%E5%AE%9E%E7%8E%B0-overlay2/
https://www.jianshu.com/p/3826859a6d6e
https://time.geekbang.org/column/article/318173
https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/
https://vividcode.cc/oci-image-spec-introduction/
https://segmentfault.com/a/1190000009309276
https://github.com/opencontainers/image-spec