docker镜像深入理解

大家好,本篇文章和大家聊下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 工具,结合一下理解~~

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

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

参考材料:

  • https://github.com/opencontainers/image-spec/blob/main/spec.md
  • https://github.com/moby/moby/blob/v20.10.8/image/spec/v1.2.md
  • https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html
  • https://linuxconfig.org/introduction-to-the-overlayfs
  • https://github.com/vbatts/tar-split
  • 《第一本Docker书》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/685369.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序毕业设计-民大食堂用餐综合服务平台系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

CentOS6系统因目录有隐含i权限属性致下属文件无法删除的故障一例

CentOS6服务器在升级openssh时因系统目录权限异常&#xff08;有隐含i权限属性&#xff09;&#xff0c;下属文件无法删除&#xff0c;导致系统问题的故障一例。 一、问题现象 CentOS6在升级openssh时&#xff0c;提示如下问题&#xff1a; warning: /etc/ssh/sshd_config c…

java小游戏-坦克大战1.0

文章目录 游戏界面样式游戏需求分析设计类过程1&#xff1a;初始化界面过程2&#xff1a;用面向对象思想设置功能过程3&#xff1a;调用类实例化对象过程4&#xff1a;联合调试 项目代码下载&#xff1a; CSDN_java小游戏-坦克大战1.0 来源&#xff1a;该游戏来自尚学堂~&…

5.23.3 乳腺癌成像中的深度学习:十年的进展和未来方向

乳腺影像学在早期诊断和干预以改善乳腺癌患者的预后方面发挥着重要作用。在过去的十年中,深度学习在乳腺癌成像分析方面取得了显着进展,在解释乳腺成像模式的丰富信息和复杂背景方面具有巨大前景。 基于深度学习的乳腺癌成像研究涵盖了对乳房X光检查、超声、磁共振成像和数字…

新奇css模板

引言 (csscoco.com)https://csscoco.com/inspiration/#/./init 可视化集合 (hepengwei.cn)http://hepengwei.cn/#/html/visualDesign 30 秒代码 (30secondsofcode.org)https://www.30secondsofcode.org/ Animate.css |CSS动画的跨浏览器库。https://animate.style/

探索StartAI:创成式填充与AI绘画的革新

在人工智能的浪潮中&#xff0c;StartAI以其独特的创成式填充和AI绘画功能&#xff0c;成为创意产业的新星。这款产品不仅为艺术家和设计师提供了一个全新的创作平台&#xff0c;也为普通用户提供了探索和表达自我的途径。 创成式填充&#xff1a;【AI扩图】智能艺术的起点 S…

[网鼎杯 2020 青龙组]singal

记录下angr初使用 这道题是很简单的逻辑 32位 我们提取opcode (你可以用convert) 我是用的IDApython\ import idc adr0x00403040 step4#距离 op[] n10#多少个数据 while(n):op.append(hex(idc.get_wide_dword(adr)))adrstepn-1 print(op)然后我又下断点,提取每个"i&q…

Polar Web【简单】uploader

Polar Web【简单】uploader Contents Polar Web【简单】uploader思路EXP运行&总结 思路 本题的重点仍是文件上传&#xff0c;只是期间需要加上一步自主的文件上传。 打开环境&#xff0c;审查代码&#xff0c;发现在上传文件之后会自动生成一个以MD5散列值命名的目录&#…

我的名字叫大数据: 第7章 我的自拍展

7.1 生活瞬间:通过数据图像呈现 数据健身达人们!在经过一系列的辛勤锻炼后,是时候来看看我的“自拍展”了。通过数据图像,我们不仅可以更直观地了解数据,还能将复杂的信息以简单而美观的方式呈现出来。在这一节中,我将带你领略各种数据图像的魅力,从色彩缤纷的条形图到…

在 SEO 中,一个好的网页必须具备哪些 HTML 标签和属性?

搜索引擎优化 &#xff08;SEO&#xff09; 是涉及提高网站在搜索引擎上的可见性的过程。这是通过提高网站在搜索引擎结果页面&#xff08;例如Google&#xff09;上的排名来实现的。网站在这些页面上的显示位置越高&#xff0c;就越有可能获得更大的流量。 搜索引擎优化涉及了…

Linux高级进阶-ssh配置

Ubuntu-system 允许使用root远程登陆 apt install ssh -y在/etc/ssh/sshd_config 文件修改PermitRootLogin yes systemctl restart ssh远程连接软件用户名为root

c++--priority_queue和仿函数

目录 1.priority_queue 实现&#xff1a; 2.仿函数 priority_queue仿函数 实现代码 1.priority_queue 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的&#xff0c;其实就是个堆&#xff0c;默认是大根堆。…

查看服务器端口是否打开,如何查看服务器端口是否打开

查看服务器端口是否打开&#xff0c;是确保服务器正常运行和网络通信畅通的关键步骤。以下是几个有力的方法&#xff0c;帮助你快速、准确地判断端口状态。 首先&#xff0c;你可以使用telnet命令来检测端口的连通性。telnet是一个网络协议&#xff0c;可以用于远程登录和管理网…

O2O:Offline–Online Actor–Critic

IEEE TAI 2024 paper 加权TD3_BC Method 离线阶段&#xff0c;算法基于TD3_BC&#xff0c;同时加上基于Q函数的权重函数&#xff0c;一定程度上避免了过估计 J o f f l i n e ( θ ) E ( s , a ) ∼ B [ ζ Q ϕ ( s , π θ ( s ) ) ] − ∥ π θ ( s ) − a ∥ 2 \begin…

vue开发网站--对文章详情页的接口内容进行处理

一、需求 接口返回的数据中既包含文字也包含图片&#xff0c;并且需要对图片进行处理&#xff08;设置最大宽度为100%并拼接域名&#xff09; 可以按照以下步骤进行操作&#xff1a; 二、代码 <template><div class"details"><div class"infos…

【Linux取经路】网络套接字编程——初识篇

文章目录 一、端口号1.1 认识端口号1.2 端口号 VS 进程 PID 二、认识 TCP 协议三、认识 UDP四、网络字节序列五、socket 编程接口5.1 常用 API5.2 sockaddr 结构 六、结语 一、端口号 网络通信的本质是应用层软件进行数据的发送和接受&#xff0c;软件在启动之后&#xff0c;本…

使用C++实现YOLO图像分类:从环境搭建到性能评估的完整指南

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三连支…

python的优势有哪些?

python的优点很多&#xff0c;下面简单地列举一些&#xff1a; 简单 Python的语法非常优雅&#xff0c;甚至没有像其他语言的大括号&#xff0c;分号等特殊符号&#xff0c;代表了一种极简主义的设计思想。阅读Python程序像是在读英语。 易学 Python入手非常快&#xff0c;学习…

如何打造不一样的景区文旅VR体验馆项目?

近年来影院类产品迅速火爆&#xff0c;市面上的产品越来越多&#xff0c;投资者可以说是挑花了眼。为了助力投资者实现持续盈利&#xff0c;今天来给大家分析目前普乐蛙大爆新品悬空球幕飞行影院与其他5D/7D影院有哪些区别&#xff0c;给大家的创业投资之路避避雷~ 那我们正式开…

将现有web项目打包成electron桌面端教程(一)vue3+vite+js版

说明&#xff1a;后续项目需要web端和桌面端&#xff0c;为了提高开发效率&#xff0c;准备直接将web端的代码打包成桌面端&#xff0c;在此提前记录一下demo打包的过程&#xff0c;需要注意的是vue2或者vue3vitets或者vue-cli的打包方式各不同&#xff0c;如果你的项目不是vue…