大家好,本篇文章和大家聊下docker相关的话题~~
工作中经常有关于docker镜像的问题,让人百思不解
- docker镜像加载到系统中到哪里去了?docker load 加载镜像的流程是怎样的?
- 为什么容器修改内容后,删除容器后再次开启容器内容消失了?
- docker images查看的镜像大小与docker save后的大小不一致?
- 通过docker build或docker pull后的镜像层的层级关系怎么查看?
- docker save导出的镜像后如何查看到镜像内容?
- docker镜像层内容是否可以修改?
根据上述问题,本篇将docker镜像深入解析(实践+理论)。内容较长,大家先关注收藏呀~~
本次试验环境版本信息:
- CPU:Intel
- 系统:Centos 8
- Docker Server: 23.0.1
- Docker Client: 20.10.17
- Storage Driver: overlay2
本节内容
- 镜像组成
- 镜像层内容
- 镜像文件结构
- 容器文件系统
- 案例:修改文件系统镜像层
- 案例:替换镜像文件层内容
镜像组成
镜像结构
(该图引用自网络)
Docker镜像是由文件系统叠加而成。最低层是一个引导文件系统,即bootfs,这很像典型的Linux/Unix的引导文件系统。Docker用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动后,它将会被移动到内存中,而引导文件系统则会被卸载(umount),以留出更多的内存供initrd磁盘镜像使用。
Docker镜像的第二层(由下而上数)是root文件系统rootfs也就是我们称为的base image基础镜像,它位于引导文件系统上。rootfs可以是一种或多种操作系统(如Debian或者Ubuntu文件系统)。
在传统的Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整检查后,它才会被切换为读写模式。 但是在Docker里,root文件系统永远只能是只读状态,并且Docker利用联合加载(overlay mount)技术又会在root文件系统层上加载更多的只读文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。
**Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像(parent image),可以依次类推直到镜像栈的最底部,最底部的镜像称为基础镜像 (base image)。**最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统。Docker中运行的程序就是在这个读写层中执行的。
镜像层说明
Docker镜像是由镜像层文件和镜像 json 文件组成,不论静态内容还是动态信息,Docker 均为将其在 json 文件中更新。
镜像层文件,可以查看Dockerfile为例每一行命令则代表一层镜像内容。
(该图引用自https://docs.docker.com/build/guide/images/layers.png)
Docker 每一层镜像的 json 文件,都扮演着一个非常重要的角色。
主要的作用如下:
- 记录 Docker 镜像中与容器动态信息相关的内容。
- 记录父子 Docker 镜像之间真实的差异关系。
- 弥补 Docker 镜像内容的完整性与动态内容的缺失Docker。
Docker 镜像的 json 文件可以认为是镜像的元数据信息,其重要性不言而喻。
镜像层内容
docker默认存储目录/var/lib/docker
[root@k8s-host docker]# tree /var/lib/docker -L 1
/var/lib/docker
├── buildkit
├── containers
├── engine-id
├── image # 镜像层级关系
├── network
├── overlay2 # 镜像实际数据
├── plugins
├── runtimes
├── swarm
├── tmp
├── trust
└── volumes
其中 image
目录主要记录镜像层级关系,overlay2
目录存储镜像实际数据。
内容寻址机制
首先认识下镜像层ID
每一层镜像数据对应着三项ID
- DiffID 是制作镜像时针对每层产生的hash值,可以通过命令
docker image inspect
查看字段中的RootFS.Layers
拿到 DiffID 哈希值 (默认排序:第一行则是最底层,由底层往上排序)。 - ChainID 是通过计算公式得出的ID,作用是与CacheID对应的层做内容寻址的索引,进而关联到每一个镜像层的镜像文件。对应的目录是
/var/lib/docker/image/overlay2/layerdb/sha256/$(ChainID计算公式后的命名目录)
。 - CacheID 作用是镜像层存储位置,对应的目录
/var/lib/docker/overlay2/$(CacheID命名目录)
,可以通过内容寻址拿到对应的层值。 该ID是根据镜像层中数据使用加密哈希算法生成UUID。
计算公式
公式1:第一层镜像层ChainID = 本层DiffID
公式2:除第一层外,其他层按照公式2来计算ChainID = sha256sum(上一层ChainID + 空格 + 本层DiffID)
(值采用sha256加密)
命令如下
$ echo -n "sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230 sha256:f6807e1a58ab4d83200064e3653c3cfd446c2a31dc3a0cbf4c9657aeb844cccd" | sha256sum -
内容寻址流程
- 通过DiffID值利用计算公式得到ChainID,在ChainID目录文件找中得到CacheID目录则是最终镜像层的目录。
- 查看镜像的镜像层顺序依据,来源于
docker image inspect 镜像ID
字段中的RootFS.Layers
DiffID列表。(也就是我们制作镜像时对每层数据生成的hash值) - 寻址关系为:DiffID > ChainID > CacheID
下面举例来演示,寻找镜像层内容寻址流程:
以 alpine:test2
镜像为例
首先,查找该镜像的 DiffID 层
该镜像一共分为三层镜像层数据。
第一层镜像层,根据公式1中定义,本层 DiffID 则为 ChainID。
下面开始拼接ChainID目录,在ChainID目录中可以拿到CacheID。
ChainID目录拼接:/var/lib/docker/image/overlay2/layerdb/sha256/ + ChainID值
[root@k8s-host docker]# ls /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230
cache-id diff size tar-split.json.gz
可以看到ChainID目录中,已经查到了 cache-id 的文件
[root@k8s-host docker]# cat /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/cache-id && echo
5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af
拿到CacheID后,进行拼接CacheID目录就可以查看到第一层的镜像层数据。
CacheID目录拼接:/var/lib/docker/overlay2/ + CacheID值 + /diff/
[root@k8s-host docker]# ls /var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff/
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
到此为止,第一层镜像层内容寻址结束。
第二层镜像层,根据公式2中定义,ChainID 等于 sha256sum(上一层ChainID + 空格 + 本层DiffID)
通过内容寻址第一层镜像层,我们已经知道上一层的ChainID值,下面通过计算得出第二层的ChainID值。
第二层的DiffID为:sha256:7d02cdab9bc74fbcfca8c9be9872527557431cfe6ee05dd242050a9baea6e6b9
[root@k8s-host docker]# echo -n "sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230 sha256:7d02cdab9bc74fbcfca8c9be9872527557431cfe6ee05dd242050a9baea6e6b9" | sha256sum
4b76dffd2e327a97a54138646d95a29cb9f364fc8d87d323e68279831a9249ab -
第二层ChainID值:4b76dffd2e327a97a54138646d95a29cb9f364fc8d87d323e68279831a9249ab
拿到ChainID值后,进行拼接ChainID目录:/var/lib/docker/image/overlay2/layerdb/sha256/ + ChainID值
**,**最后找到cache-id文件,进行拼接CacheID目录:/var/lib/docker/overlay2/ + CacheID值 + /diff/
[root@k8s-host docker]# cat /var/lib/docker/image/overlay2/layerdb/sha256/4b76dffd2e327a97a54138646d95a29cb9f364fc8d87d323e68279831a9249ab/cache-id && echo
icen45ia1w23bcq3deye0n8bf
[root@k8s-host docker]# tree /var/lib/docker/overlay2/icen45ia1w23bcq3deye0n8bf/diff/
/var/lib/docker/overlay2/icen45ia1w23bcq3deye0n8bf/diff/
└── root
└── test.sh
1 directory, 1 file
到此为止,第二层镜像层内容寻址结束。
第三层镜像层,根据公式2中定义,ChainID 等于 sha256sum(上一层ChainID + 空格 + 本层DiffID)
寻址方式还是和第二层寻址相同,我们继续操作~
寻址参考上述步骤,下面我直接将具体操作罗列
[root@k8s-host docker]# echo -n "sha256:4b76dffd2e327a97a54138646d95a29cb9f364fc8d87d323e68279831a9249ab sha256:535c535e0e2bf467f64c9f42210982a0f0a69eca171aeaaa2297beac7a449a95" | sha256sum
eaeaa2e5b3a2c635d6f120b56c11bac690cf877846f0731b7892ad332e3c0ab6 -
[root@k8s-host docker]# cat /var/lib/docker/image/overlay2/layerdb/sha256/eaeaa2e5b3a2c635d6f120b56c11bac690cf877846f0731b7892ad332e3c0ab6/cache-id && echo
a493w6d8xuz1sucb2l1ahega6
[root@k8s-host docker]# tree /var/lib/docker/overlay2/a493w6d8xuz1sucb2l1ahega6/diff/
/var/lib/docker/overlay2/a493w6d8xuz1sucb2l1ahega6/diff/
└── root
└── test2.sh
1 directory, 1 file
到此为止,第三层镜像层内容寻址结束。
镜像层级关系
镜像层级关系目录:/var/lib/docker/image/overlay2
[root@k8s-host overlay2]# tree /var/lib/docker/image/overlay2 -L 1
.
├── distribution
├── imagedb
├── layerdb
└── repositories.json
distribution目录记录包含了Layer层DiffID和digest之间的对应关系,digest的产生是本地构建完之后推送至远程仓库所生成。
imagedb目录记录元数据信息。
layerdb目录记录层级关系。
repositories.json文件记录镜像、digest信息。
distribution目录
该目录主要记录,DiffID和digest之间的对应关系**,**Digest是镜像内容的哈希值,可以保证在推送和拉取镜像时,内容不被篡改。/var/lib/docker/image/overlay2/distribution/
目录结构如下
[root@k8s-host distribution]# tree /var/lib/docker/image/overlay2/distribution/ -L 3
/var/lib/docker/image/overlay2/distribution/
├── diffid-by-digest
│ └── sha256
│ └── 7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7de
└── v2metadata-by-diffid
└── sha256
└── 4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230
举例,以alpine:latest
镜像为例,获取DiffID
[root@k8s-host ~]# docker inspect alpine:latest | grep RootFS -A 5
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"
]
},
在distribution
目录下,使用DiffID
可以查看到对应的digest
[root@k8s-host distribution]# cat v2metadata-by-diffid/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230 | jq
[
{
"Digest": "sha256:7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7de",
"SourceRepository": "docker.io/library/alpine",
"HMAC": ""
}
]
下面通过拿到的 digest 信息,在diffid-by-digest
目录可以查看到DiffID
信息
[root@k8s-host distribution]# cat diffid-by-digest/sha256/7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7de && echo
sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230
主要注意的是:查看镜像本身是否有digests信息,可以如下命令,看下DIGEST字段是否有信息。 若是<none>
则代表没有经过公共仓库发布的镜像,则不适用这种DiffID查找digests信息方法。
$ docker images --digests | grep alpine
imagedb目录
该目录主要记录,镜像的元数据。
我们通过 docker pull
下载了镜像后,docker会在宿主机上基于现有镜像层文件包和 docker pull image 数据构建本地的 layer 元数据,包括diff、parent、size等。
当docker将在宿主机上产生的新镜像层上传registry时,layer 元数据不会与镜像层一块打包上传。
元数据目录位置 /var/lib/docker/image/overlay2/imagedb
,元数据内容为JSON格式。
[root@k8s-host imagedb]# tree . -L 2
.
├── content
│ └── sha256
└── metadata
└── sha256
content 目录记录镜像元数据:/var/lib/docker/image/overlay2/imagedb/content/sha256/
metadata 目录记录元数据最后更新时间: /var/lib/docker/image/overlay2/imagedb/metadata/sha256
列如:要查找 alpine:latest
镜像的元数据,可通过下面方式
[root@k8s-host ~]# docker image inspect alpine:latest -f '{
{ .Id }}' | cut -d : -f2
7e01a0d0a1dcd9e539f8e9bbd80106d59efbdf97293b3d38f5d7a34501526cdb
[root@k8s-host ~]# cat /var/lib/docker/image/overlay2/imagedb/content/sha256/7e01a0d0a1dcd9e539f8e9bbd80106d59efbdf97293b3d38f5d7a34501526cdb
{
"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":["/bin/sh"],"Image":"sha256:39dfd593e04b939e16d3a426af525cad29b8fc7410b06f4dbad8528b45e1e5a9","Volumes":null,"WorkingDir":"","En