iT邦幫忙

2022 iThome 鐵人賽

DAY 9
1
DevOps

那些關於 docker 你知道與不知道的事系列 第 9

Day 09: 所以,到底什麼是 image?

  • 分享至 

  • xImage
  •  

標題命名無能,沒有存稿跟規劃的結果就是到現在還在 image...在前面幾天也有提出一些疑問,其實透過實驗陸續都可以觀察到解答,但有的並沒有明確地說明,這篇就讓我們來整理一下這些問題的答案吧。

先整理一下我們目前對 docker image 的認識:

  1. docker image 是把應用程式、相依的函式庫/套件,以及應用程式執行時所需的系統環境給打包起來,用這個 image 啟動的 container 就會有所有需要的資源。
  2. docker image 並不是扁平的,他是以 layer 的方式設計的,並且用 UnionFS 的方式讓這些不同的 layers 看起來像是一個同一個檔案目錄。
  3. docker 推薦的 storage driver 是 overlay2,在讀寫檔案的時候,上層的檔案會隱藏下層的檔案,要修改檔案的話,會用 copy-on-write 策略,刪除來自下層的檔案時也不是真的刪除,而是在上層建立一個 whiteout 檔案。

這邊再補充一下 overlay2 與 docker image/container 的對照,讓我們回到在 Day 05 啟動的那個 nginx-a container,我們當時把他 commit 成一個新的 image mynginx:a 後,對這個 image 做了一些觀察:

$ docker image inspect --format='{{json .GraphDriver}}' mynginx:a | jq
{
  "Data": {
    "LowerDir": "...略",
    "MergedDir": "/var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/merged",
    "UpperDir": "/var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/diff",
    "WorkDir": "/var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/work"
  },
  "Name": "overlay2"
}

之前我們把焦點放在 LowerDirUpperDir 上,現在我們來看看 MergedDir,透過對 OverlayFS 的了解,我們現在已經知道 merged 檔案夾,其實就是所有 lower 與 upper 層的聯合目錄,讓我們看一下 mynginx:a 這個 image 的聯合目錄裡有哪些東西:

$ ls /var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/merged
ls: cannot access '/var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/merged': No such file or directory

$ ls /var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20
committed  diff  link  lower  work

$ ls /var/lib/docker/overlay2/e6f2ed0e6d55115918467a74b4dfab3f3423c346cccdbe68763de8727061bf20/diff
a.txt  etc  root  run  var

咦,並沒有 merged 這個檔案夾喔,裡頭只有 diff, link, lower,... 等檔案夾,就是沒有 merged。如果去查看一下 diff 檔案夾,可以發現這檔案夾裡的內容,是我們在 commit 前,對 nginx-a container 所做的「異動」。

現在讓我們回到 container 身上來,一樣用 inspect 觀察一下:

$ docker container inspect --format='{{json .GraphDriver}}' nginx-a  | jq
{
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f-init/diff:...略",
    "MergedDir": "/var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f/merged",
    "UpperDir": "/var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f/diff",
    "WorkDir": "/var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f/work"
  },
  "Name": "overlay2"
}

一樣讓我們去看一下 MergedDir:

$ ls /var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f/merged
a.txt  boot  docker-entrypoint.d   etc   lib    media  opt   root  sbin  sys  usr
bin    dev   docker-entrypoint.sh  home  lib64  mnt    proc  run   srv   tmp  var

這邊可以看到,這裡有「完整」的目錄。

看一下 diff 檔案夾:

ls /var/lib/docker/overlay2/93cab3c6f70cdd4ed919d12d9d98d1e1a18354dd110fede29b77697e4d28c00f/diff
a.txt  etc  root  run  var

這裡有在這個 container 中做的異動。


從以上的觀察加上官方文件的資料,這邊可以試著回答「分層是怎麼分的?commit 又是做了什麼事?」這個問題了,透過之前的實驗,我們可以發現每一層其實都是對下層所做的「異動」。

當我們用 nginx 這個 image 啟動一個 container 時,會將 nginx 這個 image 的每一層都作為 overlay2 的 lowerdir,在 docker 中稱為 image layer,僅唯讀,不可寫。然後在加上新增加的一層 layer 作為 upperdir,這在 docker 中稱為 container layer,是一個「可寫層」。然後將這些 lowerdirupperdir 聯合起來在 merged 中呈現,當進入 container 時,看到的就是 merged 中的樣子。

https://ithelp.ithome.com.tw/upload/images/20220924/20151857HJMZGXFMn3.png

當我們嘗試增加新的檔案時,是在這層 container layer 中建立的。當我們嘗試要修改來自 lowerdir 裡的檔案時,就會進行 copy_up 操作,將檔案複製到 container layer (即為upperdir) 中來讀寫,刪除 lowerdir 裡的檔案時,也是在 container layer 這層建立一個同檔名的 whiteout 檔案來阻擋對下層檔案的存取。而針對 container layer 這層所有的這些變化,都是存在這層中 的 diff 檔案夾中,由於在 container 裡看到的是聯合了 lower 與 upper 的結果,所以所有這些在 upper 層的操作,自然都會在 merged 中可見。

這層「可寫層」可以說是 image 與 container 最大的差異了,現在應該也可以知道為什麼 lowerdir 都是唯讀的了,對吧,我們所有的操作都是發生在 container layer (upperdir) 中,也因此會說 container layer 是可寫層。這層可寫層可不是持久化的,當我們移除這個 container 時,這層可寫層就會被移掉了,也就是對這個 container 所做的所有操作都會消失。

如果想要讓這些操作可以被留下來,那我們就需要把這個 container commit 成 image,所以我們可以說 commit 其實只是把這層可寫層給持久化保留下來,讓下次要用時,就可以作為下層一起被聯合起來,這樣在新的 container 中,就能看到該層裡的異動了。

到這裡,也不難推想為什麼 image 中的 merged 檔案夾是不存在的了,image 是作為每一個 container 可寫層的永久保存,並不會被「操作」,因此作為 image 時,自然也不需要去「聯合」所謂的上下層,只需要好好的保存好我自己這層的異動即可。但當 image 執行成 container 時,我們就會開始在 container 讀寫檔案,自然也就需要把這些不同層的檔案目錄依照順序聯合起來,使其看起來像是一個完整系統。當你用 docker stop 停止 container,但先不要刪除,去 ls 他的目錄,你會發現,雖然 container 還在(只是停掉了),merged 檔案夾一樣會不見了,同理,當 container 在停止中時,也不是一個可以操作的狀態,所以也不需要去聯合那些檔案夾,如果再把這個 container 執行回來,那 merged 檔案夾就又會再回來。


現在我們知道 image 是怎麼設計的了,但為什麼要這樣做呢?為什麼要讓寫入都發生在最上層的 container layer 呢?回到我們的初衷,我們一開始為什麼會討論到分層呢?我們一開始的問題是這樣的「如果有兩個類似環境的應用程式,例如有兩個不同的 Node.js 程式要用 container 來執行,他們都需要 Node.js 16 這個版本的執行環境,那會不會包了兩個很大的 image 呢?」

答案是不會,因為我們讓底層那些相同的環境都透過 layer 來共用了,對於每一個 container,僅需要管理好自己這個 container 的異動即可,即便是 commit 成一個新的 image,那也僅會保存這個 container 在可寫層異動的那些東西,並不會把底層的東西再重複保存一份起來。

https://ithelp.ithome.com.tw/upload/images/20220924/20151857fAJjHaxC0z.png

既然要共用,那底層那些 layer,就不該是任何一個 container 可以去修改的,每一個 container 的修改都保持在自己的可寫層中,這樣就不會互相影響啦。道理不難,但真的設計並實作出來,還是真心覺得 docker 很厲害呢!


上一篇
Day 08: 什麼是 copy-on-write 跟 whiteout?
下一篇
Day 10: 什麼是 namespace?
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言