本文為從零開始寫 Docker 系列第十四篇,實現容器間的 rootfs 隔離,使得多個容器間互不影響。
完整程式碼見: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. 概述
雖然在前面透過 pivotRoot、overlayfs 實現了容器和宿主機的 rootfs 隔離,但是多個容器還是共用的一個rootfs,多容器之間會互相影響。
之前容器都是用的宿主機上的 /root/merged 目錄作為自己的 rootfs,當啟動多個容器時可寫層會互相影響。
本篇透過為每個容器單獨準備一個 rootfs 來實現隔離,使得我們多個容器之間互不影響。
2. 實現
為了實現該功能,需要做以下工作:
- 修改 mydocker commit 命令,實現對不同容器進行打包映象的功能。
- 修改 mydocker run 命令,使用者可以指定不同映象,併為每個容器分配單獨的隔離檔案系統
- 根據映象名稱找到對應 tar 檔案,解壓後作為overlay 中的 lower 目錄進行掛載
- 修改 mydocker rm 命令,刪除容器時順帶刪除檔案系統
這三處調整實際上都是對宿主機上容器 rootfs 目錄的調整,把 rootfs 從原來的 /root/merged
調整為 /var/lib/mydocker/overlay2/{containerID}/merged
,這樣實現容器之間的隔離。
docker 也是使用的
var/lib/docker/overlay2/{containerID}/merged
目錄作為 rootfs.可以使用docker inspect {containerID} -f '{{json .GraphDriver}}'
命令檢視。
2.1 commit 命令更新
之前 commit 命令直接把/root/merged
目錄壓縮為 tar 作為映象,現在需要根據 containerID 以/var/lib/mydocker/overlay2/{containerID}/merged
格式來拼接目錄。
首先,在 main_command.go 檔案中修改 commitCommand,將使用者輸入引數改為 containerID 和 imageName,並呼叫 commitContainer 方法實現 commit 操作。
var commitCommand = cli.Command{
Name: "commit",
Usage: "commit container to image,e.g. mydocker commit 123456789 myimage",
Action: func(context *cli.Context) error {
if len(context.Args()) < 2 {
return fmt.Errorf("missing container name and image name")
}
containerID := context.Args().Get(0)
imageName := context.Args().Get(1)
return commitContainer(containerID, imageName)
},
}
然後 commitContainer 中調整一下壓縮路徑,根據 containerID 拼接要壓縮的目錄
var ErrImageAlreadyExists = errors.New("Image Already Exists")
func commitContainer(containerID, imageName string) error {
mntPath := utils.GetMerged(containerID)
imageTar := utils.GetImage(imageName)
exists, err := utils.PathExists(imageTar)
if err != nil {
return errors.WithMessagef(err, "check is image [%s/%s] exist failed", imageName, imageTar)
}
if exists {
return ErrImageAlreadyExists
}
log.Infof("commitContainer imageTar:%s", imageTar)
if _, err = exec.Command("tar", "-czf", imageTar, "-C", mntPath, ".").CombinedOutput(); err != nil {
return errors.WithMessagef(err, "tar folder %s failed", mntPath)
}
return nil
}
2.2 run 命令更新, 實現隔離檔案系統
run 命令改動比較大, 需要把涉及到目錄的都進行調整。
改動點:
- 1)runCommand 命令中新增 imageName 引數,讓使用者可以指定映象啟動容器
- 2)啟動容器時, rootfs 部分需要根據 containerID 拼接目錄
runCommand
runCommand 命令中新增 imageName 作為第一個引數輸入
var runCommand = cli.Command{
Action: func(context *cli.Context) error {
// 省略其他內容
// get image name
imageName := cmdArray[0]
cmdArray = cmdArray[1:]
tty := context.Bool("it")
detach := context.Bool("d")
// Run方法增加對應引數
Run(tty, cmdArray, resConf, volume, containerName, imageName)
return nil
},
}
相關方法都要增加 imageName 引數:
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume, containerName, imageName string) {
containerId := container.GenerateContainerID() // 生成 10 位容器 id
// start container
parent, writePipe := container.NewParentProcess(tty, volume, containerId, imageName)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Errorf("Run parent.Start err:%v", err)
return
}
// record container info
err := container.RecordContainerInfo(parent.Process.Pid, comArray, containerName, containerId)
if err != nil {
log.Errorf("Record container info error %v", err)
return
}
// 建立cgroup manager, 並透過呼叫set和apply設定資源限制並使限制在容器上生效
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
_ = cgroupManager.Set(res)
_ = cgroupManager.Apply(parent.Process.Pid, res)
// 在子程序建立後才能透過pipe來傳送引數
sendInitCommand(comArray, writePipe)
if tty { // 如果是tty,那麼父程序等待,就是前臺執行,否則就是跳過,實現後臺執行
_ = parent.Wait()
container.DeleteWorkSpace(containerId, volume)
container.DeleteContainerInfo(containerId)
}
}
rootfs 相關調整
rootfs 相關目錄定義成變數,並提供相應的 Get 方法,呼叫時指定 containerID 即可拿到對應目錄。
// 容器相關目錄
const (
ImagePath = "/var/lib/mydocker/image/"
RootPath = "/var/lib/mydocker/overlay2/"
lowerDirFormat = RootPath + "%s/lower"
upperDirFormat = RootPath + "%s/upper"
workDirFormat = RootPath + "%s/work"
mergedDirFormat = RootPath + "%s/merged"
overlayFSFormat = "lowerdir=%s,upperdir=%s,workdir=%s"
)
func GetRoot(containerID string) string { return RootPath + containerID }
func GetImage(imageName string) string { return fmt.Sprintf("%s%s.tar", ImagePath, imageName) }
func GetLower(containerID string) string {
return fmt.Sprintf(lowerDirFormat, containerID)
}
func GetUpper(containerID string) string {
return fmt.Sprintf(upperDirFormat, containerID)
}
func GetWorker(containerID string) string {
return fmt.Sprintf(workDirFormat, containerID)
}
func GetMerged(containerID string) string { return fmt.Sprintf(mergedDirFormat, containerID) }
func GetOverlayFSDirs(lower, upper, worker string) string {
return fmt.Sprintf(overlayFSFormat, lower, upper, worker)
}
另外則是 NewWorkSpace 和 DeleteWorkSpace 這兩個方法以及其內部的一系列方法涉及到的路徑全改成動態的,根據 containerID 進行拼接:
這裡貼一下 NewWorkSpace 和 DeleteWorkSpace 兩個方法:
// NewWorkSpace Create an Overlay2 filesystem as container root workspace
/*
1)建立lower層
2)建立upper、worker層
3)建立merged目錄並掛載overlayFS
4)如果有指定volume則掛載volume
*/
func NewWorkSpace(volume, imageName, containerName string) {
err := createLower(imageName)
if err != nil {
log.Errorf("createLower err:%v", err)
return
}
err = createUpperWorker(containerName)
if err != nil {
log.Errorf("createUpperWorker err:%v", err)
return
}
err = mountOverlayFS(containerName)
if err != nil {
log.Errorf("mountOverlayFS err:%v", err)
return
}
if volume != "" {
volumeURLs := volumeUrlExtract(volume)
if len(volumeURLs) == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
err = mountVolume(containerName, volumeURLs)
if err != nil {
log.Errorf("mountVolume err:%v", err)
return
}
} else {
log.Infof("volume parameter input is not correct.")
}
}
}
// DeleteWorkSpace Delete the OverlayFS filesystem while container exit
/*
和建立相反
1)有volume則解除安裝volume
2)解除安裝並移除merged目錄
3)解除安裝並移除upper、worker層
*/
func DeleteWorkSpace(volume, containerName string) error {
// 如果指定了volume則需要先umount volume
if volume != "" {
volumeURLs := volumeUrlExtract(volume)
length := len(volumeURLs)
if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
err := umountVolume(containerName, volumeURLs)
if err != nil {
return errors.Wrap(err, "umountVolume")
}
}
}
// 然後umount整個容器的掛載點
err := umountOverlayFS(containerName)
if err != nil {
return errors.Wrap(err, "umountOverlayFS")
}
// 最後移除相關資料夾
err = removeUpperWorker(containerName)
if err != nil {
return errors.Wrap(err, "removeUpperWorker")
}
return nil
}
至此,基本改動完成了,建立出的每個容器都會單獨在/var/lib/mydocker/overlay2/
目錄下生成一個 rootfs 目錄,這樣就避免了多個容器之間互相影響。
2.3 更新 rm 命令
之前,由於對應的檔案系統因為是共用的,所以沒有刪除, rm 命令只把容器資訊刪了,這次對 rm 命令進行調整,刪除時也把檔案系統刪了。
func removeContainer(containerId string, force bool) {
containerInfo, err := getInfoByContainerId(containerId)
if err != nil {
log.Errorf("Get container %s info error %v", containerId, err)
return
}
switch containerInfo.Status {
case container.STOP: // STOP 狀態容器直接刪除即可
// 先刪除配置目錄,再刪除rootfs 目錄
if err = container.DeleteContainerInfo(containerId); err != nil {
log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err)
return
}
container.DeleteWorkSpace(containerId, containerInfo.Volume)
case container.RUNNING: // RUNNING 狀態容器如果指定了 force 則先 stop 然後再刪除
if !force {
log.Errorf("Couldn't remove running container [%s], Stop the container before attempting removal or"+
" force remove", containerId)
return
}
log.Infof("force delete running container [%s]", containerId)
stopContainer(containerId)
removeContainer(containerId, force)
default:
log.Errorf("Couldn't remove container,invalid status %s", containerInfo.Status)
return
}
}
增加了下面這一句:
container.DeleteWorkSpace(containerId, containerInfo.Volume)
3. 測試
rootfs 調整
用 busybox.tar 映象啟動一個容器,然後檢視/var/lib/mydocker/overlay2/
目錄下是否生成對應內容。
首先在/var/lib/mydocker/image/
目錄準備好映象
root@mydocker:~# mv busybox.tar /var/lib/mydocker/image/
然後使用該映象啟動容器
root@mydocker:~/refactor-isolate-rootfs/mydocker# go build .
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name rootfs busybox top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/5341624332/lower image.tar:/var/lib/mydocker/image/busybox.tar","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/5341624332/lower,upperdir=/var/lib/mydocker/overlay2/5341624332/upper,workdir=/var/lib/mydocker/overlay2/5341624332/work /var/lib/mydocker/overlay2/5341624332/merged]","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:34:12+08:00"}
檢視容器
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 rootfs 219016 running top 2024-02-22 13:34:12
檢視/var/lib/mydocker/overlay2
目錄下是否生成對應內容
root@mydocker:/var/lib/mydocker/overlay2# cd /var/lib/mydocker/overlay2/5341624332
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls
lower merged upper work
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls lower
bin dev etc home proc root sys tmp usr var
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
bin dev etc home proc root sys tmp usr var
可以看到,在/var/lib/mydocker/overlay2/{containerID}
目錄下生成了,lower、merged、upper、work 等 overlay2 目錄。
其中 lower 中的內容由映象解壓得到,merged 則是容器 rootfs 掛載點。
然後進入容器建立檔案
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 5341624332 /bin/sh
{"level":"info","msg":"container pid:219016 command:/bin/sh","time":"2024-02-22T13:37:42+08:00"}
got mydocker_pid=219016
got mydocker_cmd=/bin/sh
/ # echo KubeExplorer > a.txt
/ # cat a.txt
KubeExplorer
接著到對應 merged 目錄檢視檔案是否存在
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
a.txt bin dev etc home proc root sys tmp usr var
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cat merged/a.txt
KubeExplorer
至此,說明 rootfs 調整一切正常。
commit 命令
接下來測試一下 mydocker commit 命令,把剛才啟動的容器提交為映象。
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 rootfs 219016 running top 2024-02-22 13:34:12
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker commit 5341624332 busybox-with-custom
{"level":"info","msg":"commitContainer imageTar:/var/lib/mydocker/image/busybox-with-custom.tar","time":"2024-02-22T13:43:33+08:00"}
然後檢視 var/lib/mydocker/image/ 目錄是否生成了對應的映象檔案
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cd /var/lib/mydocker/image/
root@mydocker:/var/lib/mydocker/image# ls
busybox-with-custom.tar busybox.tar
busybox-with-custom.tar
就是 commit 命令生成的映象。
接下來使用該映象啟動一個容器,檢視之前建立的檔案是否存在
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name rootfs2 busybox-with-custom top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/8118341786/lower image.tar:/var/lib/mydocker/image/busybox-with-custom.tar","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/8118341786/lower,upperdir=/var/lib/mydocker/overlay2/8118341786/upper,workdir=/var/lib/mydocker/overlay2/8118341786/work /var/lib/mydocker/overlay2/8118341786/merged]","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:45:53+08:00"}
進入容器檢視內容
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 rootfs 219016 running top 2024-02-22 13:34:12
8118341786 rootfs2 219109 running top 2024-02-22 13:45:53
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 8118341786 /bin/sh
{"level":"info","msg":"container pid:219109 command:/bin/sh","time":"2024-02-22T13:46:14+08:00"}
got mydocker_pid=219109
got mydocker_cmd=/bin/sh
/ # cat a.txt
KubeExplorer
可以看到,提交的映象中包含了我們新建的 a.txt 檔案,說明 commit 命令也是正常的。
rm 命令
最後測試一下 mydocker rm 命令,能否刪除映象配置和對應的 rootfs 目錄。
ps 命令拿到 id
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 rootfs 219016 running top 2024-02-22 13:34:12
8118341786 rootfs2 219109 running top 2024-02-22 13:45:53
根據 id 刪除容器
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker rm 5341624332 -f
{"level":"info","msg":"force delete running container [5341624332]","time":"2024-02-22T13:47:36+08:00"}
{"level":"info","msg":"umountOverlayFS,cmd:/usr/bin/umount /var/lib/mydocker/overlay2/5341624332/merged","time":"2024-02-22T13:47:36+08:00"}
檢視一下 /var/lib/mydocker/overlay2
中的 rootfs 目錄是否刪除
cd /var/lib/mydocker/overlay2
root@mydocker:/var/lib/mydocker/overlay2# ls
可以看到,容器相關目錄都被移除了。
4. 小結
本小節主要完善了容器的檔案系統,在/var/lib/mydocker/overlay2/
目錄下為每個容器單獨分配一個 rootfs,避免了多容器之間互相影響。
【從零開始寫 Docker 系列】持續更新中,搜尋公眾號【探索雲原生】訂閱,閱讀更多文章。
完整程式碼見:https://github.com/lixd/mydocker
歡迎關注~
相關程式碼見 refactor-isolate-rootfs
分支,測試指令碼如下:
需要提前在 /var/lib/mydocker/image 目錄準備好 busybox.tar 檔案,具體見第四篇第二節。
# 克隆程式碼
git clone -b refactor-isolate-rootfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試
./mydocker run -d -name c1 busybox top
# 檢視容器 Id
./mydocker ps
# stop 停止指定容器
./mydocker rm ${containerId} -f