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 下的信息就是鏡像在機器上的儲存路徑了。
將上面信息整理一下,得到下面的結構
- /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 信息
{
"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