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
下的信息就是鏡像在機器上的儲存路徑了。
將上面信息整理一下,得到下面的結構
- /var/lib/docker/overlay2/92820.../diff
- /var/lib/docker/overlay2/cc191.../diff
- /var/lib/docker/overlay2/5157f.../diff
- /var/lib/docker/overlay2/3d008.../diff
- /var/lib/docker/overlay2/53f44.../diff
- /var/lib/docker/overlay2/3b5e8.../diff
- /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: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)
推薦閱讀:
※【DockerCon2017最新技術解讀】Docker最新特性介紹
※Egg.js+Antd 擼個簡易版阿里雲CS控制台?
※Docker Remote API 如何使用?
※基於OSS搭建私有(跨區域)Docker鏡像倉庫
TAG:Docker |