docker镜像深入理解

发布于:2024-06-07 ⋅ 阅读:(55) ⋅ 点赞:(0)

大家好,本篇文章和大家聊下docker相关的话题~~

工作中经常有关于docker镜像的问题,让人百思不解

  1. docker镜像加载到系统中到哪里去了?docker load 加载镜像的流程是怎样的?
  2. 为什么容器修改内容后,删除容器后再次开启容器内容消失了?
  3. docker images查看的镜像大小与docker save后的大小不一致?
  4. 通过docker build或docker pull后的镜像层的层级关系怎么查看?
  5. docker save导出的镜像后如何查看到镜像内容?
  6. 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 文件,都扮演着一个非常重要的角色。

主要的作用如下:

  1. 记录 Docker 镜像中与容器动态信息相关的内容。
  2. 记录父子 Docker 镜像之间真实的差异关系。
  3. 弥补 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 -

内容寻址流程

  1. 通过DiffID值利用计算公式得到ChainID,在ChainID目录文件找中得到CacheID目录则是最终镜像层的目录。
  2. 查看镜像的镜像层顺序依据,来源于docker image inspect 镜像ID 字段中的 RootFS.Layers DiffID列表。(也就是我们制作镜像时对每层数据生成的hash值)
  3. 寻址关系为: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包。
前提准备:需要通过githttps://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 911 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 112 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 87 09:09 bin
drwxr-xr-x.  1 root root   43 1026 03:59 dev
drwxr-xr-x.  1 root root   66 1026 03:59 etc
drwxr-xr-x.  2 root root    6 87 09:09 home
drwxr-xr-x.  7 root root  243 87 09:09 lib
drwxr-xr-x.  5 root root   44 87 09:09 media
drwxr-xr-x.  2 root root    6 87 09:09 mnt
drwxr-xr-x.  2 root root    6 87 09:09 opt
dr-xr-xr-x.  2 root root    6 87 09:09 proc
drwx------.  1 root root   26 1030 02:14 root
drwxr-xr-x.  2 root root    6 87 09:09 run
drwxr-xr-x.  2 root root 4096 87 09:09 sbin
drwxr-xr-x.  2 root root    6 87 09:09 srv
drwxr-xr-x.  2 root root    6 87 09:09 sys
drwxrwxrwt.  2 root root    6 87 09:09 tmp
drwxr-xr-x.  7 root root   66 87 09:09 usr
drwxr-xr-x. 12 root root  137 87 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 1114 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 1114 09:18 5a3ea318409c16ca7e6acfb02e9f55c41e78de54dae6e72a28fbf0e7f77d081a
drwxr-xr-x. 2 root root   50 1114 09:18 7f5bb53fbdad6e37035d2c5e82535e3de5bb5e1d6cd5c6dca155d88bc0c8f301
drwxr-xr-x. 2 root root   50 1114 09:18 a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480ced
-rw-r--r--. 1 root root 1294 1114 09:18 c90a9e6123f29eea53b1cf62ae7dc4f45496467bb212ed6a5901461b840236f1.json
-rw-r--r--. 1 root root  354 1231 1969 manifest.json
-rw-r--r--. 1 root root   87 1231 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"
  }
}

其中 a140f3eecb02a856ce594c0d155a13f420faf7e45798da313ee98a9821480cedmanifest.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 文件系统(下层)。 当两个文件系统中都存时,“上层”文件系统中的对象可见,而“下层”文件系统中的对象则隐藏,或者在目录的情况下与“上层”对象合并。
上层文件系统通常是可写的,下层文件系统则为只读。
当上层和下层对象都是目录时,就形成一个合并目录。在挂载时,作为挂载选项lowerdirupperdir给出的两个目录将组合成一个合并目录:

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内容寻址机制详细展示了镜像层与镜像关联的关系。
功能

  1. 根据镜像名称:TAG, 显示实际镜像层内容
  2. 根据镜像名称:TAG, 找出镜像层信息, 包含镜像每层的位置 (docker history基础下扩展信息)
  3. 根据镜像层id, 找出所关联的镜像
  4. 根据指定文件, 找出对应镜像层信息、所关联的镜像
  5. 根据none标记的镜像, 显示当时层的镜像名称:TAG

大家可以根据上面的内容和 docker-image 工具,结合一下理解~~

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】

参考材料:


网站公告

今日签到

点亮在社区的每一天
去签到