從零開始寫 Docker(五)---基於 overlayfs 實現寫操作隔離

探索云原生發表於2024-03-12

isolate-write-by-overlayfs.png

本文為從零開始寫 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),也叫隱式共享,是一種對可修改資源實現高效複製的資源管理技術。

linux-ufs-edit

它的思想是,如果一個資源是重複的,但沒有任何修改,這時候並不需要立即建立一個新的資源,這個資源可以被新舊例項共享。

建立新資源發生在第一次寫操作,也就是對資源進行修改的時候。透過這種資源共享的方式,可以顯著地減少未修改資源複製帶來的消耗,但是也會在進行資源修改的時候增減小部分的開銷。

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

相關文章