大家好,本篇文章和大家聊下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":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"ba09fe2c8f99faad95871d467a22c96f4bc8166bd01ce0a7c28dd5472697bfd1","container_config":{"Hostname":"ba09fe2c8f99","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 [\"/bin/sh\"]"],"Image":"sha256:39dfd593e04b939e16d3a426af525cad29b8fc7410b06f4dbad8528b45e1e5a9","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2023-08-07T19:20:20.894140623Z","docker_version":"20.10.23","history":[{"created":"2023-08-07T19:20:20.71894984Z","created_by":"/bin/sh -c #(nop) ADD file:32ff5e7a78b890996ee4681cc0a26185d3e9acdb4eb1e2aaccb2411f922fed6b in / "},{"created":"2023-08-07T19:20:20.894140623Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"]}}
需要注意,元数据中时间为 {0001-01-01 00:00:00 +0000 UTC}
内容,则没有记录最后更新时间。 对应的metadata目录中没有该值的记录。
[root@k8s-host ~]# docker image inspect alpine:latest -f '{{ .Metadata }}'
{0001-01-01 00:00:00 +0000 UTC}
layerdb目录
层级关系目录 /var/lib/docker/image/overlay2/layerdb
主要包含
[root@k8s-host layerdb]# tree . -L 1
.
├── mounts
├── sha256
└── tmp
sha256目录,记录镜像层级关系。
mounts目录,容器启动后写入的文件,挂载到系统实际位置的层级关系。
sha256目录
可以理解为寻找镜像实际文件的中间层,不存放实际的镜像信息,只记录寻找关系。
举例:/var/lib/docker/image/overlay2/layerdb/sha256/003dc4cac94f6a8d49f6715277edaa0903ed6b1a181a61c86466c7e45d5d78ec
下存放层的信息如下:
[root@k8s-host 003dc4cac94f6a8d49f6715277edaa0903ed6b1a181a61c86466c7e45d5d78ec]# ls
cache-id diff parent size tar-split.json.gz
- cache-id: 由宿主机随机生成的一个uuid,根镜像层文件一一对应,用于宿主机标志和索引镜像层文件
- diff: 镜像层校验ID、根据该镜像层的打包文件校验获得
- parent: 父镜像层的chainID(最底层不含该文件)
- size: 大小
- tar-split.json.gz: 记录元数据,可以还原层的tar文件
这里注重介绍一下 tar-split.json.gz 文件, 其他信息可以和上文中内容寻址机制部分内容参考理解。
下面以 tar-split 工具使用来作为例子介绍:
工具的作用:验证层级中的/var/lib/docker/image/overlay2/layerdb/sha256/$(diff_id)/tar-split.json.gz
元数据文件与/var/lib/docker/overlay2/$(cache_id)/diff/
实际文件内容的校验,并可以根据层内容还原回tar包。
前提准备:需要通过git
把https://github.com/vbatts/tar-split
仓库拉到本地。
代码结构
.
├── archive
│ └── tar
├── cmd
│ └── tar-split
├── concept
│ ├── DESIGN.md
│ └── main.go
├── LICENSE
├── mage_color.go
├── magefile.go
├── mage.go
├── README.md
├── tar
│ ├── asm
│ └── storage
在 cmd/tar-split 目录下进行编译
go build -o tar-split asm.go checksize.go disasm.go main.go
举例,通过tar-split
工具还原alpine:latest
镜像中的最底层 tar文件
找出底层 (从上向下,第一层则为最底层)
[root@k8s-host tmp]# docker image inspect alpine:latest | grep RootFS -A 4
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"
]
因为第一层是 chain_id
则直接使用就行, 路径需要拼接起来 /var/lib/docker/image/overlay2/layerdb/sha256/
+ chain_id
+tar-split.json.gz
[root@k8s-host tmp]# ls -l /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/tar-split.json.gz
-rw-r--r--. 1 root root 20917 9月 11 10:28 /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/tar-split.json.gz
最底层实际文件目录,则通过cache-id
就可以拿到, 最后也需要拼接起来 /var/lib/docker/overlay2/
+ cache-id
+ /diff/
[root@k8s-host tmp]# cat /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/cache-id && echo
5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af
[root@k8s-host tmp]# ls /var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff/
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
最后执行tar-split
命令将最底层tar文件还原
[root@k8s-host tmp]# ./tar-split asm --output new1.tar --input /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/tar-split.json.gz --path /var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff
[root@k8s-host tmp]# ls -l new1.tar
-rw-r--r--. 1 root root 7625728 11月 2 01:40 new1.tar
[root@k8s-host tmp]# sha256sum new1.tar
4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230 new1.tar
[root@k8s-host tmp]# mkdir test && tar xf new1.tar -C test/ && ls test
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
new1.tar 是输出后的底层tar文件, 使用sha256sum
计算后new1.tar
hash也是和层hash是一致的。 在使用tar命令解压后也是和实际的层内容一致。
比如我把最底层释放的文件系统内容修改,再次用tar-split工具根据修改的层对应这tar-split.json.gz 元数据记录,是否能还原回去吗?
[root@k8s-host tmp]# echo "test" >> test/etc/alpine-release
[root@k8s-host tmp]# cat test/etc/alpine-release
3.18.3
test
[root@k8s-host tmp]# ./tar-split asm --output new1.tar --input /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/tar-split.json.gz --path test/
FATA[0000] file integrity checksum failed for "etc/alpine-release"
注意:会发现 etc/alpine-release
文件校验失败,代表docker镜像层被篡改。
mounts目录
举例,启动alpine:latest
容器
[root@k8s-host ~]# docker run -d --name alpine alpine:latest sh -c "tail -f /dev/null"
129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad
容器启动后生成的容器id目录/var/lib/docker/image/overlay2/layerdb/mounts/129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad
,该目录下信息如下
[root@k8s-host 129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad]# ls
init-id mount-id parent
init-id
:是在mount-id后加了-init
,夹在只读层和读写层之间,Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf
等信息。对应目录/var/lib/docker/overlay2/
mount-id
: 读写层,镜像层的cache-id,对应目录/var/lib/docker/overlay2/
parent
: 父镜像层id
查看mount-id,找到实际读写层位置
[root@k8s-host 129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad]# cat mount-id
d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47
实际可以在宿主机mount挂载中查看到容器的读写文件系统已经挂载成功
[root@k8s-host 129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad]# mount | grep d71bdc89d5
overlay on /var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47/merged type overlay (rw,relatime,seclabel,lowerdir=/var/lib/docker/overlay2/l/WX3EUQ3ZIGQWVDGKNGWAULAMSX:/var/lib/docker/overlay2/l/N772WKXWL45Y3QGDX4QQZ5EWQD,upperdir=/var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47/diff,workdir=/var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47/work)
[root@k8s-host 129ab93a27b7b44e8946976627556582efc325bf27f7777923d1497f35a600ad]# ls /var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47
diff link lower merged work
[root@k8s-host tmp]# ls -l /var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47/merged/
总用量 8
drwxr-xr-x. 2 root root 4096 8月 7 09:09 bin
drwxr-xr-x. 1 root root 43 10月 26 03:59 dev
drwxr-xr-x. 1 root root 66 10月 26 03:59 etc
drwxr-xr-x. 2 root root 6 8月 7 09:09 home
drwxr-xr-x. 7 root root 243 8月 7 09:09 lib
drwxr-xr-x. 5 root root 44 8月 7 09:09 media
drwxr-xr-x. 2 root root 6 8月 7 09:09 mnt
drwxr-xr-x. 2 root root 6 8月 7 09:09 opt
dr-xr-xr-x. 2 root root 6 8月 7 09:09 proc
drwx------. 1 root root 26 10月 30 02:14 root
drwxr-xr-x. 2 root root 6 8月 7 09:09 run
drwxr-xr-x. 2 root root 4096 8月 7 09:09 sbin
drwxr-xr-x. 2 root root 6 8月 7 09:09 srv
drwxr-xr-x. 2 root root 6 8月 7 09:09 sys
drwxrwxrwt. 2 root root 6 8月 7 09:09 tmp
drwxr-xr-x. 7 root root 66 8月 7 09:09 usr
drwxr-xr-x. 12 root root 137 8月 7 09:09 var
merged目录为合并层后的目录,该层也是读写层。
当容器意外运行中断时,mount所对应的挂载还存在吗?
[root@k8s-host ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
129ab93a27b7 alpine:latest "sh -c 'tail -f /dev…" 10 days ago Exited (137) 13 seconds ago alpine
对应的/var/lib/docker/overlay2/$(mount-id)
目录还是存在的,目录内的联合挂载(merge目录)已经删除,如果之前在容器新增或修改文件则会存在diff目录中
[root@k8s-host mounts]# tree /var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47 -L 3
/var/lib/docker/overlay2/d71bdc89d5d1cd86f3344ed7a1a7f1afa24075be74d33a90b1a5b82a03d0eb47
├── diff
│ ├── etc
│ │ └── etc.txt
│ └── root
│ └── root.txt
├── link
├── lower
└── work
└── work
[root@k8s-host mounts]# mount | grep d71bdc89d5
[root@k8s-host mounts]#
mount实际挂载点也被取消。
镜像Cache层数据
存放位置 /var/lib/docker/overlay2/镜像层hash命名目录(CacheID值)
,镜像层实际存放到文件系统上的数据。
可以通过docker image inspect
命令查看镜像层hash值
$ docker image inspect test4/alpine:v1.0
...
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/xgn33v0jnaz5n1vdcec4uybxn/diff:/var/lib/docker/overlay2/pw396v9iiqj4uqgs2i4ahbc07/diff:/var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff",
"MergedDir": "/var/lib/docker/overlay2/ikq4ii208tyb0xzswnm03y4mf/merged",
"UpperDir": "/var/lib/docker/overlay2/ikq4ii208tyb0xzswnm03y4mf/diff",
"WorkDir": "/var/lib/docker/overlay2/ikq4ii208tyb0xzswnm03y4mf/work"
},
"Name": "overlay2"
},
...
查看alpine:letest
的最底层
[root@k8s-host imagedb]# ls /var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af
committed diff link
一般每层目录中会包含 committed、 link、lower、diff、work文件或目录。
- committed 空文件
- link 代表本层id软连接
- lower 代表下一层id软连接(如果目录下没有lower文件,则表示该层是最底层)
- diff 目录代表,实际该层的文件内容
- work 目录代表,联合文件系统后的中间过程目录
RootFS数据
rootfs是根文件系统,一般制作系统时会用到。rootfs只是一个操作系统所包含的文件、配置和目录,不包含操作系统的内核。
在docker镜像中,一些基础镜像层对应的RootFS只有一层, 这是制作者将最小化系统以rootfs格式制作成了一个tar包。
列如 alpine:latest
镜像
[root@k8s-host mounts]# docker image inspect alpine:latest | grep RootFS -A 4
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"
]
sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230
层被解开是一个根文件系统目录,系统启动所依赖的文件都包含了。
实际层对应关系:需要根据 diff_id
来找到该层的 chain_id
,在根据 chain_id
找到 cache_id
。
注意:当前alpine:latest
镜像中只有一层,上文中已经介绍到当镜像一层时 chain_id
则就是 diff_id
。
[root@k8s-host ~]# cat /var/lib/docker/image/overlay2/layerdb/sha256/4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230/cache-id && echo
5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af
拿到cache_id后,拼接/var/lib/docker/overlay2/$(cache_id)/diff
目录就能看到rootfs文件系统了
[root@k8s-host ~]# tree /var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff -L 1/var/lib/docker/overlay2/5c203c0adfa1c33600242df609195ceb17f8b8918a783920efe0fffbcb54b0af/diff
├── bin
├── dev
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var
镜像文件结构
在实际工作中有很多场景是需要通过离线方式进行docker镜像升级,大多数操作需要手动的将docker build
后的镜像通过docker save
命令保存后,然后在将镜像拷贝到指定的设备上升级验证。
根据这种情况我们来看一下docker save
保存后的docker镜像是一个什么样的结构。
首先,先基于alpine:latest
镜像在增加两层数据,build完成后alpine镜像一共为三层。
[root@k8s-host docker]# echo 1 > data1.txt
[root@k8s-host docker]# echo 2 > data2.txt
[root@k8s-host docker]# cat > Dockerfile << EOF
> FROM alpine:latest
> ADD data1.txt /root
> ADD data2.txt /root
> EOF
[root@k8s-host docker]# docker build -t alpine:test .
[+] Building 0.1s (8/8) FINISHED
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 155B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 176B 0.0s
=> [1/3] FROM docker.io/library/alpine:latest 0.0s
=> CACHED [2/3] ADD data1.txt /root 0.0s
=> CACHED [3/3] ADD data2.txt /root 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1 0.0s
=> => naming to docker.io/library/alpine:test 0.0s
[root@k8s-host docker]# docker images | grep c90a9e6123
alpine test c90a9e6123f2 2 minutes ago 7.34MB
[root@k8s-host docker]# docker save alpine:test | gzip > alpine-test.docker
[root@k8s-host docker]# ls -l alpine-test.docker
-rw-r--r--. 1 root root 3295780 11月 14 10:02 alpine-test.docker
实际上 docker save
出来的docker镜像是一个 tar 压缩的文件,这里使用gzip压缩导出。
[root@k8s-host docker]# file alpine-test.docker
alpine-test.docker: gzip compressed data, last modified: Tue Nov 14 15:02:38 2023, from Unix, original size 7645696
使用tar xf
直接解压缩即可
[root@k8s-host docker]# mkdir alpine-test
[root@k8s-host docker]# tar xf alpine-test.docker -C alpine-test
[root@k8s-host docker]# ls -l alpine-test
总用量 12
drwxr-xr-x. 2 root root 50 11月 14 09:18 5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a
drwxr-xr-x. 2 root root 50 11月 14 09:18 7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301
drwxr-xr-x. 2 root root 50 11月 14 09:18 a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced
-rw-r--r--. 1 root root 1294 11月 14 09:18 c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json
-rw-r--r--. 1 root root 354 12月 31 1969 manifest.json
-rw-r--r--. 1 root root 87 12月 31 1969 repositories
注:docker镜像的规范格式是按照 OCI规范(Open container Initiative) 来定义的,详细可以查看https://github.com/opencontainers/image-spec
OCI规范的定义,主要包含镜像清单、镜像索引、一组文件系统层和配置组成。
(该图引用自https://github.com/opencontainers/image-spec/blob/main/img/build-diagram.png)
下面就基于alpine:test
镜像解开后的目录结构,详细分析下镜像目录结构的定义。
[root@k8s-host docker]# tree alpine-test -L 2
alpine-test
├── 5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json
├── manifest.json
└── repositories
镜像目录中:主要分为 repositories
(镜像名称)、manifest.json
(镜像清单)、c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json
(配置文件) 文件 和 3个层级目录。 repositories
是一个json格式文件,其主要记录内容是仓库镜像名称
、Tag版本信息
、最后一层的hash
。
[root@k8s-host docker]# cat alpine-test/repositories | jq
{
"alpine": {
"test": "a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced"
}
}
其中 a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced
为 manifest.json
清单中最后一层的hash值。
manifest.json
镜像清单,主要内容是配置文件名称、层级目录文件路径。
[root@k8s-host docker]# cat alpine-test/manifest.json | jq
[
{
"Config": "c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json",
"RepoTags": [
"alpine:test"
],
"Layers": [
"7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301/layer.tar",
"5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a/layer.tar",
"a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced/layer.tar"
]
}
]
注:这里的层级目录只是镜像文件中的层级信息,不是加载到文件系统中的层级关系。
manifest中字段含义:
- Config: 内容为配置文件名称, 文件名是生成的镜像唯一ID作为镜像ID内容寻址用意,内容为JSON格式。
- RepoTags: 仓库镜像名称+镜像Tag。
- Layers: 镜像目录层路径,默认层机制是将文件内容tar压缩。按顺序由上至下,第一层则为最底层。
c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json
文件,主要包含配置和rootfs镜像层级关系。其中文件名称 c90a9e6123f.....
则是实际的镜像ID。
[root@k8s-host docker]# cat alpine-test/c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json | jq
{
"architecture": "amd64",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
],
"OnBuild": null
},
"created": "2023-11-14T09:18:46.133049408-05:00",
"history": [
{
"created": "2023-08-07T19:20:20.71894984Z",
"created_by": "/bin/sh -c #(nop) ADD file:32ff5e7a78b890996ee4681cc0a26185d3e9acdb4eb1e2aaccb2411f922fed6b in / "
},
{
"created": "2023-08-07T19:20:20.894140623Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
},
{
"created": "2023-11-14T09:18:46.107020416-05:00",
"created_by": "ADD data1.txt /root # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2023-11-14T09:18:46.133049408-05:00",
"created_by": "ADD data2.txt /root # buildkit",
"comment": "buildkit.dockerfile.v0"
}
],
"moby.buildkit.buildinfo.v1": "eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lOmxhdGVzdCIsInBpbiI6InNoYTI1Njo0NjkzMDU3Y2UyMzY0NzIwZDM5ZTU3ZTg1YTViOGUwYmQ5YWMzNTczNzE2MjM3NzM2ZDY0NzBlYzViN2I3MjMwIn1dfQ==",
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230",
"sha256:9fe1625435f76488c4b2c3d23544319d013500f890887252ea8df15acd089711",
"sha256:83d322e372d6def67b53cd085aacbfbf14db4b6e9ba2e030046c625033d10162"
]
}
}
重点说一下以下字段
- history: 每个
{}
结构则是一层。如果包含"empty_layer": true
字段,该层则是空的仅包含执行的命令,不属于一层。 - rootfs: 镜像层,当镜像加载到系统目录中,
diff_ids
则表示层级关系的寻址依据。 - moby.buildkit.buildinfo.v1:这是构建镜像时buildkit产生的构建信息,值类型是base64。
这里编码可以用base64
命令解开
[root@k8s-host docker]# echo eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lOmxhdGVzdCIsInBpbiI6InNoYTI1Njo0NjkzMDU3Y2UyMzY0NzIwZDM5ZTU3ZTg1YTViOGUwYmQ5YWMzNTczNzE2MjM3NzM2ZDY0NzBlYzViN2I3MjMwIn1dfQ== | base64 -Dd
{"frontend":"dockerfile.v0","sources":[{"type":"docker-image","ref":"docker.io/library/alpine:latest","pin":"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"}]}
这里层级目录,是镜像中的层级不是加载到文件中的,可以参考上文中manifest.json镜像清单层级顺序
├── 5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced
│ ├── json
│ ├── layer.tar
│ └── VERSION
manifest.json层级顺序
"Layers": [
"7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301/layer.tar",
"5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a/layer.tar",
"a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced/layer.tar"
]
首先查看第一层,7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301
目录中,则对应着 alpine:latest
镜像,因为基础镜像只有一层。
目录中包含的文件介绍如下:
- json: 记录容器运行态配置,其中
"parent"
字段是记录父层级目录的hash (如果是第一层则没有parent字段) - layer.tar:实际的镜像层内容,tar格式压缩
- VERSION: 版本
其他层目录内容也是类似。
容器文件系统
运行中的容器是如何加载镜像的呢? 在容器中删除或添加了文件是否影响镜像呢? 这两个问题最核心的技术实现并不是容器,而是 overlay 文件系统。
(引用于https://linuxconfig.org/wp-content/uploads/2022/09/02-introduction-to-the-overlay-filesystem.avif)
overlay 有时称为联合文件系统。overlay直译过来是覆盖文件系统,该文件系统是将一个文件系统覆盖在另一个文件系统之上的结果。overlay2是在overlay基础上做了优化的迭代。
overlay文件系统分为两部分,upper 文件系统(上层) 和 lower 文件系统(下层)。 当两个文件系统中都存时,“上层”文件系统中的对象可见,而“下层”文件系统中的对象则隐藏,或者在目录的情况下与“上层”对象合并。
上层文件系统通常是可写的,下层文件系统则为只读。
当上层和下层对象都是目录时,就形成一个合并目录。在挂载时,作为挂载选项lowerdir
和upperdir
给出的两个目录将组合成一个合并目录:
mount -t overlay overlay -olowerdir=/lower,upperdir=/upper,workdir=/work /merged
workdir
必须是与 upperdir
位于同一文件系统上的空目录。
简要说下overlay文件系统的特性
whiteout机制: 为了在不更改下层文件系统的情况下支持 rm 和 rmdir,overlay文件系统在上层文件系统中记录文件已被删除。是通过whiteout机制实现,whiteout创建为具有 0/0 设备号的字符设备。当在合并目录的上层发现whiteout时,下层中任何匹配的名称都会被忽略,并且whiteout本身也会被隐藏。
缓存机制: 对合并目录将分别读取上层目录和下层目录,此合并的名称列表缓存在“结构文件”中,因此只要文件保持打开状态,该列表就会保留下来。如果目录同时被两个进程打开和读取,它们将各自拥有单独的缓存。
写时复制机制: 当以需要写访问的方式访问下层文件系统中的文件时,例如打开写访问、更改某些元数据等,该文件首先从下层文件系统复制到上层文件系统(copy_up)。
**元数据复制:**当启用仅复制元数据功能时,当执行 chown/chmod 等元数据特定操作时,overlayfs 将仅复制元数据(而不是整个文件)。当文件打开进行写操作时,完整的文件将被复制。
共享和复制镜像层:下层可以在多个overlay之间共享,不允许使用已被另一个overlay挂载使用的上层路径和/或 workdir 路径。
以上机制中overlay使用了很多文件扩展属性来配合这些机制的实现。
下面看一下overlay
是针对这些问题如何处理的。
首先将镜像 alpine:latest
启动后,容器加载镜像实际上是挂载了overlay2文件系统。通过mount
命令可以看到文件系统中多了一条overlay挂载点。
[root@k8s-host ~]# mount | grep overlay
overlay on /var/lib/docker/overlay2/5ecc73742be968cd2b37034697127b038c2db3065f957e1c5557daeace48be88/merged type overlay (rw,relatime,seclabel,lowerdir=/var/lib/docker/overlay2/l/O5U23EMPVWRI3WE3SBLLM3NARA:/var/lib/docker/overlay2/l/N772WKXWL45Y3QGDX4QQZ5EWQD,upperdir=/var/lib/docker/overlay2/5ecc73742be968cd2b37034697127b038c2db3065f957e1c5557daeace48be88/diff,workdir=/var/lib/docker/overlay2/5ecc73742be968cd2b37034697127b038c2db3065f957e1c5557daeace48be88/work)
继续分析上文中 mount 命令查到的overlay挂载
mount挂载信息显示的格式: 文件系统类型 on 挂载点(合并后的目录) type overlay (参数信息默认以逗号分隔)
括号中参数说明
rw
支持读写。relatime
更新索引节点访问的时间。seclabel
表示文件系统使用xattrs作为标签,并且支持通过设置xattrs来更改标签。lowerdir=
只读层,多个层则用冒号隔开,顺序第一个则最底层,以此类推。upperdir=
读写层,当在容器中新增文件后,则可以在该层看到。workdir=
overlay文件系统联合合并后的过程目录,默认情况下都会是空目录。
注:文件系统类型不同,参数也有所不同。
案例:修改文件系统镜像层
**目的:**镜像释放到文件系统后,是否可以替换镜像层文件?
以alpine:latest
为例,基于该镜像增加一层内容,Dockerfile如下
FROM alpine:latest
ADD test.sh /root
创建test.sh脚本
#!/bin/sh
echo "test.sh"
将此dockerfile进行build
docker build -t alpine:test-1.0 .
下面操作主要将 ADD test.sh /root
层内容在文件系统上更改,再次尝试启动容器后会不会是修改后的内容。
首先找到 alpine:test-1.0
镜像ADD test.sh
层中对应的 diff_id 哈希值。(查找步骤可参考上文中内容寻址)
[root@k8s-host ~]# docker inspect alpine:test-1.0 | grep RootFS -A 6
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230",
"sha256:1cffaa5e44107dfc1de7fb81ac51263e8f77a0bb2f1ff458a73f5f88effc840a"
]
},
alpine:test-1.0
镜像实际只有两层,第二层则是ADD test.sh
层。
通过内容寻址机制,找出对应层的镜像cache_id
哈希值 qlmrea8g4s3c6o6uvtflsvydy
。
找到 test.sh
脚本,将其内容更改。 (这一步则是此次实验目的,验证是否启动容器后是否是改动后的脚本)
[root@k8s-host ~]# tree /var/lib/docker/overlay2/qlmrea8g4s3c6o6uvtflsvydy/diff
/var/lib/docker/overlay2/qlmrea8g4s3c6o6uvtflsvydy/diff
└── root
└── test.sh
[root@k8s-host ~]# echo 'echo "add data"' >> /var/lib/docker/overlay2/qlmrea8g4s3c6o6uvtflsvydy/diff/root/test.sh
[root@k8s-host ~]# cat /var/lib/docker/overlay2/qlmrea8g4s3c6o6uvtflsvydy/diff/root/test.sh
#!/bin/sh
echo "test.sh"
echo "add data"
最后启动容器,执行test.sh脚本看下效果
[root@k8s-host ~]# docker run -it --rm alpine:test-1.0 /root/test.sh
test.sh
add data
可以看到启动容器后,立即执行了test.sh
脚本所输出的结果,已是刚刚修改镜像层的内容。
如果将修改后的镜像层保存导出tar镜像文件,是否成功?
[root@k8s-host save]# docker save alpine:test-1.0 | gzip > alpine_test-1.0.docker
Error response from daemon: file integrity checksum failed for "root/test.sh"
答案是不行的。 这块报错是因为tar-split工具效验失败,也可参考上文中 tar-split工具的使用有详细说明。
结论:当镜像加载到文件系统后,层文件内容没有得到操作系统的保护,层文件内容只是存放到docker规定的目录中。root用户可以对其内容进行更改删除等操作,如果层内容发生了更改,再次启动容器可以正常运行,因为容器启动镜像文件只是overlay文件系统联合合并原理并不会对文件内容进行检查;但将其docker save另存镜像文件时则会触发文件检查的机制,检测到文件效验值不符合则会失败。
案例:替换镜像文件层内容
**目的:**基于docker save 后的tar镜像文件,解压缩后是否可以将里面的文件层修改?
以上个试验的alpine:test-1.0
镜像为例,需要先将上实验中的 "root/test.sh"
脚本还原。
首先将alpine:test-1.0
镜像导出,默认是tar文件格式。
[root@k8s-host tar]# docker save alpine:test-1.0 > alpine_test-1.0.tar
因为docker服务有缓存机制,这里为了不影响下面的试验需要将docker服务重启以及删除alpine:test-1.0
镜像
[root@k8s-host tar]# systemctl restart docker
[root@k8s-host tar]# docker rmi alpine:test-1.0
下面在alpine_test-1.0.tar镜像中找到test.sh脚本层,进行更改内容。
[root@k8s-host tar]# tar xf alpine_test-1.0.tar
[root@k8s-host tar]# cat manifest.json | jq
[
{
"Config": "d7ea9291452d90a2d48743712ecb6dcad95fea4a22739e3e7015b8725f6e7feb.json",
"RepoTags": [
"alpine:test-1.0"
],
"Layers": [
"7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301/layer.tar",
"0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985/layer.tar"
]
}
]
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# tar vtf layer.tar
drwx------ root/root 0 2023-12-09 22:05 root/
-rwxr-xr-x root/root 41 2023-12-09 22:05 root/test.sh
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# tar xf layer.tar
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# echo 'echo "add data"' >> root/test.sh
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# cat root/test.sh
#!/bin/sh
echo "test.sh"
echo "add data
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# tar cf layer.tar root ; rm -rf root
计算更改后的 layer.tar 层文件的 sha256值
[root@k8s-host 0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985]# sha256sum layer.tar
f57375f094b22d8d3c6fa7834d313d536a798f5570302f7c5506f1d897d2e58e layer.tar
因test.sh
脚本sha256值发生了变化,需要将rootfs.diff_ids最后一层sha256值更新。
更改前
[root@k8s-host tar]# cat d7ea9291452d90a2d48743712ecb6dcad95fea4a22739e3e7015b8725f6e7feb.json | jq | grep rootfs -A 10
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230",
"sha256:1cffaa5e44107dfc1de7fb81ac51263e8f77a0bb2f1ff458a73f5f88effc840a"
]
}
}
更改后
[root@k8s-host tar]# sed -i 's/1cffaa5e44107dfc1de7fb81ac51263e8f77a0bb2f1ff458a73f5f88effc840a/f57375f094b22d8d3c6fa7834d313d536a798f5570302f7c5506f1d897d2e58e/g' d7ea9291452d90a2d48743712ecb6dcad95fea4a22739e3e7015b8725f6e7feb.json
[root@k8s-host tar]# cat d7ea9291452d90a2d48743712ecb6dcad95fea4a22739e3e7015b8725f6e7feb.json | jq | grep rootfs -A 10 "rootfs": {
"type": "layers",
"diff_ids": [
"sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230",
"sha256:f57375f094b22d8d3c6fa7834d313d536a798f5570302f7c5506f1d897d2e58e"
]
}
}
如果不进行更新,docker load 导入时会效验sha256失败。
[root@k8s-host tar]# docker load < alpine_test-1.0.tar
1cffaa5e4410: Loading layer [==================================================>] 10.24kB/10.24kB
invalid diffID for layer 1: expected "sha256:1cffaa5e44107dfc1de7fb81ac51263e8f77a0bb2f1ff458a73f5f88effc840a", got "sha256:f57375f094b22d8d3c6fa7834d313d536a798f5570302f7c5506f1d897d2e58e"
最后,将镜像文件打包,导入镜像文件。
[root@k8s-host tar]# ls
0e81a757f7e4aaeb63277890db4e75a5fdd234efae08a1c5193765cc5be09985 manifest.json
7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301 repositories
d7ea9291452d90a2d48743712ecb6dcad95fea4a22739e3e7015b8725f6e7feb.json
[root@k8s-host tar]# tar cf alpine_test-1.0.tar .
[root@k8s-host tar]# docker load < alpine_test-1.0.tar
f57375f094b2: Loading layer [==================================================>] 10.24kB/10.24kB
Loaded image: alpine:test-1.0
下面我们将验证 test.sh 脚本层是否是更改后的效果。
找到对应 test.sh 层的 cache_id: 9e69047376eadfacff8de7623f463e3b6fe3dcd0fa16f91a34ebdfa4a0bba0b0
, docker内容寻址方法(参考上文中内容寻址)。
[root@k8s-host tar]# cat /var/lib/docker/overlay2/683022a5d68360eca6710ca9c9623161f1e2d5b6cd457e2342d3cef51ba99002/diff/root/test.sh
#!/bin/sh
echo "test.sh"
echo "add data"
可以看到镜像层是更改后的内容,代表已经替换tar镜像成功。
最后启动容器,执行test.sh脚本看下效果
[root@k8s-host tar]# docker run -it --rm alpine:test-1.0 /root/test.sh
test.sh
add data
试验结论:通过修改tar镜像中的层,可以实现替换层成功。 如果当前有docker build编译的环境下还是要用Dockerfile更简单和规范一些。 如果本地环境中没有docker build环境,可以修改tar镜像层作为debug的一种方式。
小工具:找出镜像层所有的关联的镜像
仓库地址 https://github.com/hltfaith/docker-image
docker-image 工具主要功能实现了, 利用docker内容寻址机制详细展示了镜像层与镜像关联的关系。
功能
- 根据镜像名称:TAG, 显示实际镜像层内容
- 根据镜像名称:TAG, 找出镜像层信息, 包含镜像每层的位置 (docker history基础下扩展信息)
- 根据镜像层id, 找出所关联的镜像
- 根据指定文件, 找出对应镜像层信息、所关联的镜像
- 根据none标记的镜像, 显示当时层的镜像名称:TAG
大家可以根据上面的内容和 docker-image
工具,结合一下理解~~
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】
参考材料: