映象的在節點上的儲存結構是怎麼樣的?

2023-07-10 21:00:54

每日一問系列

映象的在節點上的儲存結構是怎麼樣的?

我們經常會使用 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 只會提交可讀寫層,所以是不包含這些內容的。