镜像的在节点上的存储结构是怎么样的?

每日一问系列

镜像的在节点上的存储结构是怎么样的?

我们经常会使用 docker 或者其他 cri 工具拉取镜像来运行容器,却没有去实际了解 pull 下来的镜像在机器上是怎么存储的。以下以常用的 overlay2 存储驱动为例,解析镜像的存储结构,其他存储驱动也是类似

编写如下 Dockerfile 文件

FROM ubuntu:latest
ENV author jlz
RUN echo "x1" >> /tmp/test
RUN echo "x2" >> /tmp/test2
RUN echo "x3" >> /tmp/test3
ENTRYPOINT ["/bin/bash", "-c", "sh"]

通过 docker build 命令构建一个镜像

docker build -t my-ubuntu:0,1 .

镜像存储目录结构

在 overlay2 存储驱动中,镜像层之间的关系可以通过 LowerDir、UpperDir、MergedDir 目录结构表示 对应上面 inspect 出来的镜像 GraphDriver 字段

通过 docker inspect {image id} 命令查看镜像信息,如下

"Config": {
    "Env": [
        "author=jlz"
    ],
    "Entrypoint": [
        "/bin/bash",
        "-c",
        "cat /tmp/test"
    ]
},
"GraphDriver": {
    "Data": {
        "LowerDir": "/mnt/datadisk0/docker/overlay2/dff0bddcffaaa428ea232b202275d48845c11783ea428e9cfa335987cf91805c/diff:/mnt/datadisk0/docker/overlay2/3b5766ed7c43b9417311635ec98d844a98586b9854538975bc4ef12d22edfe1c/diff:/mnt/datadisk0/docker/overlay2/51798d33e8f37ed44c79b7ed5626e95936dd60b8269328557bb6d09f3e353356/diff",
        "MergedDir": "/mnt/datadisk0/docker/overlay2/492b8eb5dba9dbb4c72616fe0f8e9423a552d42e5ffe017cbd2e2fb60b3e20a7/merged",
        "UpperDir": "/mnt/datadisk0/docker/overlay2/492b8eb5dba9dbb4c72616fe0f8e9423a552d42e5ffe017cbd2e2fb60b3e20a7/diff",
        "WorkDir": "/mnt/datadisk0/docker/overlay2/492b8eb5dba9dbb4c72616fe0f8e9423a552d42e5ffe017cbd2e2fb60b3e20a7/work"
    },
    "Name": "overlay2"
},
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:cdd7c73923174e45ea648d66996665c288e1b17a0f45efdbeca860f6dafdf731",
        "sha256:120009c8f50a6cc9022bf7b9fcc7b4f7ef5ba8ea3736dfe974e11780d1a840a0",
        "sha256:b6f2b52c36d89acd2e8ce8d85c178c722501dad0ee64de2aa4d15ac18c1cf0fc",
        "sha256:7949cc4bef953bb279a2b9b3c27def2a9399706bb1344461299ac4c01c4692df"
    ]
},

如上 RootFS.Layers 表示这个镜像只有 4 层,因为上面的 Dockerfile 中 base 镜像 ubuntu 本身只有一层,RUN 指令分别对应一层,而 ENV 和 ENTRYPOINT 由于没有涉及到文件系统修改,所以不会有对应的镜像层,他们直接存在于镜像的元数据信息中,如上面的 Config.Env 和 Config.Entrypoint

UpperDir:最新的一层镜像层的变更信息(第 n 层),这里对应为 第 4 层,即 RUN echo "x3" >> /tmp/test3

图片

LowerDir: 除最新镜像层的所有层(第 1 ~n-1 层),格式为 {n-1}:{n-2}...{1}

MergedDir:LowerDir 和 UpperDir 的合并,形成最终的镜像的 rootfs 结构

容器存储目录结构

通过这个镜像创建一个容器

docker run -it --entrypoint sh {image id}

注意这里的 --entrypoint 参数用于修改容器的 entrypoint

在容器中执行命令 echo "hahaha" test4 创建新文件,并通过 docker inspect {container_id} 查看容器存储结构

"Config": {
            "Entrypoint": [
                "sh"
            ]
        },
"GraphDriver": {
            "Data": {
                "LowerDir": "/mnt/datadisk0/docker/overlay2/f2a196d05ccbae06927091297ea503ce59ddf6bc01b8edd686358ca9a41b9abd-init/diff:/mnt/datadisk0/docker/overlay2/492b8eb5dba9dbb4c72616fe0f8e9423a552d42e5ffe017cbd2e2fb60b3e20a7/diff:/mnt/datadisk0/docker/overlay2/dff0bddcffaaa428ea232b202275d48845c11783ea428e9cfa335987cf91805c/diff:/mnt/datadisk0/docker/overlay2/3b5766ed7c43b9417311635ec98d844a98586b9854538975bc4ef12d22edfe1c/diff:/mnt/datadisk0/docker/overlay2/51798d33e8f37ed44c79b7ed5626e95936dd60b8269328557bb6d09f3e353356/diff",
                "MergedDir": "/mnt/datadisk0/docker/overlay2/f2a196d05ccbae06927091297ea503ce59ddf6bc01b8edd686358ca9a41b9abd/merged",
                "UpperDir": "/mnt/datadisk0/docker/overlay2/f2a196d05ccbae06927091297ea503ce59ddf6bc01b8edd686358ca9a41b9abd/diff",
                "WorkDir": "/mnt/datadisk0/docker/overlay2/f2a196d05ccbae06927091297ea503ce59ddf6bc01b8edd686358ca9a41b9abd/work"
            },
            "Name": "overlay2"
        },

可以看到 Config.Entrypoint 被修改为 sh,此时 GraphDriver 中的目录相比 inspect 镜像的结果也发生了变化

UpperDir:这个目录包含了容器的可写层,可以看到在容器中创建的 test4 文件。这个目录中的文件可以被修改,但是它们只存在于容器的生命周期中。

LowerDir:这个目录包含了镜像的只读层,也就是镜像的文件系统。结合上面镜像的存储结构可以发现,这里包含了所有的 n 层镜像目录。这些文件是只读的,不能被修改

WorkDir:这个目录是 overlay2 文件系统的工作目录,也就是容器内部的工作目录。当你在容器中运行一个命令时,Docker会将该命令的工作目录设置为WorkDir指定的目录。

MergedDir:LowerDir 和 UpperDir 的合并结果,也就是镜像只读层和容器可写层的合并结果。

init 层的作用

如果细心的话可以发现 inpect 容器的结果中, LowerDir 除了所有的镜像只读层外,还有一个 init 层

“init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息

需要这样一层的原因是,用户往往需要在启动容器时写入一些指定的值比如在/etc/hosts中写入hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。

所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。

图片

posted @ 2023-07-10 19:35  JL_Zhou  阅读(239)  评论(0编辑  收藏  举报