Docker的鏡像基本原理和概念

這篇文章主要講講 docker 中鏡像有關的知識,將涉及到下面幾個方面:linux

  • docker images 命令的使用
  • docker 和 registry 交互的過程,pull 命令到底作了什麼
  • docker storage driver
  • aufs 的格式和實際的組織結構
  • Dockerfile 原語和 docker 鏡像之間的關係

做者:cizixs 時間:2016-04006 原文連接: cizixs.com/2016/04/06/…nginx

簡介

一圖看盡 docker 鏡像

  • docker 鏡像表明了容器的文件系統裏的內容,是容器的基礎,鏡像通常是經過 Dockerfile 生成的
  • docker 的鏡像是分層的,全部的鏡像(除了基礎鏡像)都是在以前鏡像的基礎上加上本身這層的內容生成的
  • 每一層鏡像的元數據都是存在 json 文件中的,除了靜態的文件系統以外,還會包含動態的數據

使用鏡像:docker image 命令

docker client 提供了各類命令和 daemon 交互,來完成各類任務,其中和鏡像有關的命令有:redis

  • 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 pulldocker run 都是客戶端的命令,最終這些命令會發送到 server 端(docker daemon 啓動的時候會啓動docker server)進行處理。下載鏡像還會和 Registry 打交道,下面咱們就說說使用 docker pull 的時候,docker 到底在作些什麼!docker

docker client 組織配置和參數,把 pull 指令發送給 docker server,server 端接收到指令以後會交給對應的 handler。handler 會新開一個 CmdPull job 運行,這個 job 在 docker daemon 啓動的時候被註冊進來,因此控制權就到了 docker daemon 這邊。docker daemon 是怎麼根據傳過來的 registry 地址、repo 名、image 名和tag 找到要下載的鏡像呢?具體流程以下:apache

  1. 獲取 repo 下面全部的鏡像 id:GET /repositories/{repo}/images
  2. 獲取 repo 下面全部 tag 的信息: GET /repositories/{repo}/tags
  3. 根據 tag 找到對應的鏡像 uuid,並下載該鏡像
    • 獲取該鏡像的 history 信息,並依次下載這些鏡像層: GET /images/{image_id}/ancestry
    • 若是這些鏡像層已經存在,就 skip,不存在的話就繼續
    • 獲取鏡像層的 json 信息:GET /images/{image_id}/json
    • 下載鏡像內容: GET /images/{image_id}/layer
    • 下載完成後,把下載的內容存放到本地的 UnionFS 系統
    • 在 TagStore 添加剛下載的鏡像信息

存儲鏡像:docker storage 介紹

在上一個章節提到下載的鏡像會保存起來,這一節就講講究竟是怎麼存的。json

UnionFS 和 aufs

若是對 docker 有所瞭解的話,會據說過 UnionFS 的概念,這是 docker 實現層級鏡像的基礎。在 wikipedia 是這麼解釋的:ubuntu

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.bash

簡單來講,就是用多個文件夾和文件(這些是系統文件系統的概念)存放內容,對上(應用層)提供虛擬的文件訪問。 好比 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//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//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//,好比這裏的命令主要會修改 /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 參數來強制刪除。

另一個要注意的是:若是一個鏡像有不少層,而且中間層沒有被引用,那麼在刪除這個鏡像的時候,全部沒有被引用的鏡像都會被刪除。

相關文章
相關標籤/搜索