標籤:

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:8nMAINTAINER @cloverstd <cloverstd@gmail.com>nnRUN apt-get update -y && n apt-get install -y emacsnRUN apt-get install -y apache2nnCMD ["/bin/bash"]n

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

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

IMAGE CREATED CREATED BY SIZE COMMENTnd951e6ed5b00 34 minutes ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0Bn4ea03e7b0db6 34 minutes ago /bin/sh -c apt-get install -y apache2 13.5MBn9ea713f268c9 36 minutes ago /bin/sh -c apt-get update -y && apt-ge... 364MBn0f8e9812e8b8 42 minutes ago /bin/sh -c #(nop) MAINTAINER @cloverstd <... 0Bn25fc9eb3417f 4 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0Bn<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:55b071e2cfc3ea2... 123MBn

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

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

儲存

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

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

FROM alpine:3.4nnRUN mkdir -p /data/layernWORKDIR /data/layernnCOPY layer1 /data/layernCOPY layer2 /data/layernnRUN touch /data/layer/layer1nnCOPY layer3 /data/layernRUN echo echo "hello" >> /etc/profilen

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

{n // ...n "GraphDriver": {n "Data": {n "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",n "MergedDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/merged",n "UpperDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/diff",n "WorkDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/work"n },n "Name": "overlay2"n }n // ...n}n

其中 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 中。

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

容器

圖片來源網路

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

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

{n "GraphDriver": {n "Data": {n "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",n "MergedDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/merged",n "UpperDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/diff",n "WorkDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/work"n },n "Name": "overlay2"n }n}n

從上面可以看到,從/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:layerrepository 這個 namespace。

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

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

相同的層的只會存一份。

編寫 Dockerfile

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

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

推薦閱讀:

【DockerCon2017最新技術解讀】Docker最新特性介紹
Egg.js+Antd 擼個簡易版阿里雲CS控制台?
Docker Remote API 如何使用?
基於OSS搭建私有(跨區域)Docker鏡像倉庫

TAG:Docker |