Docker鏡像(image)詳解



如果曾經做過 VM 管理員,則可以把 Docker 鏡像理解為 VM 模板,VM 模板就像停止運行的 VM,而 Docker 鏡像就像停止運行的容器;而作為一名研發人員,則可以將鏡像理解為類(Class)。

首先需要先從鏡像倉庫服務中拉取鏡像。常見的鏡像倉庫服務是 Docker Hub,但是也存在其他鏡像倉庫服務。

拉取操作會將鏡像下載到本地 Docker 主機,可以使用該鏡像啟動一個或者多個容器。

鏡像由多個層組成,每層疊加之后,從外部看來就如一個獨立的對象。鏡像內部是一個精簡的操作系統(OS),同時還包含應用運行所必須的文件和依賴包。

因為容器的設計初衷就是快速和小巧,所以鏡像通常都比較小。

前面多次提到鏡像就像停止運行的容器(類)。實際上,可以停止某個容器的運行,並從中創建新的鏡像。

在該前提下,鏡像可以理解為一種構建時(build-time)結構,而容器可以理解為一種運行時(run-time)結構,如下圖所示。

鏡像與容器
鏡像和容器
上圖從頂層設計層面展示了鏡像和容器間的關系。通常使用docker container run和docker service create命令從某個鏡像啟動一個或多個容器。

一旦容器從鏡像啟動后,二者之間就變成了互相依賴的關系,並且在鏡像上啟動的容器全部停止之前,鏡像是無法被刪除的。嘗試刪除鏡像而不停止或銷毀使用它的容器,會導致出錯。
鏡像通常比較小
容器目的就是運行應用或者服務,這意味着容器的鏡像中必須包含應用/服務運行所必需的操作系統和應用文件。

但是,容器又追求快速和小巧,這意味着構建鏡像的時候通常需要裁剪掉不必要的部分,保持較小的體積。

例如,Docker 鏡像通常不會包含 6 個不同的 Shell 讓讀者選擇——通常 Docker 鏡像中只有一個精簡的Shell,甚至沒有 Shell。

鏡像中還不包含內核——容器都是共享所在 Docker 主機的內核。所以有時會說容器僅包含必要的操作系統(通常只有操作系統文件和文件系統對象)。

    提示:Hyper-V 容器運行在專用的輕量級 VM 上,同時利用 VM 內部的操作系統內核。

Docker 官方鏡像 Alpine Linux 大約只有 4MB,可以說是 Docker 鏡像小巧這一特點的比較典型的例子。

但是,鏡像更常見的狀態是如 Ubuntu 官方的 Docker 鏡像一般,大約有 110MB。這些鏡像中都已裁剪掉大部分的無用內容。

Windows 鏡像要比 Linux 鏡像大一些,這與 Windows OS 工作原理相關。比如,未壓縮的最新 Microsoft .NET 鏡像(microsoft/dotnet:latest)超過 1.7GB。Windows Server 2016 Nano Server 鏡像(microsoft/nanoserver:latest)在拉取並解壓后,其體積略大於 1GB。
拉取鏡像
Docker 主機安裝之后,本地並沒有鏡像。

docker image pull 是下載鏡像的命令。鏡像從遠程鏡像倉庫服務的倉庫中下載。

默認情況下,鏡像會從 Docker Hub 的倉庫中拉取。docker image pull alpine:latest 命令會從 Docker Hub 的 alpine 倉庫中拉取標簽為 latest 的鏡像。

Linux Docker 主機本地鏡像倉庫通常位於 /var/lib/docker/<storage-driver>,Windows Docker 主機則是 C:\ProgramData\docker\windowsfilter。

可以使用以下命令檢查 Docker 主機的本地倉庫中是否包含鏡像。

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
將鏡像取到 Docker 主機本地的操作是拉取。所以,如果讀者想在 Docker 主機使用最新的 Ubuntu 鏡像,需要拉取它。通過下面的命令可以將鏡像拉取到本地,並觀察其大小。

    提示:如果使用 Linux,並且還沒有將當前用戶加入到本地 Docker UNIX 組中,則需要在下面的命令前面添加 sudo。

Windows示例如下。

> docker image pull microsoft/powershell:nanoserver

nanoserver: Pulling from microsoft/powershell
bce2fbc256ea: Pull complete
58f68fa0ceda: Pull complete
04083aac0446: Pull complete
e42e2e34b3c8: Pull complete
0c10d79c24d4: Pull complete
715cb214dca4: Pull complete
a4837c9c9af3: Pull complete
2c79a32d92ed: Pull complete
11a9edd5694f: Pull complete
d223b37dbed9: Pull complete
aee0b4393afb: Pull complete
0288d4577536: Pull complete
8055826c4f25: Pull complete
Digest: sha256:090fe875...fdd9a8779592ea50c9d4524842
Status: Downloaded newer image for microsoft/powershell:nanoserver
>
> docker image pull microsoft/dotnet:latest

latest: Pulling from microsoft/dotnet
bce2fbc256ea: Already exists
4a8c367fd46d: Pull complete
9f49060f1112: Pull complete
0334ad7e5880: Pull complete
ea8546db77c6: Pull complete
710880d5cbd5: Pull complete
d665d26d9a25: Pull complete
caa8d44fb0b1: Pull complete
cfd178ff221e: Pull complete
Digest: sha256:530343cd483dc3e1...6f0378e24310bd67d2a
Status: Downloaded newer image for microsoft/dotnet:latest
>
> docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
microsoft/dotnet latest 831..686d 7 hrs ago 1.65 GB
microsoft/powershell nanoserver d06..5427 8 days ago 1.21 GB
就像讀者看到的一樣,剛才拉取的鏡像已經存在於 Docker 主機本地倉庫中。同時可以看到 Windows 鏡像要遠大於 Linux 鏡像,鏡像中分層也更多。
鏡像倉庫服務
Docker 鏡像存儲在鏡像倉庫服務(Image Registry)當中。

Docker 客戶端的鏡像倉庫服務是可配置的,默認使用 Docker Hub。

鏡像倉庫服務包含多個鏡像倉庫(Image Repository)。同樣,一個鏡像倉庫中可以包含多個鏡像。

可能這聽起來讓人有些迷惑,所以下圖展示了包含 3 個鏡像倉庫的鏡像倉庫服務,其中每個鏡像倉庫都包含一個或多個鏡像。

包含3個鏡像倉庫的鏡像倉庫服務
官方和非官方鏡像倉庫
Docker Hub 也分為官方倉庫(Official Repository)和非官方倉庫(Unofficial Repository)。

顧名思義,官方倉庫中的鏡像是由 Docker 公司審查的。這意味着其中的鏡像會及時更新,由高質量的代碼構成,這些代碼是安全的,有完善的文檔和最佳實踐。

非官方倉庫更像江湖俠客,其中的鏡像不一定具備官方倉庫的優點,但這並不意味着所有非官方倉庫都是不好的!非官方倉庫中也有一些很優秀的鏡像。

在信任非官方倉庫鏡像代碼之前需要我們保持謹慎。說實話,讀者在使用任何從互聯網上下載的軟件之前,都要小心,甚至是使用那些來自官方倉庫的鏡像時也應如此。

大部分流行的操作系統和應用在 Docker Hub 的官方倉庫中都有其對應鏡像。這些鏡像很容易找到,基本都在 Docker Hub 命名空間的頂層。
鏡像命名和標簽
只需要給出鏡像的名字和標簽,就能在官方倉庫中定位一個鏡像(采用“:”分隔)。從官方倉庫拉取鏡像時,docker image pull 命令的格式如下。

docker image pull <repository>:<tag>
在之前的 Linux 示例中,通過下面的兩條命令完成 Alpine 和 Ubuntu 鏡像的拉取。

docker image pull alpine:latest
docker image pull ubuntu:latest
這兩條命令從 alpine 和 ubuntu 倉庫拉取了標有“latest”標簽的鏡像。

下面來介紹一下如何從官方倉庫拉取不同的鏡像。

$ docker image pull mongo:3.3.11
//該命令會從官方Mongo庫拉取標簽為3.3.11的鏡像

$ docker image pull redis:latest
//該命令會從官方Redis庫拉取標簽為latest的鏡像

$ docker image pull alpine
//該命令會從官方Alpine庫拉取標簽為latest的鏡像
關於上述命令,需要注意以下幾點。

首先,如果沒有在倉庫名稱后指定具體的鏡像標簽,則 Docker 會假設用戶希望拉取標簽為 latest 的鏡像。

其次,標簽為 latest 的鏡像沒有什么特殊魔力!標有 latest 標簽的鏡像不保證這是倉庫中最新的鏡像!例如,Alpine 倉庫中最新的鏡像通常標簽是 edge。通常來講,使用 latest 標簽時需要謹慎!

從非官方倉庫拉取鏡像也是類似的,讀者只需要在倉庫名稱面前加上 Docker Hub 的用戶名或者組織名稱。

下面通過示例來展示如何從 tu-demo 倉庫中拉取 v2 這個鏡像,其中鏡像的擁有者是 Docker Hub 賬戶 nigelpoulton,一個不應該被信任的賬戶。

$ docker image pull nigelpoulton/tu-demo:v2
//該命令會從以我自己的 Docker Hub 賬號為命名空間的 tu-demo 庫中下載標簽為 v2 的鏡像
在之前的 Windows 示例中,使用下面的兩條命令拉取了 PowerShell 和 .NET 鏡像。

> docker image pull microsoft/powershell:nanoserver

> docker image pull microsoft/dotnet:latest
第一條命令從 microsoft/powershell 倉庫中拉取了標簽為 nanoserver 的鏡像,第二條命令從 microsoft/dotnet 倉庫中拉取了標簽為 latest 的鏡像。

如果希望從第三方鏡像倉庫服務獲取鏡像(非 Docker Hub),則需要在鏡像倉庫名稱前加上第三方鏡像倉庫服務的 DNS 名稱。

假設上面的示例中的鏡像位於 Google 容器鏡像倉庫服務(GCR)中,則需要在倉庫名稱前面加上 gcr.io,如 docker pull gcr.io/nigelpoulton/tu-demo:v2(這個倉庫和鏡像並不存在)。

可能需要擁有第三方鏡像倉庫服務的賬戶,並在拉取鏡像前完成登錄。
為鏡像打多個標簽
關於鏡像有一點不得不提,一個鏡像可以根據用戶需要設置多個標簽。這是因為標簽是存放在鏡像元數據中的任意數字或字符串。一起來看下面的示例。

在 docker image pull 命令中指定 -a 參數來拉取倉庫中的全部鏡像。接下來可以通過運行 docker image ls 查看已經拉取的鏡像。

如果使用 Windows 示例,則可以將 Linux 示例中的鏡像倉庫 nigelpoulton/tu-demo 替換為 microsoft/nanoserver。

如果拉取的鏡像倉庫中包含用於多個平台或者架構的鏡像,比如同時包含 Linux 和 Windows 的鏡像,那么命令可能會失敗。

$ docker image pull -a nigelpoulton/tu-demo

latest: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Pull complete
a3ed95caeb02: Pull complete
<Snip>
Digest: sha256:42e34e546cee61adb1...3a0c5b53f324a9e1c1aae451e9
v1: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Already exists
a3ed95caeb02: Already exists
<Snip>
Digest: sha256:9ccc0c67e5c5eaae4b...624c1d5c80f2c9623cbcc9b59a
v2: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Already exists
a3ed95caeb02: Already exists
<Snip>
Digest: sha256:d3c0d8c9d5719d31b7...9fef58a7e038cf0ef2ba5eb74c
Status: Downloaded newer image for nigelpoulton/tu-demo

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nigelpoulton/tu-demo v2 6ac21e..bead 1 yr ago 211.6 MB
nigelpoulton/tu-demo latest 9b915a..1e29 1 yr ago 211.6 MB
nigelpoulton/tu-demo v1 9b915a..1e29 1 yr ago 211.6 MB
剛才發生了如下幾件事情。

首先,該命令從 nigelpoulton/tu-demo 倉庫拉取了 3 個鏡像:latest、v1 以及 v2。

其次,注意看 docker image ls 命令輸出中的 IMAGE ID 這一列。發現只有兩個不同的 Image ID。這是因為實際只下載了兩個鏡像,其中有兩個標簽指向了相同的鏡像。

換句話說,其中一個鏡像擁有兩個標簽。如果仔細觀察會發現 v1 和 latest 標簽指向了相同的 IMAGE ID,這意味着這兩個標簽屬於相同的鏡像。

這個示例也完美證明了前文中關於 latest 標簽使用的警告。latest 標簽指向了 v1 標簽的鏡像。這意味着 latest 實際指向了兩個鏡像中較早的那個版本,而不是最新的版本!latest 是一個非強制標簽,不保證指向倉庫中最新的鏡像!

過濾 docker image ls 的輸出內容

Docker 提供 --filter 參數來過濾 docker image ls 命令返回的鏡像列表內容。

下面的示例只會返回懸虛(dangling)鏡像。

$ docker image ls --filter dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 4fd34165afe0 7 days ago 14.5MB
那些沒有標簽的鏡像被稱為懸虛鏡像,在列表中展示為<none>:<none>。

通常出現這種情況,是因為構建了一個新鏡像,然后為該鏡像打了一個已經存在的標簽。

當此情況出現,Docker 會構建新的鏡像,然后發現已經有鏡像包含相同的標簽,接着 Docker 會移除舊鏡像上面的標簽,將該標簽標在新的鏡像之上。

例如,首先基於 alpine:3.4 構建一個新的鏡像,並打上 dodge:challenger 標簽。然后更新 Dockerfile,將 alpine:3.4 替換為 alpine:3.5,並且再次執行 docker image build 命令,該命令會構建一個新的鏡像,並且標簽為 dodge:challenger,同時移除了舊鏡像上面對應的標簽,舊鏡像就變成了懸虛鏡像。

可以通過 docker image prune 命令移除全部的懸虛鏡像。如果添加了 -a 參數,Docker 會額外移除沒有被使用的鏡像(那些沒有被任何容器使用的鏡像)。

Docker 目前支持如下的過濾器。

    dangling:可以指定 true 或者 false,僅返回懸虛鏡像(true),或者非懸虛鏡像(false)。
    before:需要鏡像名稱或者 ID 作為參數,返回在之前被創建的全部鏡像。
    since:與 before 類似,不過返回的是指定鏡像之后創建的全部鏡像。
    label:根據標注(label)的名稱或者值,對鏡像進行過濾。docker image ls命令輸出中不顯示標注內容。


其他的過濾方式可以使用 reference。

下面就是使用 reference 完成過濾並且僅顯示標簽為 latest 的示例。

$ docker image ls --filter=reference="*:latest"
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 3fd9065eaf02 8 days ago 4.15MB
test latest 8426e7efb777 3 days ago 122MB

可以使用 --format 參數來通過 Go 模板對輸出內容進行格式化。例如,下面的指令將只返回 Docker 主機上鏡像的大小屬性。

$ docker image ls --format "{{.Size}}"
99.3MB
111MB
82.6MB
88.8MB
4.15MB
108MB
使用下面命令返回全部鏡像,但是只顯示倉庫、標簽和大小信息。

$ docker image ls --format "{{.Repository}}: {{.Tag}}: {{.Size}}"
dodge: challenger: 99.3MB
ubuntu: latest: 111MB
python: 3.4-alpine: 82.6MB
python: 3.5-alpine: 88.8MB
alpine: latest: 4.15MB
nginx: latest: 108MB
如果讀者需要更復雜的過濾,可以使用 OS 或者 Shell 自帶的工具,比如 Grep 或者 AWK 。
通過 CLI 方式搜索 Docker Hub
docker search 命令允許通過 CLI 的方式搜索 Docker Hub。可以通過“NAME”字段的內容進行匹配,並且基於返回內容中任意列的值進行過濾。

簡單模式下,該命令會搜索所有“NAME”字段中包含特定字符串的倉庫。例如,下面的命令會查找所有“NAME”包含“nigelpoulton”的倉庫。

$ docker search nigelpoulton
NAME DESCRIPTION STARS AUTOMATED
nigelpoulton/pluralsight.. Web app used in... 8 [OK]
nigelpoulton/tu-demo 7
nigelpoulton/k8sbook Kubernetes Book web app 1
nigelpoulton/web-fe1 Web front end example 0
nigelpoulton/hello-cloud Quick hello-world image 0
“NAME”字段是倉庫名稱,包含了 Docker ID,或者非官方倉庫的組織名稱。例如,下面的命令會列出所有倉庫名稱中包含“alpine”的鏡像。

$ docker search alpine
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
alpine A minimal Docker.. 2988 [OK]
mhart/alpine-node Minimal Node.js.. 332
anapsix/alpine-java Oracle Java 8... 270 [OK]
<Snip>
需要注意,上面返回的鏡像中既有官方的也有非官方的。讀者可以使用 --filter "is-official=true",使命令返回內容只顯示官方鏡像。

$ docker search alpine --filter "is-official=true"
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
alpine A minimal Docker.. 2988 [OK]
重復前面的操作,但這次只顯示自動創建的倉庫。

$ docker search alpine --filter "is-automated=true"
NAME DESCRIPTION OFFICIAL AUTOMATED
anapsix/alpine-java Oracle Java 8 (and 7).. [OK]
frolvlad/alpine-glibc Alpine Docker image.. [OK]
kiasaki/alpine-postgres PostgreSQL docker.. [OK]
zzrot/alpine-caddy Caddy Server Docker.. [OK]
<Snip>
關於 docker search 需要注意的最后一點是,默認情況下,Docker 只返回 25 行結果。但是,可以通過指定 --limit 參數來增加返回內容行數,最多為 100 行。
鏡像和分層
Docker 鏡像由一些松耦合的只讀鏡像層組成。如下圖所示。

Docker鏡像

Docker 負責堆疊這些鏡像層,並且將它們表示為單個統一的對象。

查看鏡像分層的方式可以通過 docker image inspect 命令。下面同樣以 ubuntu:latest 鏡像為例。

$ docker image inspect ubuntu:latest
[
{
"Id": "sha256:bd3d4369ae.......fa2645f5699037d7d8c6b415a10",
"RepoTags": [
"ubuntu:latest"

<Snip>

"RootFS": {
  "Type": "layers",
  "Layers": [
   "sha256:c8a75145fc...894129005e461a43875a094b93412",
   "sha256:c6f2b330b6...7214ed6aac305dd03f70b95cdc610",
   "sha256:055757a193...3a9565d78962c7f368d5ac5984998",
   "sha256:4837348061...12695f548406ea77feb5074e195e3",
   "sha256:0cad5e07ba...4bae4cfc66b376265e16c32a0aae9"
  ]
  }
}
]
縮減之后的輸出也顯示該鏡像包含 5 個鏡像層。只不過這次的輸出內容中使用了鏡像的 SHA256 散列值來標識鏡像層。不過,兩中命令都顯示了鏡像包含 5 個鏡像層。

docker history 命令顯示了鏡像的構建歷史記錄,但其並不是嚴格意義上的鏡像分層。例如,有些 Dockerfile 中的指令並不會創建新的鏡像層。比如 ENV、EXPOSE、CMD 以及 ENTRY- POINT。不過,這些命令會在鏡像中添加元數據。

所有的 Docker 鏡像都起始於一個基礎鏡像層,當進行修改或增加新的內容時,就會在當前鏡像層之上,創建新的鏡像層。

舉一個簡單的例子,假如基於 Ubuntu Linux 16.04 創建一個新的鏡像,這就是新鏡像的第一層;如果在該鏡像中添加 Python 包,就會在基礎鏡像層之上創建第二個鏡像層;如果繼續添加一個安全補丁,就會創建第三個鏡像層。

該鏡像當前已經包含 3 個鏡像層,如下圖所示(這只是一個用於演示的很簡單的例子)。

基於Ubuntu Linux 16.04創建鏡像

在添加額外的鏡像層的同時,鏡像始終保持是當前所有鏡像的組合,理解這一點非常重要。下圖中舉了一個簡單的例子,每個鏡像層包含 3 個文件,而鏡像包含了來自兩個鏡像層的 6 個文件。

添加額外的鏡像層后的鏡像

上圖中的鏡像層跟之前圖中的略有區別,主要目的是便於展示文件。

下圖中展示了一個稍微復雜的三層鏡像,在外部看來整個鏡像只有 6 個文件,這是因為最上層中的文件 7 是文件 5 的一個更新版本。

三層鏡像

這種情況下,上層鏡像層中的文件覆蓋了底層鏡像層中的文件。這樣就使得文件的更新版本作為一個新鏡像層添加到鏡像當中。

Docker 通過存儲引擎(新版本采用快照機制)的方式來實現鏡像層堆棧,並保證多鏡像層對外展示為統一的文件系統。

Linux 上可用的存儲引擎有 AUFS、Overlay2、Device Mapper、Btrfs 以及 ZFS。顧名思義,每種存儲引擎都基於 Linux 中對應的文件系統或者塊設備技術,並且每種存儲引擎都有其獨有的性能特點。

Docker 在 Windows 上僅支持 windowsfilter 一種存儲引擎,該引擎基於 NTFS 文件系統之上實現了分層和 CoW[1]。

下圖展示了與系統顯示相同的三層鏡像。所有鏡像層堆疊並合並,對外提供統一的視圖。

從系統角度看三層鏡像
共享鏡像層
多個鏡像之間可以並且確實會共享鏡像層。這樣可以有效節省空間並提升性能。

回顧一下之前用於拉取 nigelpoulton/tu-demo 倉庫下全部包含標簽的 docker image pull 命令(包含 -a 參數)。

$ docker image pull -a nigelpoulton/tu-demo

latest: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Pull complete
a3ed95caeb02: Pull complete
<Snip>
Digest: sha256:42e34e546cee61adb100...a0c5b53f324a9e1c1aae451e9

v1: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Already exists
a3ed95caeb02: Already exists
<Snip>
Digest: sha256:9ccc0c67e5c5eaae4beb...24c1d5c80f2c9623cbcc9b59a

v2: Pulling from nigelpoulton/tu-demo
237d5fcd25cf: Already exists
a3ed95caeb02: Already exists
<Snip>
eab5aaac65de: Pull complete
Digest: sha256:d3c0d8c9d5719d31b79c...fef58a7e038cf0ef2ba5eb74c

Status: Downloaded newer image for nigelpoulton/tu-demo

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nigelpoulton/tu-demo v2 6ac...ead 4 months ago 211.6 MB
nigelpoulton/tu-demo latest 9b9...e29 4 months ago 211.6 MB
nigelpoulton/tu-demo v1 9b9...e29 4 months ago 211.6 MB
注意那些以 Already exists 結尾的行。

由這幾行可見,Docker 很聰明,可以識別出要拉取的鏡像中,哪幾層已經在本地存在。

在本例中,Docker 首先嘗試拉取標簽為 latest 的鏡像。然后,當拉取標簽為 v1 和 v2 的鏡像時,Docker 注意到組成這兩個鏡像的鏡像層,有一部分已經存在了。出現這種情況的原因是前面 3 個鏡像相似度很高,所以共享了很多鏡像層。

如前所述,Docker 在 Linux 上支持很多存儲引擎(Snapshotter)。每個存儲引擎都有自己的鏡像分層、鏡像層共享以及寫時復制(CoW)技術的具體實現。

但是,其最終效果和用戶體驗是完全一致的。盡管 Windows 只支持一種存儲引擎,還是可以提供與 Linux 相同的功能體驗。
根據摘要拉取鏡像
咱們前面介紹了通過標簽來拉取鏡像,這也是常見的方式。但問題是,標簽是可變的!這意味着可能偶爾出現給鏡像打錯標簽的情況,有時甚至會給新鏡像打一個已經存在的標簽。這些都可能導致問題!

假設鏡像 golftrack:1.5 存在一個已知的 Bug。因此可以拉取該鏡像后修復它,並使用相同的標簽將更新的鏡像重新推送回倉庫。

一起來思考下剛才發生了什么。鏡像 golftrack:1.5 存在 Bug,這個鏡像已經應用於生產環境。如果創建一個新版本的鏡像,並修復了這個 Bug。

那么問題來了,構建新鏡像並將其推送回倉庫時使用了與問題鏡像相同的標簽!原鏡像被覆蓋,但在生產環境中遺留了大量運行中的容器,沒有什么好辦法區分正在使用的鏡像版本是修復前還是修復后的,因為兩個鏡像的標簽是相同的!

Docker 1.10 中引入了新的內容尋址存儲模型。作為模型的一部分,每一個鏡像現在都有一個基於其內容的密碼散列值。

為了討論方便,用摘要代指這個散列值。因為摘要是鏡像內容的一個散列值,所以鏡像內容的變更一定會導致散列值的改變。這意味着摘要是不可變的。這種方式可以解決前面討論的問題。

每次拉取鏡像,摘要都會作為 docker image pull 命令返回代碼的一部分。只需要在 docker image ls 命令之后添加 --digests 參數即可在本地查看鏡像摘要。

接下來通過示例進行相關演示。

$ docker image pull alpine
Using default tag: latest
latest: Pulling from library/alpine
e110a4a17941: Pull complete
Digest: sha256:3dcdb92d7432d56604d...6d99b889d0626de158f73a
Status: Downloaded newer image for alpine:latest

$ docker image ls --digests alpine
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
alpine latest sha256:3dcd...f73a 4e38e38c8ce0 10 weeks ago 4.8 MB
從上面的代碼片段中可知,Alpine 鏡像的簽名值如下。

sha256:3dcdb92d7432d56604d... 6d99b889d0626de158f73a。
現在已知鏡像的摘要,那么可以使用摘要值再次拉取這個鏡像。這種方式可以確保准確拉取想要的鏡像。

沒有原生 Docker 命令支持從遠端鏡像倉庫服務(如Docker Hub)中獲取鏡像簽名。這意味着只能先通過標簽方式拉取鏡像到本地,然后自己維護鏡像的摘要列表。鏡像摘要在未來絕對不會發生變化。

下面通過示例首先在 Docker 主機上刪除 alpine:latest 鏡像,然后顯示如何通過摘要(而不是標簽)來再次拉取該鏡像。

$ docker image rm alpine:latest
Untagged: alpine:latest
Untagged: alpine@sha256:c0537...7c0a7726c88e2bb7584dc96
Deleted: sha256:02674b9cb179d...abff0c2bf5ceca5bad72cd9
Deleted: sha256:e154057080f40...3823bab1be5b86926c6f860

$ docker image pull alpine@sha256:c0537...7c0a7726c88e2bb7584dc96
sha256:c0537...7726c88e2bb7584dc96: Pulling from library/alpine
cfc728c1c558: Pull complete
Digest: sha256:c0537ff6a5218...7c0a7726c88e2bb7584dc96
Status: Downloaded newer image for alpine@sha256:c0537...bb7584dc96
鏡像散列值(摘要)
從 Docker 1.10 版本開始,鏡像就是一系列松耦合的獨立層的集合。

鏡像本身就是一個配置對象,其中包含了鏡像層的列表以及一些元數據信息。

鏡像層才是實際數據存儲的地方(比如文件等,鏡像層之間是完全獨立的,並沒有從屬於某個鏡像集合的概念)。

鏡像的唯一標識是一個加密 ID,即配置對象本身的散列值。每個鏡像層也由一個加密 ID 區分,其值為鏡像層本身內容的散列值。

這意味着修改鏡像的內容或其中任意的鏡像層,都會導致加密散列值的變化。所以,鏡像和其鏡像層都是不可變的,任何改動都能很輕松地被辨別。

這就是所謂的內容散列(Content Hash)。

到目前為止,事情都很簡單。但是接下來的內容就有點兒復雜了。

在推送和拉取鏡像的時候,都會對鏡像層進行壓縮來節省網絡帶寬以及倉庫二進制存儲空間。

但是壓縮會改變鏡像內容,這意味着鏡像的內容散列值在推送或者拉取操作之后,會與鏡像內容不相符!這顯然是個問題。

例如,在推送鏡像層到 Docker Hub 的時候,Docker Hub 會嘗試確認接收到的鏡像沒有在傳輸過程中被篡改。

為了完成校驗,Docker Hub 會根據鏡像層重新計算散列值,並與原散列值進行比較。

因為鏡像在傳輸過程中被壓縮(發生了改變),所以散列值的校驗也會失敗。

為避免該問題,每個鏡像層同時會包含一個分發散列值(Distribution Hash)。這是一個壓縮版鏡像的散列值,當從鏡像倉庫服務拉取或者推送鏡像的時候,其中就包含了分發散列值,該散列值會用於校驗拉取的鏡像是否被篡改過。

這個內容尋址存儲模型極大地提升了鏡像的安全性,因為在拉取和推送操作后提供了一種方式來確保鏡像和鏡像層數據是一致的。

該模型也解決了隨機生成鏡像和鏡像層 ID 這種方式可能導致的 ID 沖突問題。
多層架構的鏡像
Docker 最值得稱贊的一點就是使用方便。例如,運行一個應用就像拉取鏡像並運行容器這么簡單。無須擔心安裝、依賴或者配置的問題。開箱即用。

但是,隨着 Docker 的發展,事情開始變得復雜——尤其是在添加了新平台和架構之后,例如 Windows、ARM 以及 s390x。

這是會突然發現,在拉取鏡像並運行之前,需要考慮鏡像是否與當前運行環境的架構匹配,這破壞了 Docker 的流暢體驗。

多架構鏡像(Multi-architecture Image)的出現解決了這個問題!

Docker(鏡像和鏡像倉庫服務)規范目前支持多架構鏡像。這意味着某個鏡像倉庫標簽(repository:tag)下的鏡像可以同時支持 64 位 Linux、PowerPC Linux、64 位 Windows 和 ARM 等多種架構。

簡單地說,就是一個鏡像標簽之下可以支持多個平台和架構。下面通過實操演示該特性。

為了實現這個特性,鏡像倉庫服務 API 支持兩種重要的結構:Manifest 列表(新)和 Manifest。

Manifest 列表是指某個鏡像標簽支持的架構列表。其支持的每種架構,都有自己的 Mainfest 定義,其中列舉了該鏡像的構成。

下圖使用 Golang 官方鏡像作為示例。圖左側是 Manifest 列表,其中包含了該鏡像支持的每種架構。

Manifest 列表的每一項都有一個箭頭,指向具體的 Manifest,其中包含了鏡像配置和鏡像層數據。

Golang官方鏡像

在具體操作之前,先來了解一下原理。

假設要在 Raspberry Pi(基於 ARM 架構的 Linux)上運行 Docker。

在拉取鏡像的時候,Docker 客戶端會調用 Docker Hub 鏡像倉庫服務相應的 API 完成拉取。

如果該鏡像有 Mainfest 列表,並且存在 Linux on ARM 這一項,則 Docker Client 就會找到 ARM 架構對應的 Mainfest 並解析出組成該鏡像的鏡像層加密 ID。

然后從 Docker Hub 二進制存儲中拉取每個鏡像層。

下面的示例就展示了多架構鏡像是如何在拉取官方 Golang 鏡像(支持多架構)時工作的,並且通過一個簡單的命令展示了 Go 的版本和所在主機的 CPU 架構。

需要注意的是,兩個例子都使用相同的命令 docker container run。不需要告知 Docker 具體的鏡像版本是 64 位 Linux 還是 64 位 Windows。

示例中只運行了普通的命令,選擇當前平台和架構所需的正確鏡像版本是有由 Docker 完成的。

64 位 Linux 示例如下。

$ docker container run --rm golang go version

Unable to find image 'golang:latest' locally
latest: Pulling from library/golang
723254a2c089: Pull complete
<Snip>
39cd5f38ffb8: Pull complete
Digest: sha256:947826b5b6bc4...
Status: Downloaded newer image for golang:latest
go version go1.9.2 linux/amd64

64 位 Windows 示例如下。

PS> docker container run --rm golang go version

Using default tag: latest
latest: Pulling from library/golang
3889bb8d808b: Pull complete
8df8e568af76: Pull complete
9604659e3e8d: Pull complete
9f4a4a55f0a7: Pull complete
6d6da81fc3fd: Pull complete
72f53bd57f2f: Pull complete
6464e79d41fe: Pull complete
dca61726a3b4: Pull complete
9150276e2b90: Pull complete
cd47365a14fb: Pull complete
1783777af4bb: Pull complete
3b8d1834f1d7: Pull complete
7258d77b22dd: Pull complete
Digest: sha256:e2be086d86eeb789...e1b2195d6f40edc4
Status: Downloaded newer image for golang:latest
go version go1.9.2 windows/amd64
前面的操作包括從 Docker Hub 拉取 Golang 鏡像,以容器方式啟動,執行 go version 命令,並且輸出 Go 的版本和主機 OS / CPU 架構信息。

每個示例的最后一行都展示了 go version 命令的輸出內容。可以看到兩個示例使用了完全相同的命令,但是 Linux 示例中拉取的是 linux/amd64 鏡像,而 Windows 示例中拉取的是 windows/amd64 鏡像。

所有官方鏡像都支持 Manifest 列表。但是,全面支持各種架構的工作仍在推進當中。

創建支持多架構的鏡像需要鏡像的發布者做更多的工作。同時,某些軟件也並非跨平台的。在這個前提下,Manifest 列表是可選的——在沒有 Manifest 列表的情況下,鏡像倉庫服務會返回普通的 Manifest。
刪除鏡像
當讀者不再需要某個鏡像的時候,可以通過 docker image rm 命令從 Docker 主機刪除該鏡像。其中,rm 是 remove 的縮寫。

刪除操作會在當前主機上刪除該鏡像以及相關的鏡像層。這意味着無法通過 docker image ls 命令看到刪除后的鏡像,並且對應的包含鏡像層數據的目錄會被刪除。

但是,如果某個鏡像層被多個鏡像共享,那只有當全部依賴該鏡像層的鏡像都被刪除后,該鏡像層才會被刪除。

下面的示例中通過鏡像 ID 來刪除鏡像,可能跟讀者機器上鏡像 ID 有所不同。

$ docker image rm 02674b9cb179
Untagged: alpine@sha256:c0537ff6a5218...c0a7726c88e2bb7584dc96
Deleted: sha256:02674b9cb179d57...31ba0abff0c2bf5ceca5bad72cd9
Deleted: sha256:e154057080f4063...2a0d13823bab1be5b86926c6f860
如果被刪除的鏡像上存在運行狀態的容器,那么刪除操作不會被允許。再次執行刪除鏡像命令之前,需要停止並刪除該鏡像相關的全部容器。

一種刪除某 Docker 主機上全部鏡像的快捷方式是在 docker image rm 命令中傳入當前系統的全部鏡像 ID,可以通過 docker image ls 獲取全部鏡像 ID(使用 -q 參數)。

如果是在 Windows 環境中,那么只有在 PowerShell 終端中執行才會生效。在 CMD 中執行並不會生效。

$ docker image rm $(docker image ls -q) -f
為了理解具體工作原理,首先下載一組鏡像,然后通過運行 docker image ls -q。

$ docker image rm $(docker image ls -q) -f
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:f4691c9...2128ae95a60369c506dd6e6f6ab
Deleted: sha256:bd3d4369aebc494...fa2645f5699037d7d8c6b415a10
Deleted: sha256:cd10a3b73e247dd...c3a71fcf5b6c2bb28d4f2e5360b
Deleted: sha256:4d4de39110cd250...28bfe816393d0f2e0dae82c363a
Deleted: sha256:6a89826eba8d895...cb0d7dba1ef62409f037c6e608b
Deleted: sha256:33efada9158c32d...195aa12859239d35e7fe9566056
Deleted: sha256:c8a75145fcc4e1a...4129005e461a43875a094b93412
Untagged: alpine:latest
Untagged: alpine@sha256:3dcdb92...313626d99b889d0626de158f73a
Deleted: sha256:4e38e38c8ce0b8d...6225e13b0bfe8cfa2321aec4bba
Deleted: sha256:4fe15f8d0ae69e1...eeeeebb265cd2e328e15c6a869f

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
可以看到 docker image ls -q 命令只返回了系統中本地拉取的全部鏡像的 ID 列表。將這個列表作為參數傳給 docker image rm會刪除本地系統中的全部鏡像。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM