前陣子因為工作需要,研究了一下所謂 Docker build 的 3.0 版本 —— docker buildx build
,希望能找出其與 docker build
背後的差異
docker buildx
指令是使用 Moby’s BuildKit 這套工具來代理 building,所以以下會用 BuildKit 來指稱新的建構模式
直接下結論
- BuildKit 須要透過一個前端服務去轉換 Dockerfile (或其他語言定義的建構流程),再交給 LLB 處理
- 即使是同一個 Dockerfile,docker build/buildx 兩者建構出的 layer 完全不同,無法共享
- 如果你建構很複雜,已經有在使用 multi-stage build(例如須要先編譯成二進制檔案),那會比較有幫助,如果是要 build 類似 python 這種直譯式的應用程式,因為流程較為單純所以幫助不大
原理
要了解背後實作的差異,會需要知道幾個背景知識:
- Docker、BuildKit 跟 containerd 的關係
- Overlay Filesystem
- containerd 的 snapshotter
Docker、BuildKit 跟 containerd 的關係
containerd 原本是 Docker 的底層服務,用來:
* 管理容器的 content (包含 filesystem、config、metadata、image)
* 負責 container 的啟動、刪除(透過 runc
調用 kernel)
containerd 後來從 Docker 抽離出來,變成專門管理容器的服務,且支援公開協議 Container Runtime Interface(CRI),其他上層的實現可以直接拿來用,例如 Kubernetes 就是使用者之一
BuildKit 就作用於 Docker 與 containerd 之間,為了取代 docker build
而生
Overlay Filesystem
overlay 是 containerd 預設的 storage driver,背後透過 union mount
,讓 container 可以共享 filesystem,以節省空間並加快容器的啟動速度
若在 upper layer 更動了 lower layer 的檔案,會先做 copy-on-write
,將變動的檔案先複製一份到 upper layer,並確保 lower layer 的檔案都是唯讀的
containerd 的 snapshotter
snapshot 跟 layer 一樣,都是屬於 containerd 的 content,要理解 snapshot 要先知道 layer 怎麼被儲存的
先來看一下映像檔(.json)所描述的 layers:
1 2 3 4 5 6 7 8 9 10 |
"RootFS": { "Type": "layers", "Layers": [ "sha256:24302eb7d9085da80f016e7e4ae55417e412fb7e0a8021e95e3b60c67cde557d", "sha256:25ece83ddc8aa2e3aed147f65c223cfe6eede2e99852af949144e0bc92fe720c", "sha256:d520722da3f37c656c0d1730ecbb2c935be859a26f1d5725cb9bed839fa58804", "sha256:9045588752f71bdd537f2d634818739acf9d275e86ea2479a2139a19a1847079" ] }, |
每一層 layer 的 hash 如 24302eb7d9085da80f016e7e4ae55417e412fb7e0a8021e95e3b60c67cde557d
都是指向一個「未壓縮的 tar」,可以把他看作一個檔案系統的目錄,存放在 /var/lib/containerd/io.containerd.content.v1.content/blobs/sha256
,而這些目錄全都是唯讀的
如果都是唯讀,那為什麼在容器中可以做檔案寫入?
這邊就會需要 snapshot 了,snapshot 把實體的 layer 抽象出來,提供一個可讀寫檔案的介面
首先 containerd 會幫每個 layer 做一份快照,每個快照都會指向另一個 parent 快照,是一個 parent-pointer-tree 的資料結構
做快照的方式(很像 git 的快照,可以一起理解):
1 2 3 4 5 6 7 8 9 10 11 |
# 準備一份快照,狀態為 active $ ctr snapshot prepare active0 # 將目標檔案系統掛載到快照上 $ ctr snapshot mount <dir> active0 # 建立或變更檔案,這邊跟 git add 是一樣道理 $ touch <dir>/hello # 取消掛載 $ umount <dir> # 提交快照,狀態為 committed,提交後不能再變更,就跟 git commit 一樣道理 $ ctr snapshot commit snapshot0 active0 |
當要啟動一個容器時,containerd 就會根據 image json 定義的 layers,準備好這一連串的 snapshots,然後在最上層疊加一個「active 的快照」,專門給這個容器用,讓容器的檔案系統變成是可寫的,不再是 immutable
想像一下你可以對一個 active 的快照做各種掛載,且一樣具備 copy-on-write
的機制,還能隨意操作不用擔心會影響到原始檔案,雖然我不是 snapshot 專家,但這聽起來很不錯對吧?
總結
snapshot 是 layer 的抽象界面,其背後真實的檔案系統可以是 overlay
、vfs
、btrfs
等
利用 snapshot 的樹狀結構,BuildKit 可以更有彈性地去建構 content 的依賴關係,而不用受限於傳統 docker build
只有一維陣列的依賴關係