原文發布在:http://cizixs.com/2016/04/06/docker-images,轉載請注明出處。
這篇文章主要講講 docker 中鏡像有關的知識,將涉及到下面幾個方面:
- docker images 命令的使用
- docker 和 registry 交互的過程,pull 命令到底做了什么
- docker storage driver
- aufs 的格式和實際的組織結構
- Dockerfile 原語和 docker 鏡像之間的關系
簡介
- docker 鏡像代表了容器的文件系統里的內容,是容器的基礎,鏡像一般是通過 Dockerfile 生成的
- docker 的鏡像是分層的,所有的鏡像(除了基礎鏡像)都是在之前鏡像的基礎上加上自己這層的內容生成的
- 每一層鏡像的元數據都是存在 json 文件中的,除了靜態的文件系統之外,還會包含動態的數據
使用鏡像:docker image 命令
docker client 提供了各種命令和 daemon 交互,來完成各種任務,其中和鏡像有關的命令有:
-
docker images
:列出 docker host 機器上的鏡像,可以使用-f
進行過濾 -
docker build
:從 Dockerfile 中構建出一個鏡像 -
docker history
:列出某個鏡像的歷史 -
docker import
:從 tarball 中創建一個新的文件系統鏡像 -
docker pull
:從 docker registry 拉去鏡像 -
docker push
:把本地鏡像推送到 registry -
docker rmi
: 刪除鏡像 -
docker save
:把鏡像保存為 tar 文件 -
docker search
:在 docker hub 上搜索鏡像 -
docker tag
:為鏡像打上 tag 標記
從上面這么多命令中,我們就可以看出來,docker 鏡像在整個體系中的重要性。
下載鏡像:pull 和 push 鏡像到底在做什么?
如果了解 docker 結構的話,你會知道 docker 是典型的 C/S 架構。平時經常使用的 docker pull
, docker run
都是客戶端的命令,最終這些命令會發送到 server 端(docker daemon 啟動的時候會啟動docker server)進行處理。下載鏡像還會和 Registry 打交道,下面我們就說說使用 docker pull
的時候,docker 到底在做些什么!
docker client 組織配置和參數,把 pull 指令發送給 docker server,server 端接收到指令之后會交給對應的 handler。handler 會新開一個 CmdPull job 運行,這個 job 在 docker daemon 啟動的時候被注冊進來,所以控制權就到了 docker daemon 這邊。docker daemon 是怎么根據傳過來的 registry 地址、repo 名、image 名和tag 找到要下載的鏡像呢?具體流程如下:
- 獲取 repo 下面所有的鏡像 id:
GET /repositories/{repo}/images
- 獲取 repo 下面所有 tag 的信息:
GET /repositories/{repo}/tags
- 根據 tag 找到對應的鏡像 uuid,并下載該鏡像
- 獲取該鏡像的 history 信息,并依次下載這些鏡像層:
GET /images/{image_id}/ancestry
- 如果這些鏡像層已經存在,就 skip,不存在的話就繼續
- 獲取鏡像層的 json 信息:
GET /images/{image_id}/json
- 下載鏡像內容:
GET /images/{image_id}/layer
- 下載完成后,把下載的內容存放到本地的 UnionFS 系統
- 在 TagStore 添加剛下載的鏡像信息
- 獲取該鏡像的 history 信息,并依次下載這些鏡像層:
存儲鏡像:docker storage 介紹
在上一個章節提到下載的鏡像會保存起來,這一節就講講到底是怎么存的。
UnionFS 和 aufs
如果對 docker 有所了解的話,會聽說過 UnionFS 的概念,這是 docker 實現層級鏡像的基礎。在 wikipedia 是這么解釋的:
Unionfs is a filesystem service for Linux, FreeBSD and NetBSD which
implements a union mount for other file systems. It allows files and
directories of separate file systems, known as branches, to be
transparently overlaid, forming a single coherent file system.
Contents of directories which have the same path within the merged
branches will be seen together in a single merged directory, within
the new, virtual filesystem.
簡單來說,就是用多個文件夾和文件(這些是系統文件系統的概念)存放內容,對上(應用層)提供虛擬的文件訪問。
比如 docker 中有鏡像的概念,應用層看來只是一個文件,可以讀取、刪除,在底層卻是通過 UnionFS 系統管理各個鏡像層的內容和關系。
docker 負責鏡像的模塊是 Graph
,對上提供一致和方便的接口,在底層通過調用不同的 driver 來實現。常用的 driver 包括 aufs、devicemapper,這樣的好處是:用戶可以選擇甚至實現自己的 driver。
aufs 鏡像在機器上的存儲結構
NOTE:
- 只下載了 ubuntu:14.04 鏡像
- docker version:1.6.3
- image driver:aufs
使用 docker history 查看鏡像歷史:
root@cizixs-ThinkPad-T450:~# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
172.16.1.41:5000/ubuntu 14.04 2d24f826cb16 13 months ago 188.3 MB
root@cizixs-ThinkPad-T450:~# docker history 2d24
IMAGE CREATED CREATED BY SIZE
2d24f826cb16 13 months ago /bin/sh -c #(nop) CMD [/bin/bash] 0 B
117ee323aaa9 13 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB
1c8294cc5160 13 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 194.5 kB
fa4fd76b09ce 13 months ago /bin/sh -c #(nop) ADD file:0018ff77d038472f52 188.1 MB
511136ea3c5a 2.811686 years ago 0 B
可以看到,ubuntu:14.04 一共有五層鏡像。aufs 數據存放在 /var/lib/docker/aufs 目錄下:
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# tree -L 1
.
├── diff
├── layers
└── mnt
一共有三個文件夾,每個文件夾下面都是以鏡像 id 命令的文件夾,保存了每個鏡像的信息。先來介紹一下這三個文件夾
- layers:顯示了每個鏡像有哪些層構成
- diff:每個鏡像的和之前鏡像的區別,就是這一層的內容
- mnt:UnionFS 對外提供的 mount point,因為 UnionFS 底層是多個文件夾和文件,對上層要提供統一的文件服務,是通過 mount 的形式實現的。每個運行的容器都會在這個目錄下有一個文件夾
比如 diff 文件夾是這樣的:
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7/
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c/
etc
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/1c8294cc516082dfbb731f062806b76b82679ce38864dd87635f08869c993e45/
etc sbin usr var
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/fa4fd76b09ce9b87bfdc96515f9a5dd5121c01cc996cf5379050d8e13d4a864b/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/
除了這些實際的數據之外,docker 還為每個鏡像層保存了 json 格式的元數據,存儲在 /var/lib/docker/graph/<image_id>/json
,比如:
root@cizixs-ThinkPad-T450:/var/lib/docker# cat graph/2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7/json | jq '.'
{
"id": "2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7",
"parent": "117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c",
"created": "2015-02-21T02:11:06.735146646Z",
"container": "c9a3eda5951d28aa8dbe5933be94c523790721e4f80886d0a8e7a710132a38ec",
"container_config": {
"Hostname": "43bd710ec89a",
"Domainname": "",
"User": "",
"Memory": 0,
"MemorySwap": 0,
"CpuShares": 0,
"Cpuset": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"PortSpecs": null,
"ExposedPorts": null,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) CMD [/bin/bash]"
],
"Image": "117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"NetworkDisabled": false,
"MacAddress": "",
"OnBuild": [],
"Labels": null
},
"docker_version": "1.4.1",
"config": {
"Hostname": "43bd710ec89a",
"Domainname": "",
"User": "",
"Memory": 0,
"MemorySwap": 0,
"CpuShares": 0,
"Cpuset": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"PortSpecs": null,
"ExposedPorts": null,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"Image": "117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"NetworkDisabled": false,
"MacAddress": "",
"OnBuild": [],
"Labels": null
},
"architecture": "amd64",
"os": "linux",
"Size": 0
}
除了 json 之外,還有一個文件 /var/lib/docker/graph/<image_id>/layersize
保存了鏡像層的大小。
創建鏡像:鏡像的 cache 機制
在使用 docker build
創建新的鏡像的時候,docker 會使用到 cache 機制,來提高執行的效率。為了理解這個問題,我們先看一下 build 命令都做了哪些東西吧。
我們來看一個簡單的 Dockerfile:
FROM ubuntu:14.04
RUN apt-get update
ADD run.sh /
VOLUME /data
CMD ["./run.sh"]
這個文件雖然簡單,卻包含了很多命令:RUN、ADD、VOLUME、CMD 涉及到很多概念。
一般情況下,對于每條命令,docker 都會生成一層鏡像。 cache 的作用也很容易猜測,如果在構建某個鏡像層的時候,發現這個鏡像層已經存在了,就直接使用,而不是重新構建。這里最重要的問題在于:怎么知道要構建的鏡像層已經存在了? 下面就重點解釋這個問題。
docker daemon 讀到 FROM
命令的時候,會在本地查找對應的鏡像,如果沒有找到,會從 registry 去取,當然也會取到包含 metadata 的 json 文件。然后到了 RUN
命令,如果沒有 cache 的話,這個命令會做什么呢?
我們已經知道,每層鏡像都是由文件系統內容和 metadata 構成的。
文件系統的內容,就是執行 apt-get update
命令導致的文件變動,會保存到 /var/lib/docker/aufs/diff/<image_id>/
,比如這里的命令主要會修改 /var/lib 和 /var/cache 下面和 apt 有關的內容:
root@cizixs-ThinkPad-T450:/var/lib/docker# tree -L 2 aufs/diff/e7ae26691ff649c55296adf7c0e51b746e22abefa6b30310b94bbb9cfa6fce63/
aufs/diff/e7ae26691ff649c55296adf7c0e51b746e22abefa6b30310b94bbb9cfa6fce63/
├── tmp
└── var
├── cache
└── lib
我們來看一下 json 文件的內容,最重要的改變就是 container_config.Cmd 變成了:
"Cmd": [
"/bin/sh",
"-c",
"apt-get update"
],
也就是說,如果下次再構建鏡像的時候,我們發現新的鏡像層 parent 還是 ubuntu:14.04,并且 json 文件中 cmd 要更改的內容也一致,那么就認為這兩層鏡像是相同的,不需要重新構建。好了,那么構建的時候,daemon 一定會遍歷本地所有鏡像,如果發現鏡像一致就使用已經構建好的鏡像。
ADD 和 COPY 文件
如果 Dockerfile 中有 ADD 或者 COPY 命令,那么怎么判斷鏡像是否相同呢?第一個想法肯定是文件名,但即使文件名不變,那么文件也是可以變的;那就再加上文件大小,不過兩個同名并且大小相同的文件也不一定內容完全一樣啊!最保險的辦法就是用 hash 了,嗯!docker 就是這個干的,我們來看一下 ADD 這層鏡像的 json 文件變化:
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ADD file:9fb96e5dd9ce3e03665523c164bbe775d64cc5d8cc8623fbcf5a01a63e9223ab in /"
],
看到沒,ADD 的時候只有一串 hash 字符串,hash 算法的實現,如果感興趣可以自己研究一下。
喂!這樣真的就萬無一失了嗎?
看完上面的內容,大多數同學會覺得 cache 機制真好, 很節省時間,也能節省空間。但是這里還有一個問題,有些命令是依賴外部的,比如 apt-get update
或者 curl http://some.url.com/
,如果外部內容發生了改變,docker 就沒有辦法偵測到,去做相應的處理了。所以它提供了 --no-cache
參數來強制不要使用 cache 機制,所以說這部分內容是要用戶自己維護的。
除此之外,還需要在編寫 Dockerfile 的時候考慮到 cache,這一點在官方提供的 dockerfile best practice 也有提及。
運行鏡像:docker 鏡像和 docker 容器
我們都知道 docker 容器就是運行態的docker 鏡像,但是有一個問題:docker 鏡像里面保存的都是靜態的東西,而容器里面的東西是動態的,那么這些動態的東西是如何管理的呢?比如說:
- docker 容器里該運行那些進程?
- 怎么把 docker 鏡像轉換成docker 容器?
- docker 容器里面 ip、hostname 這些東西使如何動態生成的?
這就是上面提到的 json 文件的功能,哪些信息會存放在 json 文件呢?答案就是:除了文件系統的內容外,其他都是,比如:
- ENV FOO=BAR: 環境變量,
- VOLUME /some/path:容器使用的 volume,乍看上去這是文件系統的一部分,其實這部分內容不是確定的,在構建鏡像的時候數據卷可以是不存在的,會在容器運行的時候動態地添加。所以這部分內容不能放到鏡像層文件中
- EXPOSE 80:expose 命令記錄了容器運行的時候要暴露給外部的端口,這也是運行時狀態,不是文件系統的一部分
- CMD ["./myscript.sh"]:CMD 命令記錄了 docker 容器的執行入口,這不是文件系統的一部分
好了,既然我們已經知道這些東西是怎么存儲的,那么實際運行容器的時候這些內容是怎么被加載到容器里的呢?答案就是 docker daemon,這個實際管理容器實現的家伙。
我們知道,在容器實際運行過程中,每個容器就是 docker daemon 的子進程:
root 3249 0.1 6.6 985212 33288 ? Ssl 04:53 0:19 /usr/bin/docker daemon --insecure-registry 172.16.1.41:5000 --exec-opt native.cgroupdriver=cgroupfs --bip=10.12.240.1/20 --mtu=1500 --ip-masq=false
root 3597 0.0 0.1 3816 632 ? Ssl 04:55 0:00 \_ /pause
root 3633 0.0 0.1 3816 504 ? Ssl 04:55 0:00 \_ /pause
root 3695 0.0 0.1 3816 516 ? Ssl 04:55 0:00 \_ /pause
root 3710 0.0 0.1 3816 528 ? Ssl 04:55 0:00 \_ /pause
root 3745 0.0 0.1 3816 504 ? Ssl 04:55 0:00 \_ /pause
polkitd 3793 0.0 0.2 36524 1280 ? Ssl 04:55 0:07 \_ redis-server *:6379
root 3847 0.0 0.0 4184 184 ? Ss 04:55 0:00 \_ /bin/sh -c /run.sh
root 3872 0.0 0.0 17668 360 ? S 04:55 0:00 | \_ /bin/bash /run.sh
root 3873 0.0 0.3 42824 1752 ? Sl 04:55 0:01 | \_ redis-server *:6379
root 3865 0.0 1.5 166256 8024 ? Ss 04:55 0:00 \_ apache2 -DFOREGROUND
33 3881 0.0 1.0 166280 5140 ? S 04:55 0:00 | \_ apache2 -DFOREGROUND
33 3882 0.0 1.0 166280 5140 ? S 04:55 0:00 | \_ apache2 -DFOREGROUND
33 3883 0.0 1.0 166280 5140 ? S 04:55 0:00 | \_ apache2 -DFOREGROUND
33 3884 0.0 1.0 166280 5140 ? S 04:55 0:00 | \_ apache2 -DFOREGROUND
33 3885 0.0 1.0 166280 5140 ? S 04:55 0:00 | \_ apache2 -DFOREGROUND
root 3939 0.0 0.7 90264 4016 ? Ss 04:55 0:00 \_ nginx: master process nginx
33 3947 0.0 0.3 90632 1660 ? S 04:55 0:00 \_ nginx: worker process
33 3948 0.0 0.3 90632 1660 ? S 04:55 0:00 \_ nginx: worker process
33 3949 0.0 0.3 90632 1660 ? S 04:55 0:00 \_ nginx: worker process
33 3950 0.0 0.3 90632 1660 ? S 04:55 0:00 \_ nginx: worker process
也是說,docker daemon 會讀取鏡像的信息,作為容器的 rootfs,然后讀取 json 文件中的動態信息作為運行時狀態。
刪除鏡像:清理鏡像之道
鏡像是按照 UnionFS 的格式存放在本地的,刪除也很容易理解,就是把對應鏡像層的本地文件(夾)刪除。docker 也提供了 docker rmi
這個命令來處理。
不過需要注意一點:鏡像也是有“引用”這個概念的,只有當該鏡像層沒有被引用的時候,才能刪除。“引用”就是被打上 tag,同一個 uuid 的鏡像是可以被打上不同的 tag 的。我們來看一個官方提供的例子:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test1 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
test2 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
$ docker rmi fd484f19954f
Error: Conflict, cannot delete image fd484f19954f because it is tagged in multiple repositories, use -f to force
2013/12/11 05:47:16 Error: failed to remove one or more images
$ docker rmi test1
Untagged: test1:latest
$ docker rmi test2
Untagged: test2:latest
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
$ docker rmi test
Untagged: test:latest
Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8
刪除有 tag 的鏡像時,會先有 untag 的操作。如果刪除的鏡像還有其他 tag,必須先把所有的 tag 刪除后才能繼續,當然你也可以使用 -f
參數來強制刪除。
另外一個要注意的是:如果一個鏡像有很多層,并且中間層沒有被引用,那么在刪除這個鏡像的時候,所有沒有被引用的鏡像都會被刪除。
docker 1.10 的新變化
docker 鏡像的 uuid 是怎么生成的?
在 1.10 之前,docker 鏡像的 uuid 是隨機生產的;在 1.10 引入了 Content addressable storage 的概念,uuid 是通過 SHA256 hash 算法生產的,主要好處有兩點:可以作為鏡像內容的驗證,不同鏡像可以共享鏡像層。需要注意的是:容器的 uuid 還是隨機生成的,因為容器不存在共享的情況。
image 的存儲
上面講到的鏡像存儲方式在 1.10 版本之前是正確的,但是 docker 1.10 引入了新的方式。所以 docker image id 和 aufs 的文件目錄的名字不是對應的!