1. Docker 鏡像
Docker 鏡像是個只讀的容器模板,它組成了 Docker 容器的靜態文件系統運行環境 rootfs,是啟動 Docker 容器的基礎。
Docker 鏡像是容器的靜態視角,容器是鏡像的運行狀態。那么,怎么構建 Docker 鏡像呢?這就不得不提 Liunx 的聯合文件系統(union filesystem)了。
1.1 聯合文件系統
聯合文件系統是實現聯合掛載技術的文件系統。聯合掛載技術可以實現在一個掛載點同時掛載多個文件系統,將掛載點的原目錄與被掛載內容進行整合,使得最終可見的文件系統包含整合之后的各層文件和目錄。
以 aufs(advanced multi layered unification filesystem) 聯合文件系統為例。首先創建 aufs 聯合文件系統如下:
root@chunqiu:~/chunqiu/docker# ls -R
.:
beautifulGirl handsomeBoy mnt
./beautifulGirl:
chunqiu_girlfriend root
./beautifulGirl/root:
baby
./handsomeBoy:
chunqiu root
./handsomeBoy/root:
baby
./mnt:
root@chunqiu:~/chunqiu/docker# mount -t aufs -o dirs=./beautifulGirl/:./handsomeBoy/ none ./mnt
root@chunqiu:~/chunqiu/docker# df -hT | grep aufs
none aufs 150G 143G 840M 100% /root/chunqiu/docker/mnt
root@chunqiu:~/chunqiu/docker# ls -R ./mnt/
./mnt/:
chunqiu chunqiu_girlfriend root
./mnt/root:
baby
可以看到,目錄 beautifulGirl 和 handsomeBoy 的內容被聯合掛載到 mnt 目錄下,修改 mnt 目錄下的文件:
root@chunqiu:~/chunqiu/docker/mnt# echo 'who?' > chunqiu_girlfriend
root@chunqiu:~/chunqiu/docker/mnt# echo 'chunqiu' > chunqiu
root@chunqiu:~/chunqiu/docker/mnt# echo 'who?' > root/baby
root@chunqiu:~/chunqiu/docker# ls -R ./beautifulGirl/ handsomeBoy/
./beautifulGirl/:
chunqiu chunqiu_girlfriend root
./beautifulGirl/root:
baby
handsomeBoy/:
chunqiu root
handsomeBoy/root:
baby
root@chunqiu:~/chunqiu/docker# cat beautifulGirl/chunqiu beautifulGirl/chunqiu_girlfriend
chunqiu
who?
root@chunqiu:~/chunqiu/docker# cat handsomeBoy/chunqiu
root@chunqiu:~/chunqiu/docker# cat beautifulGirl/root/baby
who?
root@chunqiu:~/chunqiu/docker# cat handsomeBoy/root/baby
看起來很奇怪,修改 mnt 下 chunqiu 的內容會將改動寫到 beautifulGirl 目錄下,而修改 root 目錄下的 baby 修改只顯示在 beautifulGirl 目錄下。
這是因為 mount aufs 命令未指定目錄的權限,默認第一個出現的目錄是可讀寫目錄,而后面出現的目錄是只讀目錄。所以,寫入文件實際上都是寫到可讀寫目錄 beautifulGirl 目錄下。
那如果刪除文件呢?這里介紹一種特殊的刪除聯合文件系統的特性,稱為 whiteout 如下:
root@chunqiu:~/chunqiu/docker/handsomeBoy# touch whiteout
root@chunqiu:~/chunqiu/docker/handsomeBoy# ls
chunqiu root whiteout
root@chunqiu:~/chunqiu/docker/mnt# ls
chunqiu chunqiu_girlfriend root whiteout
root@chunqiu:~/chunqiu/docker/mnt# rm -rf whiteout
root@chunqiu:~/chunqiu/docker/handsomeBoy# ls
chunqiu root whiteout
root@chunqiu:~/chunqiu/docker/beautifulGirl# ls -al
total 28
drwxr-xr-x 5 root root 4096 May 8 06:11 .
drwxr-xr-x 5 root root 4096 May 8 05:43 ..
-rw-r--r-- 1 root root 8 May 8 05:56 chunqiu
-rw-r--r-- 1 root root 5 May 8 05:56 chunqiu_girlfriend
drwxr-xr-x 2 root root 4096 May 8 05:44 root
-r--r--r-- 2 root root 0 May 8 05:45 .wh.whiteout
在只讀目錄 handsomeBoy 下創建文件 whiteout,這個文件被映射到 mnt 目錄下。在 mnt 目錄下刪除該文件,會發現 handsomeBoy 下這個文件還是存在(因為它是只讀目錄),而在可讀寫目錄 beautifulGril 下多了個隱藏文件 .wh.whiteout。這就是 whiteout 的特性,它是上層目錄覆蓋下層相同名字目錄,用於隱藏低層分支的機制。
這里簡要介紹了聯合文件系統,可以發現它將目錄以層級的形式表現出來。相比於聯合文件系統,容器文件系統利用聯合掛載技術將可讀寫層(read-write layer 以及 volumes),init-layer,只讀層組合在一起呈現給容器內的進程,進程是感受不到這些層級結構的。那么,讓我們開始容器文件系統的學習吧。
1.2 Docker overlay2 最佳實踐
容器文件系統有多種存儲驅動實現方式,aufs,devicemapper,overlay,overlay2 等。這里選其中一種 overlay2 加以介紹。
在介紹 overlay2 之前需要先介紹下 docker 鏡像相關概念,理解它們是后續介紹的基礎:
- registry/repository: registry 是 repository 的集合,repository 是鏡像的集合。
- image:image 是存儲鏡像相關的元數據,包括鏡像的架構,鏡像默認配置信息,鏡像的容器配置信息等等。它是“邏輯”上的概念,並無物理上的鏡像文件與之對應。
- layer:layer(鏡像層) 組成了鏡像,單個 layer 可以被多個鏡像共享。
使用 docker info 命令查看宿主機上使用的存儲驅動是否是 overlay2 (配置 overlay2 可看 這里):
[root@k8s-master-node-1 centos]# docker info | grep overlay
Storage Driver: overlay2
宿主機上已經配置好了 overlay2 存儲驅動,使用 docker pull 下載 ubuntu 鏡像:
[root@k8s-master-node-1 overlay2]# docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
345e3491a907: Pull complete
57671312ef6f: Pull complete
5e9250ddb7d0: Pull complete
Digest: sha256:cf31af331f38d1d7158470e095b132acd126a7180a54f263d386da88eb681d93
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
可以看到 ubuntu 鏡像分為三層,那怎么找到這三層呢?
首先查看 ubuntu 鏡像:
[root@k8s-master-node-1 centos]# docker image ls | grep ubuntu
ubuntu latest 7e0aa2d69a15 2 weeks ago 72.7MB
鏡像的短 ID 為 7e0aa2d69a15,通過它可以查找到鏡像的三層結構。查看目錄:
[root@k8s-master-node-1 centos]# cd /var/lib/docker/image/overlay2/
distribution/ imagedb/ layerdb/ repositories.json
這個目錄是查找的入口,非常重要。它存儲了鏡像管理的元數據。其中, repositories.json 記錄了 repo 與鏡像 ID 的映射關系。imagedb 記錄了鏡像架構,操作系統,構建鏡像的容器 ID 和配置以及 rootfs 等信息。layerdb 記錄了每層鏡像層的元數據。
通過短 ID 查找 repositories.json 文件,找到鏡像 ubuntu 的長 ID,通過長 ID 在 imagedb 中找到該鏡像的元數據:
[root@k8s-master-node-1 overlay2]# cat repositories.json | grep 7e0aa2d69a15
...
{"ubuntu:latest":"sha256:7e0aa2d69a153215c790488ed1fcec162015e973e49962d438e18249d16fa9bd"}
[root@k8s-master-node-1 overlay2]# cat imagedb/content/sha256/7e0aa2d69a153215c790488ed1fcec162015e973e49962d438e18249d16fa9bd
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439",
"sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107",
"sha256:2f140462f3bcf8cf3752461e27dfd4b3531f266fa10cda716166bd3a78a19103"]
}
...
這里僅保留我們想要的元數據 rootfs。在 rootfs 中看到 layers 有三層,這三層即對應鏡像的三層鏡像層。並且,自上而下分別映射到容器的底層到頂層。找到了鏡像的三層,接下來的問題是每層的文件內容在哪里呢?
layerdb 元數據會給我們想要的信息,通過底層 diff-id: ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439 我們查到最底層鏡像層的 cache_id,通過 cache_id 即可查找到鏡像層的文件內容:
[root@k8s-master-node-1 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439]# ls
cache-id diff size tar-split.json.gz
[root@k8s-master-node-1 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439]# cat cache-id
1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7
[root@k8s-master-node-1 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439]# cat diff
sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439
[root@k8s-master-node-1 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439]# pwd
/var/lib/docker/image/overlay2/layerdb/sha256/ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439
// 使用 cacheID 查找文件內容
[root@k8s-master-node-1 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439]# cd /var/lib/docker/overlay2/1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7/
[root@k8s-master-node-1 1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7]# ls
committed diff link
[root@k8s-master-node-1 1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7]# cd diff/
[root@k8s-master-node-1 diff]# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
[root@k8s-master-node-1 1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7]# cat link
5OLEHO4UPBPTXSVUTVZ2JB2WJR
上示例中,鏡像元數據和鏡像層內容是分開存儲的。因此通過 cache-id 我們需要到 /var/lib/docker/overlay2 目錄下查看鏡像層內容,它就存在 diff 目錄下,其中 link 存儲的是鏡像層對應的短 ID,后面會看到它的用場。
找到了鏡像層的最底層,接着查找鏡像層的“中間層”,發現在 layerdb 目錄下沒有 diff-id 63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107 的鏡像層:
[root@k8s-master-node-1 layerdb]# cd sha256/63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107
bash: cd: sha256/63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107: No such file or directory
[root@k8s-master-node-1 layerdb]#
這是因為 docker 引入了內容尋址機制,該機制會根據文件內容來索引鏡像和鏡像層。docker 利用 rootfs 中的 diff_id 計算出內容尋址的 chainID,通過 chainID 獲取 layer 相關信息,最終索引到鏡像層文件內容。
對於最底層鏡像層其 diff_id 即是 chainID。因此我們可以查找到它的文件內容。除最底層外,chainID 需通過公式 chainID(n) = SHA256(chain(n-1) diffID(n)) 計算得到,計算“中間層” chainID:
[root@k8s-master-node-1 layerdb]# echo -n "sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439 sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107" | sha256sum -
8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741 -
根據 “中間層” chainID 查找文件內容:
[root@k8s-master-node-1 8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741]# ls
cache-id diff parent size tar-split.json.gz
[root@k8s-master-node-1 8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741]# cat cache-id
4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91
[root@k8s-master-node-1 8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741]# cat diff
sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107
[root@k8s-master-node-1 8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741]# cat parent
sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439
[root@k8s-master-node-1 4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91]# ls
committed diff link lower work
[root@k8s-master-node-1 4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91]# ls diff/
etc usr var
// 鏡像層文件內容
[root@k8s-master-node-1 4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91]# cat link
GALK5TGULR45FL2NKY54EPAQ3C
// 鏡像層文件內容短 ID
[root@k8s-master-node-1 4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91]# cat lower
l/5OLEHO4UPBPTXSVUTVZ2JB2WJR
// “父”鏡像層文件內容短 ID
找到最底層文件內容和“中間層”文件內容,再去找最頂層文件內容就變的不難了,這里就不多做贅述啦~
這一節知道了如何去查找鏡像的鏡像層文件內容,那么 docker 容器是怎么將鏡像和容器結合起來的呢?為什么說“鏡像是容器的靜態視角,容器是鏡像的運行狀態”呢?接着往下看。
1.2.1 docker 容器與鏡像
通過 docker run 命令啟動一個鏡像為 ubuntu 的容器:
[root@k8s-master-node-1 centos]# docker ps | grep ubuntu
156d4506b7ae ubuntu "/bin/bash" 24 hours ago Up 23 hours great_williamson
[root@k8s-master-node-1 centos]# mount | grep overlay
overlay on /var/lib/docker/overlay2/5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d/merged type overlay
(rw,relatime,lowerdir=/var/lib/docker/overlay2/l/Q6HPGILSGOQG5JGUURP2357S4X:/var/lib/docker/overlay2/l/Y2WW3FGR4WZDFTNZTTLGI7L24E:/var/lib/docker/overlay2/l/GALK5TGULR45FL2NKY54EPAQ3C:/var/lib/docker/overlay2/l/5OLEHO4UPBPTXSVUTVZ2JB2WJR,upperdir=/var/lib/docker/overlay2/5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d/diff,workdir=/var/lib/docker/overlay2/5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d/work)
可以看到,啟動容器會 mount 一個 overlay 的聯合文件系統到容器內。這個文件系統由三層組成:
- lowerdir:只讀層,即為鏡像的鏡像層。
- upperdir:讀寫層,該層是容器的讀寫層,對容器的讀寫操作將反映在讀寫層。
- workdir: overlayfs 的內部層,用於實現從只讀層到讀寫層的 copy_up 操作。
- merge:容器內作為同一視圖聯合掛載點的目錄。
這里需要着重介紹的是容器的 lowerdir 鏡像只讀層,查看只讀層的短 ID:
Q6HPGILSGOQG5JGUURP2357S4X
Y2WW3FGR4WZDFTNZTTLGI7L24E
GALK5TGULR45FL2NKY54EPAQ3C
5OLEHO4UPBPTXSVUTVZ2JB2WJR
鏡像層只有三層這里的短 ID 卻有四個?
在 /var/lib/docker/overlay2/l 目錄下我們找到了答案:
[root@k8s-master-node-1 l]# pwd
/var/lib/docker/overlay2/l
[root@k8s-master-node-1 l]# ls -l Q6HPGILSGOQG5JGUURP2357S4X
lrwxrwxrwx 1 root root 77 May 7 08:47 Q6HPGILSGOQG5JGUURP2357S4X -> ../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff
[root@k8s-master-node-1 l]# ls -l Y2WW3FGR4WZDFTNZTTLGI7L24E
lrwxrwxrwx 1 root root 72 May 7 08:13 Y2WW3FGR4WZDFTNZTTLGI7L24E -> ../7e27874bb1acb324bf692d0fb53ad0ebaed0837cfe650eab42cd9f8c2c592c85/diff
[root@k8s-master-node-1 l]# ls -l GALK5TGULR45FL2NKY54EPAQ3C
lrwxrwxrwx 1 root root 72 May 7 08:13 GALK5TGULR45FL2NKY54EPAQ3C -> ../4d615a437c68f0853db7749bf3d7d268efaebbe045a2af4d8b8e1148fc1acd91/diff
[root@k8s-master-node-1 l]# ls -l 5OLEHO4UPBPTXSVUTVZ2JB2WJR
lrwxrwxrwx 1 root root 72 May 7 08:13 5OLEHO4UPBPTXSVUTVZ2JB2WJR -> ../1c3b24824b7026813cc6e62b1f217f5b5bf17d67c2bc30a90bc68d286348b7b7/diff
[root@k8s-master-node-1 l]# ls -R ../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff
../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff:
dev etc
../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff/dev:
console pts shm
../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff/dev/pts:
../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff/dev/shm:
../5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d-init/diff/etc:
hostname hosts mtab resolv.conf
鏡像層 Y2WW3FGR4WZDFTNZTTLGI7L24E / GALK5TGULR45FL2NKY54EPAQ3C / 5OLEHO4UPBPTXSVUTVZ2JB2WJR 分別對應鏡像的三層鏡像層文件內容,它們分別映射到鏡像層的 diff 目錄。而 Q6HPGILSGOQG5JGUURP2357S4X 映射的是容器的初始化層 init,該層內容是和容器配置相關的文件內容,它是只讀的。
啟動了容器,docker 將鏡像的內容 mount 到容器中。那么,如果在容器內寫文件會對鏡像有什么影響呢?
1.2.2 容器內寫文件
不難理解,鏡像層是只讀的,在容器中寫文件其實是將文件寫入到 overlay 的可讀寫層。
這里有幾個 case 可以測試:
- 讀寫層不存在該文件,只讀層存在。
- 讀寫層存在該文件,只讀層不存在。
- 讀寫層和只讀層都不存在該文件。
我們簡單構建一種讀寫層和只讀層都不存在的場景:
root@156d4506b7ae:/etc# touch temp.txt
root@156d4506b7ae:/etc# ls
temp.txt ...
查看讀寫層是否有該文件:
[root@k8s-master-node-1 diff]# cd /var/lib/docker/overlay2/5d0cbbdeb08f0b3087d6635f764aa51654eb6b9fbdc7265248fd9815855c2a4d/diff
[root@k8s-master-node-1 diff]# ls
etc
[root@k8s-master-node-1 diff]# ls etc/
temp.txt
1.2.3 docker commit
上節提到容器內寫文件會反映在 overlay 的可讀寫層,那么讀寫層的文件內容可以做成鏡像嗎?
可以。docker 通過 commit 和 build 操作實現鏡像的構建。commit 將容器提交為一個鏡像,build 在一個鏡像的基礎上構建鏡像。
使用 commit 將上節的容器提交為一個鏡像:
[root@k8s-master-node-1 diff]# docker commit 156d4506b7ae
sha256:71cf2c4aad14d18e9d0ee8bfb2cdd16ea5216f68c6d4d81062143fe58fbe48a4
[root@k8s-master-node-1 diff]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 71cf2c4aad14 9 seconds ago 72.7MB
image 短 ID 71cf2c4aad14 即為容器提交的鏡像,查看鏡像的 imagedb 元數據:
[root@k8s-master-node-1 diff]# cat /var/lib/docker/image/overlay2/imagedb/content/sha256/71cf2c4aad14d18e9d0ee8bfb2cdd16ea5216f68c6d4d81062143fe58fbe48a4
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439",
"sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107",
"sha256:2f140462f3bcf8cf3752461e27dfd4b3531f266fa10cda716166bd3a78a19103",
"sha256:7dd12b1505cdc6bebe28cf63d5b374890908dcc9b1a23ca4dcc21e9de033c209"]
}
...
可以看到鏡像層自上而下的前三個鏡像層 diff_id 和 ubuntu 鏡像層 diff_id 是一樣的,說明每層鏡像層可以被多個鏡像共享。而多出來的一層鏡像層內容即是上節我們寫入文件的內容:
[root@k8s-master-node-1 diff]# echo -n "sha256:3dd8c8d4fd5b59d543c8f75a67cdfaab30aef5a6d99aea3fe74d8cc69d4e7bf2 sha256:7dd12b1505cdc6bebe28cf63d5b374890908dcc9b1a23ca4dcc21e9de033c209" | sha256sum -
0f3060e8fee611c68417fecbfc52734563ddea02157eaa7624fa23043af0bfb6 -
[root@k8s-master-node-1 diff]# cd /var/lib/docker/image/overlay2/layerdb/sha256/0f3060e8fee611c68417fecbfc52734563ddea02157eaa7624fa23043af0bfb6/
[root@k8s-master-node-1 0f3060e8fee611c68417fecbfc52734563ddea02157eaa7624fa23043af0bfb6]# ls
cache-id diff parent size tar-split.json.gz
[root@k8s-master-node-1 48e27ff2ff5302bd2dfd244610a61cc5032ec88b79b0953eb2c933a1f4146a36]# ls
diff link lower work
[root@k8s-master-node-1 48e27ff2ff5302bd2dfd244610a61cc5032ec88b79b0953eb2c933a1f4146a36]# cd diff/
[root@k8s-master-node-1 diff]# ls etc/
temp.txt