(翻譯)docker 映象中有什麼?
這是一個很好的問題,在你知道答案之前, docker
映象看起來很神祕。
我不僅想告訴你答案,還想告訴你我是如何找到這個答案的。
一、從 Dockerfile 到映象
讓我們從頭開始,希望你熟系 Dockerfile
- 關於docker如何為您構建映象的說明檔案。
下面是一個簡單的示例:
FROM ubuntu:15.04 COPY app.py /app/ CMD python /app/app.py
其中每一行都是說明 docker
如何建立映象。
它將使用 ubuntu:15.04
為基礎映象,然後複製 python
指令碼, CMD
指令是在執行容器(將映象裝換為正在執行的過程)做什麼,因此該指令在構建階段不相關。
讓我們執行 docker build .
並檢查輸出。
$ docker build -t my_test_image . Sending build context to Docker daemon364.2MB Step 1/3 : FROM ubuntu:15.04 ---> d1b55fd07600 Step 2/3 : COPY app.py /app/ ---> 44ab3f1d4cd6 Step 3/3 : CMD python /app/app.py ---> Running in c037c981012e Removing intermediate container c037c981012e ---> 174b1e992617 Successfully built 174b1e992617 Successfully tagged my_test_image:latest
看到最後兩行,我們已經成功構建了一個 docker
映象,我們可以通過識別符號來引用它(這個是映象的 sha256
值)。
我們有一個最終映象,但是各個步驟的 id 是多少? d1b55fd07600
和 44ab3f1d4cd6
?它們是映象?事實上它們也是一個映象。
試想一下我們把 Step 2( COPY app.py /app/
)從 Dockerfile
中刪除掉,依舊能夠構建出一個映象(忽略 CMD 缺失 app.py
檔案而執行失敗)。因此在構建映象的每一步驟中都會有一個映象生成。
這告訴我們映象可以構建在上一個映象上,這從 Dockerfile
的 FROM
指令中也可以看出來。
映象的結構必須以這樣的方式來組織,但是為什麼?我們需要把映象檔案進行解包進行解析。
二、匯出映象並解包
為了方便解析,我們可以匯出一個映象到檔案,使得我們能夠檢視映象檔案中的內容。
docker save my_test_image > my_test_image
而匯出的檔案是……
$ file my_test_image my_test_image: POSIX tar archive
是一個 tar
檔案,內部可能包含檔案或資料夾。讓我們解壓看看。
$ mkdir unpacked_image $ tar -xvf my_test_image -C unpacked_image x 174b1e9926177b5dfd22981ddfab78629a9ce2f05412ccb1a4fa72f0db21197b.json x 28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114/ x 28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114/VERSION x 28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114/json x 28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114/layer.tar x 4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e/ x 4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e/VERSION x 4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e/json x 4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e/layer.tar x 6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373/ x 6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373/VERSION x 6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373/json x 6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373/layer.tar x c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c/ x c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c/VERSION x c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c/json x c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c/layer.tar x cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb/ x cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb/VERSION x cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb/json x cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb/layer.tar x manifest.json x repositories
我們開始檢查 manifest.json
的內容吧。
[ { "Config": "174b1e9926177b5dfd22981ddfab78629a9ce2f05412ccb1a4fa72f0db21197b.json", "RepoTags": [ "my_test_image:latest" ], "Layers": [ "cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb/layer.tar", "28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114/layer.tar", "4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e/layer.tar", "c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c/layer.tar", "6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373/layer.tar" ] } ]
manifest.json
是描述該映象的元資料。我們可以看到映象有一個標籤 my_test_image
,它有一個叫做 Layers
的東西和一個叫做 Config
。
Config
的值前 12 位與我們在 docker 構建時看到的 id 相同,我想這不是巧合。
$ cat 174b1e9926177b5dfd22981ddfab78629a9ce2f05412ccb1a4fa72f0db21197b.json
{ "architecture": "amd64", "config": { "Hostname": "d2d404286fc4", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/sh", "-c", "python /app/app.py" ], "ArgsEscaped": true, "Image": "sha256:44ab3f1d4cd69d84c9c67187b378b1d1322b5fddf4068c11e8b11856ced7efc0", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": null }, "container": "c037c981012e8f03ac5466fcdda8f78a14fb9bb5ee517028c66915624a5616fa", "container_config": { "Hostname": "d2d404286fc4", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "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/sh\" \"-c\" \"python /app/app.py\"]" ], "ArgsEscaped": true, "Image": "sha256:44ab3f1d4cd69d84c9c67187b378b1d1322b5fddf4068c11e8b11856ced7efc0", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": {} }, "created": "2018-11-01T03:19:16.8517953Z", "docker_version": "18.09.0-ce-beta1", "history": [ { "created": "2016-01-26T17:48:17.324409116Z", "created_by": "/bin/sh -c #(nop) ADD file:3f4708cf445dc1b537b8e9f400cb02bef84660811ecdb7c98930f68fee876ec4 in /" }, { "created": "2016-01-26T17:48:31.377192721Z", "created_by": "/bin/sh -c echo '#!/bin/sh' > /usr/sbin/policy-rc.d \t&& echo 'exit 101' >> /usr/sbin/policy-rc.d \t&& chmod +x /usr/sbin/policy-rc.d \t\t&& dpkg-divert --local --rename --add /sbin/initctl \t&& cp -a /usr/sbin/policy-rc.d /sbin/initctl \t&& sed -i 's/^exit.*/exit 0/' /sbin/initctl \t\t&& echo 'force-unsafe-io' > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \t\t&& echo 'DPkg::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' > /etc/apt/apt.conf.d/docker-clean \t&& echo 'APT::Update::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' >> /etc/apt/apt.conf.d/docker-clean \t&& echo 'Dir::Cache::pkgcache \"\"; Dir::Cache::srcpkgcache \"\";' >> /etc/apt/apt.conf.d/docker-clean \t\t&& echo 'Acquire::Languages \"none\";' > /etc/apt/apt.conf.d/docker-no-languages \t\t&& echo 'Acquire::GzipIndexes \"true\"; Acquire::CompressionTypes::Order:: \"gz\";' > /etc/apt/apt.conf.d/docker-gzip-indexes" }, { "created": "2016-01-26T17:48:33.59869621Z", "created_by": "/bin/sh -c sed -i 's/^#\\s*\\(deb.*universe\\)$/\\1/g' /etc/apt/sources.list" }, { "created": "2016-01-26T17:48:34.465253028Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]" }, { "created": "2018-11-01T03:19:16.4562755Z", "created_by": "/bin/sh -c #(nop) COPY file:8069dbb6bfc301562a8581e7bbe2b7675c2f96108903c0889d258cd1e11a12f6 in /app/ " }, { "created": "2018-11-01T03:19:16.8517953Z", "created_by": "/bin/sh -c #(nop)CMD [\"/bin/sh\" \"-c\" \"python /app/app.py\"]", "empty_layer": true } ], "os": "linux", "rootfs": { "type": "layers", "diff_ids": [ "sha256:3cbe18655eb617bf6a146dbd75a63f33c191bf8c7761bd6a8d68d53549af334b", "sha256:84cc3d400b0d610447fbdea63436bad60fb8361493a32db380bd5c5a79f92ef4", "sha256:ed58a6b8d8d6a4e2ecb4da7d1bf17ae8006dac65917c6a050109ef0a5d7199e6", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", "sha256:9720cebfd814895bf5dc4c1c55d54146719e2aaa06a458fece786bf590cea9d4" ] } }
這是一個相當大的 JSON
檔案,詳細查看了你可以看到,有許多不同的元資料在其中。
特別是,有關於映象裝換為可執行的容器的元資料 - 要執行的命令( Cmd
),要新增的環境變數( Env
)。
三、映象就像洋蔥
它們都有 layers
。什麼是 layer
?我選擇了 cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb
,因為這是映象 layers
的第一個。
$ ls cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb VERSIONjsonlayer.tar
裡面有一個 layer.tar
檔案,通過檔案字尾可以得知是一個 tar
檔案,我們解包檢視一下結構。
$ tree -L 1 . ├── bin ├── boot ├── dev ├── etc ├── home ├── lib ├── lib64 ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin ├── srv ├── sys ├── tmp ├── usr └── var
這是 docker
映象的重要祕密,它由不同的檔案系統檢視組成。
這個 layer
中有著不少的東西,二進位制可檔案 /bin
,使用者共享庫 /usr/lib
,你幾乎可以看到一個標準 Ubuntu
的檔案系統。
那麼每一個 layer
又包含著什麼呢?那麼它將有幫助我們知道哪些 layer
來自基本映象,以及哪些 layer
是由我們新增的。
重複我們之前檢視映象的過程,我們可以看到 ubuntu:15.04
的所有 layers
。
cac0b96b79417d5163fbd402369f74e3fe4ff8223b655e0b603a8b570bcc76eb 28441336175b9374d04ee75fdb974539e9b8cad8fec5bf0ff8cea6f8571d0114 4631663ba627c9724cd701eff98381cb500d2c09ec78a8c58213f3225877198e c4f8838502da6456ebfcb3f755f8600d79552d1e30beea0ccc62c13a2556da9c
全部屬於 ubuntu
的基礎映象, FROM ubuntu:15.04
指令。
知道了這一點我預測屬於我們新增的最頂部的 layer
的映象 6c91b695f2ed98362f511f2490c16dae0dcf8119bcfe2fe9af50305e2173f373
應該是從 COPY app.py /app/
的指令生成的。
$ tree . └── app └── app.py
這是 layer
中的內容,而且內部的所有內容只是對檔案系統添加了 app.py
。
圖片輔助
把以上的 layers
和每個映象包含的 layers
表現為一張圖

推薦工具
四、映象是如何轉換為執行的容器呢?
我們現在已經知道一個 docker
映象內部到達包含了一些什麼,那麼 docker
又是如何把它轉換為正在執行的容器呢?
檔案系統
每個容器都有著自己的檔案系統, docker
將所有映象的 layers
獲取併合並在一起,以呈現為一個檔案系統檢視。
這種技術稱為 Union Mounting , Docker
支援 Linux
上的幾個 Union Mount File
系統,主要是 OverlayFS 和 AUFS 。
但這並非全部,容器執行時的檔案系統更改不應該在容器停止後儲存到映象。
要做到這一點的一種方法是將映象複製為一個副本,這樣容器的檔案系統就能夠和映象的分離。
但這麼做並不是很有效,作為代替( docker
中的做法)是在容器的檔案系統的最頂部新增一個支援 Read/Write
的 layer
來代替容器中的檔案系統變化。如果你需要修改下面某個映象的 layer
的檔案,則需要將該檔案複製到頂部 layer
進行修改。
這被稱之為 Copy-on-write 。當容器停止時最頂部的 layer
被丟棄。
可以從 docker文件 的圖片中看出容器最頂部的 layer
組成。

五、總結
容器的執行全過程超出了本文的範圍。在建立了檔案系統之後,除了一些配置一些後續步驟的元資料之外不會再使用到映象。
為了完整的執行容器,我們需要使用 name spaces 檢視程序的內容(檔案系統,程序,網路,使用者,配置)。
使用 Cgroups 檢視程序可以使用哪些資源(記憶體,CPU,網路,配置)。
和安全功能(Security Features)控制程序的安全限制(Capabilities, AppArmor, SELinux, Seccomp)。