本文為從零開始寫 Docker 系列第五篇,在 pivotRoot
基礎上透過 overlayfs 實現寫操作隔離,達到容器中寫操作和宿主機互不影響。
完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star
推薦閱讀以下文章對 docker 基本實現有一個大致認識:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基於 namespace 的檢視隔離:探索 Linux Namespace:Docker 隔離的神奇背後
- 基於 cgroups 的資源限制
- 初探 Linux Cgroups:資源控制的奇妙世界
- 深入剖析 Linux Cgroups 子系統:資源精細管理
- Docker 與 Linux Cgroups:資源隔離的魔法之旅
- 基於 overlayfs 的檔案系統:Docker 魔法解密:探索 UnionFS 與 OverlayFS
- 基於 veth pair、bridge、iptables 等等技術的 Docker 網路:揭秘 Docker 網路:手動實現 Docker 橋接網路
開發環境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 使用者
1. 概述
上一篇中已經實現了使用宿主機 /root/busybox 目錄作為容器的根目錄,但在容器內對檔案的操作仍然會直接影響到宿主機的 /root/busybox 目錄。
本節要進一步進行容器和映象隔離,實現在容器中進行的操作不會對映象(宿主機/root/busybox目錄)產生任何影響的功能。
什麼是 overlayfs?
overlayfs 是 UFS 的一種實現,UnionFS 全稱為 Union File System ,是一種為 Linux FreeBSD NetBSD 作業系統設計的,把其他檔案系統聯合到一個聯合掛載點的檔案系統服務。
它使用 branch 不同檔案系統的檔案和目錄“透明地
”覆蓋,形成一個單一一致的檔案系統。
這些 branches 或者是 read-only 或者是 read-write 的,所以當對這個虛擬後的聯合檔案系統進行寫操作的時候,系統是真正寫到了一個新的檔案中。看起來這個虛擬後的聯合檔案系統是可以對任何檔案進行操作的,但是其實它並沒有改變原來的檔案,這是因為 unionfs 用到了一個重要的資管管理技術叫寫時複製。
寫時複製(copy-on-write,下文簡稱 CoW),也叫隱式共享,是一種對可修改資源實現高效複製的資源管理技術。
它的思想是,如果一個資源是重複的,但沒有任何修改,這時候並不需要立即建立一個新的資源,這個資源可以被新舊例項共享。
建立新資源發生在第一次寫操作,也就是對資源進行修改的時候。透過這種資源共享的方式,可以顯著地減少未修改資源複製帶來的消耗,但是也會在進行資源修改的時候增減小部分的開銷。
UnionFS,最主要的功能是將多個不同位置的目錄聯合掛載(union mount)到同一個目錄下。
比如,我現在有兩個目錄 A 和 B,它們分別有兩個檔案:
$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
然後,我使用聯合掛載的方式,將這兩個目錄掛載到一個公共的目錄 C 上:
$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C
這時,我再檢視目錄 C 的內容,就能看到目錄 A 和 B 下的檔案被合併到了一起:
$ tree ./C
./C
├── a
├── b
└── x
可以看到,在這個合併後的目錄 C 裡,有 a、b、x 三個檔案,並且 x 檔案只有一份。這,就是“合併”的含義。
這就是聯合檔案系統,目的就是將多個檔案聯合在一起成為一個統一的檢視。
UFS 有多種實現,例如 AUFS、Overlayfs 等,這裡使用比較主流的 Overlayfs。
關於 Overlayfs 詳細介紹可以看一下這篇文章:Docker 魔法解密:探索 UnionFS 與 OverlayFS
裡面詳細介紹了 overlayfs 各個特性,以及 docker 中是如何使用 Overlayfs 的。
這裡對需要用到部分做簡要說明:
首先,overlayfs 一般分為 lower、upper、merged 和 work 4個目錄。
- lower 只讀層,該層資料不會被修改
- upper 可讀寫層,所有修改都發生在這一層,即使是修改的 lower 中的資料。
- merged 檢視層,可以看到 lower、upper 中的所有內容
- work 則是 overlayfs 內部使用
在本文實現中使用我們的映象目錄(busybox 目錄) 作為 lower 目錄,這樣可以保證映象內容部被修改。
merged 目錄由於可以看到全部內容,因此作為容器 rootfs 目錄,即 pivotRoot 會切換到 merged 目錄。
upper 目錄則是用於儲存容器中的修改,因為 overlayfs 中所有修改都會發生在這裡。
如果你對雲原生技術充滿好奇,想要深入瞭解更多相關的文章和資訊,歡迎關注微信公眾號。
搜尋公眾號【探索雲原生】即可訂閱
2. Mount Overlayfs
Docker 在使用映象啟動一個容器時,會新建2個layer: write layer和 container-init layer。
write layer是容器唯一的 可讀寫層;而 container-init layer 是為容器新建的只讀層,用來儲存容器啟動時傳入的系統資訊(不過在實際的場景下,它們並不是以write layer和container-init layer命名的)。最後把write layer、container-init layer 和相關映象的 layers 都 mount 到一個 mnt 目錄下,然後把這個 mnt 目錄作為容器啟動的根目錄。
同樣的,我們在容器啟動前,也需要先 mount 好 overlayfs 目錄,然後執行 privotRoot 時直接切換到 mount 好的 overlayfs merge 目錄即可。
NewWorkSpace 函式是用來建立容器檔案系統的,它包括 createLower、createDirs和mountOverlayFS。
分為以下步驟:
-
1)準備 busybox 目錄,之前都是手動解壓準備 /root/busybox 目錄,這次把解壓邏輯加入到程式碼中。只需要準備好 busybox.tar 檔案即可。容器啟動時自動將 busybox.tar 解壓到 busybox 目錄下,作為容器的只讀層。
-
2)準備 overlayfs 目錄,建立好掛載 overlayfs 需要的 upper、work 和 merged 目錄
-
3)實現 mount overlayfs,將 merged 目錄作為掛載點,然後把 busybox、upper 掛載到 merged 目錄。
-
4)更新 pivotRoot 呼叫目錄,將 rootfs 從宿主機目錄 root/busybox 切換到上一步中掛載的/root/merged 目錄
-
最後 NewParentProcess 函式中將容器使用的宿主機目錄 root/busybox 替換成/root/merged。
// NewWorkSpace Create an Overlay2 filesystem as container root workspace
func NewWorkSpace(rootPath string) {
createLower(rootPath)
createDirs(rootPath)
mountOverlayFS(rootPath)
}
// createLower 將busybox作為overlayfs的lower層
func createLower(rootURL string) {
// 把busybox作為overlayfs中的lower層
busyboxURL := rootURL + "busybox/"
busyboxTarURL := rootURL + "busybox.tar"
// 檢查是否已經存在busybox資料夾
exist, err := PathExists(busyboxURL)
if err != nil {
log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
}
// 不存在則建立目錄並將busybox.tar解壓到busybox資料夾中
if !exist {
if err := os.Mkdir(busyboxURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
}
if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", busyboxURL, err)
}
}
}
// createDirs 建立overlayfs需要的的upper、worker目錄
func createDirs(rootURL string) {
upperURL := rootURL + "upper/"
if err := os.Mkdir(upperURL, 0777); err != nil {
log.Errorf("mkdir dir %s error. %v", upperURL, err)
}
workURL := rootURL + "work/"
if err := os.Mkdir(workURL, 0777); err != nil {
log.Errorf("mkdir dir %s error. %v", workURL, err)
}
}
// mountOverlayFS 掛載overlayfs
func mountOverlayFS(rootURL string, mntURL string) {
// mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
// 建立對應的掛載目錄
if err := os.Mkdir(mntURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", mntURL, err)
}
// 拼接引數
// e.g. lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/merged
dirs := "lowerdir=" + rootURL + "busybox" + ",upperdir=" + rootURL + "upper" + ",workdir=" + rootURL + "work"
// dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "busybox"
cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", dirs, mntURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
}
接下來,在 NewParentProcess 函式中將容器使用的宿主機目錄/root/busybox 替換成root/mnt 。這樣 ,使用 OverlayFS 系統啟動容器的程式碼就完成了。
func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
// 省略其他程式碼
cmd.ExtraFiles = []*os.File{readPipe}
mntURL := "/root/merged/"
rootURL := "/root/"
NewWorkSpace(rootURL, mntURL)
cmd.Dir = mntURL
return cmd, writePipe
}
3. Unmount Overlayfs
Docker 會在刪除容器的時候,把容器對應 WriteLayer 和 Container-init Layer 刪除,而保留映象所有的內容。本節中在容器退出的時候也會刪除 upper、work 和 merged 目錄只保留作為映象的 lower 層目錄即 busybox。
具體步驟如下:
- 1)unmount overlayfs:將/root/merged目錄掛載解除
- 2)刪除其他目錄:刪除之前為 overlayfs 準備的 upper、work、merged 目錄
由於 overlayfs 的特性,所有修改操作都發生在 upper 目錄,因此目錄刪除後容器對檔案系統的更改,就都已經抹去了。
DeleteWorkSpace 函式包括 umountOverlayFS 和 deleteDirs。
// DeleteWorkSpace Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string) {
umountOverlayFS(mntURL)
deleteDirs(rootURL)
}
func umountOverlayFS(mntURL string) {
cmd := exec.Command("umount", mntURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
if err := os.RemoveAll(mntURL); err != nil {
log.Errorf("Remove dir %s error %v", mntURL, err)
}
}
func deleteDirs(rootURL string) {
writeURL := rootURL + "upper/"
if err := os.RemoveAll(writeURL); err != nil {
log.Errorf("Remove dir %s error %v", writeURL, err)
}
workURL := rootURL + "work"
if err := os.RemoveAll(workURL); err != nil {
log.Errorf("Remove dir %s error %v", workURL, err)
}
}
4. 測試
首先將busybox.tar
放到 /root 目錄下:
$ ls
busybox.tar
然後啟動我們的容器
root@mydocker:~/feat-overlayfs/mydocker# ./mydocker run -it /bin/sh
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"enter NewWorkSpace","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"enter createLower","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-16T13:36:38+08:00"}
再次檢視宿主機的 /root 目錄:
root@mydocker:~# ls /root
busybox busybox.tar merged upper work
可以看到,多了幾個目錄:busybox、merged、upper、work。
在容器中新建一個檔案:
/ # echo KubeExplorer > tmp/hello.txt
/ # ls /tmp
hello.txt
/ # cat /tmp/hello.txt
KubeExplorer
然後切換到宿主機:
root@mydocker:~# ls busybox/tmp
root@mydocker:~# ls upper/tmp
hello.txt
root@mydocker:~# ls merged/tmp
hello.txt
可以發現,這個新建立的檔案居然不在 busybox 目錄,而是在 upper 中,然後 merged 目錄中也可以看到。
這就是 overlayfs 的作用了。
寫操作不會修改 lower 目錄(busybox),而是發生在 upper 中,即在 upper 中 tmp 目錄並建立了 hello.txt 檔案。
而 merged 作為掛載點自然是能夠看到 hello.txt 檔案的。
最後在容器中執行 exit 退出容器。
/ # exit
然後再次檢視宿主機上的 root 資料夾內容。
root@mydocker:~# ls /root
busybox busybox.tar
可以看到,upper、work 和 merged 目錄被刪除,作為映象的 busybox 層仍然保留。
並且 busybox 中的內容未被修改:
root@mydocker:~# ls /root/busybox
bin dev etc home proc root sys tmp usr var
至此,基本實現了 Docker 的效果:
- 1)映象中的檔案不會被修改
- 2)容器中的修改不會影響宿主機
- 3)容器退出後,修改內容丟失
5. 小結
overlayfs 引入具體流程如下:
- 1)自動解壓 busybox.tar 到 busybox 作為 lower 目錄,類似 docker 映象層
- 2)容器啟動前準備好 lower、upper、work、merged 目錄並 mount 到 merged 目錄
- 3)容器啟動後使用 pivotRoot 將 rootfs 切換到 merged 目錄
- 後續容器中的修改由於 overlayfs 的特性,都會發生在 upper 目錄中,而不會影響到 lower 目錄
- 4)容器停止後 umount 並移除upper、work、merged 目錄
如果你對雲原生技術充滿好奇,想要深入瞭解更多相關的文章和資訊,歡迎關注微信公眾號。
搜尋公眾號【探索雲原生】即可訂閱
最後在推薦一下 Docker 魔法解密:探索 UnionFS 與 OverlayFS
完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star
相關程式碼見 feat-overlayfs
分支,測試指令碼如下:
需要提前在 /root 目錄準備好 busybox.tar 檔案,具體看上一篇文章第二節。
# 克隆程式碼
git clone -b feat-overlayfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試 檢視檔案系統是否變化
./mydocker run -it /bin/ls