Docker 容器與鏡像的儲存

derek_zhan 7年前發布 | 28K 次閱讀 Docker

Docker 容器與鏡像的儲存

在 Docker 的生態中,有容器(container)和鏡像(image)兩個重要的概念,那么容器和鏡像是如何在主機(host)上儲存的呢?

系統信息

  • 系統: Ubuntu 16.04
  • Docker: 17.10.0-ce
    • Storage Driver: overlay2

鏡像

首先來看下什么是容器,引用 Docker 官方的話的就是

容器是一個輕量級(lightweight)、獨立的(stand-alone)和包含一系列軟件能夠執行的程序包

那么鏡像和容器有什么關系呢?容器可以認為是一個實例化的鏡像的。鏡像在系統上,是分層儲存的,每一層的文件、配置信息疊加在一起,就成為了鏡像。

制作

首先看下制作鏡像,一般情況下,是通過編寫 Dockerfile 然后使用 Docker 命令來生成一個鏡像。

下面來看一個例子,首先新建一個文件,名字為 Dockerfile,內容如下

FROM debian:8
MAINTAINER @cloverstd <cloverstd@gmail.com>

RUN apt-get update -y && \
    apt-get install -y emacs
RUN apt-get install -y apache2

CMD ["/bin/bash"]

然后通過執行 docker build -t repository:tag . 命令,就可以生成一個名為 repository:tag 的鏡像。

通過 docker history repository:tag 命令可以看到鏡像的每一層的信息,在我的機器上,輸出如下

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d951e6ed5b00        34 minutes ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
4ea03e7b0db6        34 minutes ago      /bin/sh -c apt-get install -y apache2           13.5MB
9ea713f268c9        36 minutes ago      /bin/sh -c apt-get update -y &&     apt-ge...   364MB
0f8e9812e8b8        42 minutes ago      /bin/sh -c #(nop)  MAINTAINER @cloverstd <...   0B
25fc9eb3417f        4 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:55b071e2cfc3ea2...   123MB

可以通過上面的信息看到在 Dockerfile 中的每一個『命令』都被映射到了每一層,其實在制作鏡像的過程,在 RUN 命令執行時,docker 會運行一個臨時容器,在里面運行 RUN 后面的命令,然后再把容器提交成為鏡像,所以,容器可以變成鏡像,鏡像也可以變成容器。

通過上面的輸出的第一列可以看出,在 docker 里面,其實每一層都是一個 image,但是一般情況下,大家都把 `repository:tag` 這個稱為一個鏡像。

儲存

由于 Unix 一切皆文件,所以 Docker 鏡像也是以文件的形式儲存在系統中,并且是分層儲存的。

下面來看另外一個例子,Dockerfile 如下

FROM alpine:3.4

RUN mkdir -p /data/layer
WORKDIR /data/layer

COPY layer1 /data/layer
COPY layer2 /data/layer

RUN touch /data/layer/layer1

COPY layer3 /data/layer
RUN echo 'echo "hello"' >> /etc/profile

然后通過 docker buil -t repository:layer . 命令,生成一個名為 repository:layer 的鏡像,鏡像 ID 為 e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8 ,然后執行 docker inspect repository:layer ,可以得到以下信息(省略了部分)

{
  // ...
  "GraphDriver": {
    "Data": {
      "LowerDir": "/var/lib/docker/overlay2/cc191abf48cfa6ba96e1f4eae0133743c6cdcc6eb9942624bd0ad4df015d1f85/diff:/var/lib/docker/overlay2/5157fc9701ca747754ad8f3a18622ae1d38aab8302324c34cb5614ee30b7abdb/diff:/var/lib/docker/overlay2/3d008a0d62a6ce66adba7401a6a887a87cc0ee3fba306e7d06fcbd4d76f35207/diff:/var/lib/docker/overlay2/53f442e9e9c78238eb98fc3a9d418b66218ab34cfeb5618adb3c40558b8f5b59/diff:/var/lib/docker/overlay2/3b5e8ca8ad4b0b4605a7e27f272e5ad85a9198ac6ae730c4de3a6ee27ab558bb/diff:/var/lib/docker/overlay2/4f144dd9d686cc3c6f1dae44e921e20969ea4b977f7beef16d6f8a258f1cb894/diff",
      "MergedDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/merged",
      "UpperDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/diff",
      "WorkDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/work"
    },
    "Name": "overlay2"
  }
  // ...
}

其中 GraphDriver.Data 下的信息就是鏡像在機器上的儲存路徑了。

將上面信息整理一下,得到下面的結構

  1. /var/lib/docker/overlay2/92820.../diff
  2. /var/lib/docker/overlay2/cc191.../diff
  3. /var/lib/docker/overlay2/5157f.../diff
  4. /var/lib/docker/overlay2/3d008.../diff
  5. /var/lib/docker/overlay2/53f44.../diff
  6. /var/lib/docker/overlay2/3b5e8.../diff
  7. /var/lib/docker/overlay2/4f144.../diff

從上到下,就是鏡像當前層的文件與之前所有層的 diff 情況。

與上面鏡像的 Dockerfile 對應起來看就是,1 中存的文件就是 echo 'echo "hello"' >> /etc/profile 的改變,因為 /etc/profile 這個文件在之前的層是存在的。

所以在 docker 制作鏡像的過程中,docker 會將 /etc/profile 拷貝一份,然后在拷貝的基礎上修改儲存,diff 的級別是文件本身,而不是文件內容。

7 對應的就是看似是 FROM alpine:3.4 這一行,其實,是因為 alpine:3.4 這個鏡像就一層,所以在這里看起來,基礎鏡像會是一層。

其他層的與 Dockerfile 也是一一對應的。

而 WORKDIR /data/layer 這一條 Dockerfile,是沒有文件的改變,所以沒有單獨的一層來儲存,是存在 /var/lib/docker/image/overlay2/imagedb/content/sha256 這里的配置信息中。

上面是在 overlay2 這個 driver 中的儲存結構,但是 docker 支持多種 driver,那么 docker 是如何在不同 driver 中相互導入導出的并且保持鏡像結構不變的呢?

可以看下 docker image 脫離于 driver 的結構,首先將鏡像從 docker 中導出,執行 docker save repository:layer -o image.tar 會在當前目錄下生成一個 image.tar 的文件。

解壓后就會得到 repository:layer 這個鏡像的每一層的文件信息了,解壓后的主要文件信息如下

  • e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8.json 存的鏡像的配置信息。
  • repositories 文件存的是鏡像頂層的 layer 信息,在我這里是 f8504ccc4a74115c572be9f13925c63b628b1e3c5eb347196f62971aa8e9a335 這個 ID,也就是 layer index。

通過 repositories 里信息,可以看到 ID 的 。除了上面說的兩個文件,解壓出來的還有以 layer ID 命名的目錄。

根據 repositories 中的 layer ID 進入到對應的目錄里。

里面有三個文件,其中 layer.tar 里存的就是這一層與之前所以層的 diff 文件,也就是上面 1 中的文件, /etc/profile 。

然后還有一個 json 文件,里面存的是這一層在鏡像制作過程中的臨時容器信息,還有一個最重要的 parent 項,里面存的信息就是這一層的下面一層的 ID,根據這個 ID 就可以依次找到每一層的信息。

這里面存的就是鏡像的信息,把這個 image.tar 拿到其他裝有 docker 的機器上,通過 docker load -i image.tar 就可以將鏡像導入到 docker 中。

根據上面的鏡像儲存的文件信息,可以看出,鏡像是分層儲存的。

容器

Docker 容器與鏡像的儲存

圖片來源網絡

上面說了,容器就是一個鏡像的實例化的表現,所以,容器也是分層的,當運行一個容器時,會在鏡像的最上層加一個 writable layer(如上圖所屬),在容器運行時對于容器的讀寫文件操作,都是作用在 writable layer 的。

將上面的 repository:layer 鏡像通過命令 docker run -it --name layer --rm repository:layer sh 運行起來,然后再次通過 docker inspect layer 這個命令,還是看 GraphDriver.Data 信息

{
  "GraphDriver": {
    "Data": {
          "LowerDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b-init/diff:/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/diff:/var/lib/docker/overlay2/cc191abf48cfa6ba96e1f4eae0133743c6cdcc6eb9942624bd0ad4df015d1f85/diff:/var/lib/docker/overlay2/5157fc9701ca747754ad8f3a18622ae1d38aab8302324c34cb5614ee30b7abdb/diff:/var/lib/docker/overlay2/3d008a0d62a6ce66adba7401a6a887a87cc0ee3fba306e7d06fcbd4d76f35207/diff:/var/lib/docker/overlay2/53f442e9e9c78238eb98fc3a9d418b66218ab34cfeb5618adb3c40558b8f5b59/diff:/var/lib/docker/overlay2/3b5e8ca8ad4b0b4605a7e27f272e5ad85a9198ac6ae730c4de3a6ee27ab558bb/diff:/var/lib/docker/overlay2/4f144dd9d686cc3c6f1dae44e921e20969ea4b977f7beef16d6f8a258f1cb894/diff",
          "MergedDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/merged",
          "UpperDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/diff",
          "WorkDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/work"
      },
      "Name": "overlay2"
  }
}

從上面可以看到,從 /var/lib/docker/overlay2/92820.../diff 開始,都是和上面鏡像一模一樣的文件夾。

唯一的區別就是 /var/lib/docker/overlay2/9b949f...-init/diff ,這個是容器在運行時的 init layer,里面存的是容器的 host 和 dns 信息,這一層也是 readonly layer 。

真正的 writable layer 是 /var/lib/docker/overlay2/9b949... 。

如果在上面運行的容器中去修改一下 /data/layer/layer3 文件的值為 4,

對應的在系統中的 /var/lib/docker/overlay2/9b949.../diff 目錄下,

就會多出一個 data/layer/layer3 的文件,并且文件內容為 4 。

而 /var/lib/docker/overlay2/9b949.../merged 目錄中就是容器中的用戶視角的所以文件了,包含這個容器的每一層文件,所以在這個目錄下的 data/layer/layer3 文件的內容也會變成 4 。

以上就是容器在系統中的儲存結構了。

registry

registry 是鏡像在服務端的儲存倉庫, docker hub 就是 docker 官方提供的 docker registry。

我們也可以通過官方提供的 distribution 來自己搭建私有的鏡像倉庫。

在 registry 中,鏡像也是以分層的形式儲存的,registry 也是支持多種儲存方式( driver )的,默認就是 filesystem 本地文件存儲,關于自定義 driver 可以看 這里 

通過 docker run -d -v /var/lib/registry:/var/lib/registry -p 5000:5000 registry:2 來在本地運行一個鏡像倉庫。

然后將我們前面制作的 repository:layer 推送到這個鏡像倉庫中。

其實鏡像的名字,實際上是應該要包含鏡像倉庫的地址的,如果不寫,默認就是官方的 docker hub 了。

所以推送之前,先需要將我們的鏡像通過 docker tag repository:layer 127.0.0.1:5000/repository:layer 命令重新命名一下。

然后執行 docker push 127.0.0.1:5000/repository:layer 就可以將鏡像推送都剛剛運行的鏡像倉庫中了。

在推送的過程中,也是可以看到,鏡像是分層推送的。

當推送完畢之后,可以在主機上的 /var/lib/registry/docker/registry/v2 這個目錄下看到剛剛推送的鏡像了,當然,也是分層儲存的,并且鏡像的每一層的文件、配置信息與連接每一層的 index 是分開儲存的,這樣就可以在鏡像倉庫中復用同一層,當推送的鏡像的某一層在 registry 中時,docker 就不會再次推送這一層了,可以加速鏡像的推送,也可以節省儲存空間。

其中 repositories/repository 這個目錄,表示的是鏡像 127.0.0.1:5000/repository:layer 的 repository 這個 namespace。

在這個目錄下的 _manifests/tags 目錄下,則存的是這個 namespace 下所以的 tag 了,比如我們剛剛推送的 tag 是 layer ,所以會有一個 layer 的目錄,里面包含了 layer 這個 tag 的 index 信息。

通過 index 信息,就可以在 repositories/repository/layer/_layers/sha256 里面找到每一層的 index,根據 index 可以在 repositories/blobs 下面找到對應的每一層的文件和配置信息。

相同的層的只會存一份。

編寫 Dockerfile

通過上面的鏡像的儲存分析,所以在編寫 Dockerfile 的時候,可以遵循下面的幾點規則

  • 合理分層,重復利用鏡像緩存
  • 只刪除當前層中創建的文件
  • 選擇較小體積的基礎鏡像(比如 alpine)

 

來自:https://zhuanlan.zhihu.com/p/31744232

 

 本文由用戶 derek_zhan 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!