<bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>

    從零開始寫 Docker(六)---實現 mydocker run -v 支持數據卷掛載

    volume-by-bind-mount.png

    本文為從零開始寫 Docker 系列第六篇,實現類似 docker -v 的功能,通過掛載數據卷將容器中部分數據持久化到宿主機。


    完整代碼見:https://github.com/lixd/mydocker
    歡迎 Star

    推薦閱讀以下文章對 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. 概述

    上一篇中基于 overlayfs 實現了容器和宿主機文件系統間的寫操作隔離。但是一旦容器退出,容器可讀寫層的所有內容都會被刪除。

    那么,如果用戶需要持久化容器里的部分數據該怎么辦呢?

    docker volume 就是用來解決這個問題的。

    啟動容器時通過-v參數創建 volume 即可實現數據持久化。

    本節將會介紹如何實現將宿主機的目錄作為數據卷掛載到容器中,并且在容器退出后,數據卷中的內容仍然能夠保存在宿主機上。

    具體實現主要依賴于 linux 的 bind mount 功能。

    bind mount 是一種將一個目錄或者文件系統掛載到另一個目錄的技術。它允許你在文件系統層級中的不同位置共享相同的內容,而無需復制文件或數。

    例如:

    mount -o bind /source/directory /target/directory/
    

    這樣,/source/directory 中的內容將被掛載到 /target/directory,兩者將共享相同的數據。對其中一個目錄的更改也會反映到另一個目錄。

    基于該技術我們只需要將 volume 目錄掛載到容器中即可,就像這樣:

    mount -o bind /host/directory /container/directory/
    

    這樣容器中往該目錄里寫的數據最終會共享到宿主機上,從而實現持久化。


    如果你對云原生技術充滿好奇,想要深入了解更多相關的文章和資訊,歡迎關注微信公眾號。

    搜索公眾號【探索云原生】即可訂閱


    2. 實現

    volume 功能大致實現步驟如下:

    • 1)run 命令增加 -v 參數,格式個 docker 一致
      • 例如 -v /etc/conf:/etc/conf 這樣
    • 2)容器啟動前,掛載 volume
      • 先準備目錄,其次 mount overlayfs,最后 bind mount volume
    • 3)容器停止后,卸載 volume
      • 先 umount volume,其次 umount overlayfs,最后刪除目錄

    注意:第三步需要先 umount volume ,然后再刪除目錄,否則由于 bind mount 存在,刪除臨時目錄會導致 volume 目錄中的數據丟失。

    runCommand

    首先在 runCommand 命令中添 -v flag,以接收 volume 參數。

    var runCommand = cli.Command{
    	Name: "run",
    	Usage: `Create a container with namespace and cgroups limit
    			mydocker run -it [command]`,
    	Flags: []cli.Flag{
    		cli.BoolFlag{
    			Name:  "it", // 簡單起見,這里把 -i 和 -t 參數合并成一個
    			Usage: "enable tty",
    		},
    		cli.StringFlag{
    			Name:  "mem", // 限制進程內存使用量,為了避免和 stress 命令的 -m 參數沖突 這里使用 -mem,到時候可以看下解決沖突的方法
    			Usage: "memory limit,e.g.: -mem 100m",
    		},
    		cli.StringFlag{
    			Name:  "cpu",
    			Usage: "cpu quota,e.g.: -cpu 100", // 限制進程 cpu 使用率
    		},
    		cli.StringFlag{
    			Name:  "cpuset",
    			Usage: "cpuset limit,e.g.: -cpuset 2,4", // 限制進程 cpu 使用率
    		},
    		cli.StringFlag{ // 數據卷
    			Name:  "v",
    			Usage: "volume,e.g.: -v /ect/conf:/etc/conf",
    		},
    	},
    	/*
    		這里是run命令執行的真正函數。
    		1.判斷參數是否包含command
    		2.獲取用戶指定的command
    		3.調用Run function去準備啟動容器:
    	*/
    	Action: func(context *cli.Context) error {
    		if len(context.Args()) < 1 {
    			return fmt.Errorf("missing container command")
    		}
    
    		var cmdArray []string
    		for _, arg := range context.Args() {
    			cmdArray = append(cmdArray, arg)
    		}
    
    		tty := context.Bool("it")
    		resConf := &subsystems.ResourceConfig{
    			MemoryLimit: context.String("mem"),
    			CpuSet:      context.String("cpuset"),
    			CpuCfsQuota: context.Int("cpu"),
    		}
    		log.Info("resConf:", resConf)
    		volume := context.String("v")
    		Run(tty, cmdArray, resConf, volume)
    		return nil
    	},
    }
    

    在 Run 函數中,把 volume 傳給創建容器的 NewParentProcess 函數和刪除容器文件系統的 DeleteWorkSpace 函數。

    func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume string) {
    	parent, writePipe := container.NewParentProcess(tty, volume)
    	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
    	}
    	// 創建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)
    	_ = parent.Wait()
    	container.DeleteWorkSpace("/root/", volume)
    }
    

    NewWorkSpace

    在原有創建過程最后增加 volume bind 邏輯:

    • 1)首先判斷 volume 是否為空,如果為空,就表示用戶并沒有使用掛載參數,不做任何處理
    • 2)如果不為空,則使用 volumeUrlExtract 函數解析 volume 字符串,得到要掛載的宿主機目錄和容器目錄,并執行 bind mount
    func NewWorkSpace(rootPath, volume string) {
    	createLower(rootPath)
    	createDirs(rootPath)
    	mountOverlayFS(rootPath)
    
    	// 如果指定了volume則還需要mount volume
    	if volume != "" {
    		mntPath := path.Join(rootPath, "merged")
    		hostPath, containerPath, err := volumeExtract(volume)
    		if err != nil {
    			log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err)
    			return
    		}
    		mountVolume(mntPath, hostPath, containerPath)
    	}
    }
    

    volumeExtract

    語法和 docker run -v 一致,兩個路徑通過冒號分隔。

    // volumeExtract 通過冒號分割解析volume目錄,比如 -v /tmp:/tmp
    func volumeExtract(volume string) (sourcePath, destinationPath string, err error) {
    	parts := strings.Split(volume, ":")
    	if len(parts) != 2 {
    		return "", "", fmt.Errorf("invalid volume [%s], must split by `:`", volume)
    	}
    
    	sourcePath, destinationPath = parts[0], parts[1]
    	if sourcePath == "" || destinationPath == "" {
    		return "", "", fmt.Errorf("invalid volume [%s], path can't be empty", volume)
    	}
    
    	return sourcePath, destinationPath, nil
    }
    

    mountVolume

    掛載數據卷的過程如下。

    • 1)首先,創建宿主機文件目錄
    • 2)然后,拼接處容器目錄在宿主機上的真正目錄,格式為:$mntPath/$containerPath
      • 因為之前使用了 pivotRoot 將$mntPath 作為容器 rootfs,因此這里的容器目錄也可以按層級拼接最終找到在宿主機上的位置。
    • 3)最后,執行 bind mount 操作,至此對數據卷的處理也就完成了。
    // mountVolume 使用 bind mount 掛載 volume
    func mountVolume(mntPath, hostPath, containerPath string) {
    	// 創建宿主機目錄
    	if err := os.Mkdir(hostPath, constant.Perm0777); err != nil {
    		log.Infof("mkdir parent dir %s error. %v", hostPath, err)
    	}
    	// 拼接出對應的容器目錄在宿主機上的的位置,并創建對應目錄
    	containerPathInHost := path.Join(mntPath, containerPath)
    	if err := os.Mkdir(containerPathInHost, constant.Perm0777); err != nil {
    		log.Infof("mkdir container dir %s error. %v", containerPathInHost, err)
    	}
    	// 通過bind mount 將宿主機目錄掛載到容器目錄
    	// mount -o bind /hostPath /containerPath
    	cmd := exec.Command("mount", "-o", "bind", hostPath, containerPathInHost)
    	cmd.Stdout = os.Stdout
    	cmd.Stderr = os.Stderr
    	if err := cmd.Run(); err != nil {
    		log.Errorf("mount volume failed. %v", err)
    	}
    }
    

    DeleteWorkSpace

    刪除容器文件系統時,先判斷是否掛載了 volume,如果掛載了則刪除時則需要先 umount volume。

    注意:一定要要先 umount volume ,然后再刪除目錄,否則由于 bind mount 存在,刪除臨時目錄會導致 volume 目錄中的數據丟失。

    func DeleteWorkSpace(rootPath, volume string) {
    	mntPath := path.Join(rootPath, "merged")
    
    	// 如果指定了volume則需要umount volume
    	// NOTE: 一定要要先 umount volume ,然后再刪除目錄,否則由于 bind mount 存在,刪除臨時目錄會導致 volume 目錄中的數據丟失。
    	if volume != "" {
    		_, containerPath, err := volumeExtract(volume)
    		if err != nil {
    			log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err)
    			return
    		}
    		umountVolume(mntPath, containerPath)
    	}
    
    	umountOverlayFS(mntPath)
    	deleteDirs(rootPath)
    }
    

    umountVolume

    和普通 umount 一致

    func umountVolume(mntPath, containerPath string) {
    	// mntPath 為容器在宿主機上的掛載點,例如 /root/merged
    	// containerPath 為 volume 在容器中對應的目錄,例如 /root/tmp
    	// containerPathInHost 則是容器中目錄在宿主機上的具體位置,例如 /root/merged/root/tmp
    	containerPathInHost := path.Join(mntPath, containerPath)
    	cmd := exec.Command("umount", containerPathInHost)
    	cmd.Stdout = os.Stdout
    	cmd.Stderr = os.Stderr
    	if err := cmd.Run(); err != nil {
    		log.Errorf("Umount volume failed. %v", err)
    	}
    }
    

    3.測試

    下面來驗證一下程序的正確性。

    掛載不存在的目錄

    第一個實驗是把一個宿主機上不存在的文件目錄掛載到容器中。

    首先還是要在 root 目錄準備好 busybox.tar,作為我們的鏡像只讀層。

    $ ls
    busybox.tar
    

    啟動容器,把宿主機的 /root/volume 掛載到容器的 /tmp 目錄下。

    root@mydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
    {"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"mkdir parent dir /root/volume error. mkdir /root/volume: file exists","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"command all is /bin/sh","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"init come on","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"Current location is /root/merged","time":"2024-01-18T16:47:29+08:00"}
    {"level":"info","msg":"Find path /bin/sh","time":"2024-01-18T16:47:29+08:00"}
    

    新開一個窗口,查看宿主機 /root 目錄:

    root@DESKTOP-9K4GB6E:~# ls
    busybox  busybox.tar  merged  upper  volume  work
    

    多了幾個目錄,其中 volume 就是我們啟動容器是指定的 volume 在宿主機上的位置。

    同樣的,容器中也多了 containerVolume 目錄:

    / # ls
    bin              dev              home             root             tmp              var
    containerVolume  etc              proc             sys              usr
    

    現在往 /tmp 目錄寫入一個文件

    / # echo KubeExplorer > tmp/hello.txt
    / # ls /tmp
    hello.txt
    / # cat /tmp/hello.txt
    KubeExplorer
    

    然后查看宿主機的 volume 目錄:

    root@mydocker:~# ls /root/volume/
    hello.txt
    root@mydocker:~# cat /root/volume/hello.txt
    KubeExplorer
    

    可以看到,文件也在。

    然后測試退出容器后是否能持久化。

    退出容器:

    / # exit
    

    宿主機中再次查看 volume 目錄:

    root@mydocker:~# ls /root/volume/
    hello.txt
    

    文件還在,說明我們的 volume 功能是正常的。

    掛載已經存在目錄

    第二次實驗是測試掛載一個已經存在的目錄,這里就把剛才創建的 volume 目錄再掛載一次:

    root@mydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
    {"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"mkdir parent dir /root/volume error. mkdir /root/volume: file exists","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"command all is /bin/sh","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"init come on","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"Current location is /root/merged","time":"2024-01-18T17:02:48+08:00"}
    {"level":"info","msg":"Find path /bin/sh","time":"2024-01-18T17:02:48+08:00"}
    

    查看剛才的文件是否存在

    / # ls /tmp/hello.txt
    /tmp/hello.txt
    / # cat /tmp/hello.txt
    KubeExplorer
    

    還在,說明目錄確實掛載進去了。

    接下來更新文件內容并退出:

    / # echo KubeExplorer222 > /tmp/hello.txt
    / # cat /tmp/hello.txt
    KubeExplorer222
    / # exit
    

    在宿主機上查看:

    root@mydocker:~# cat /root/volume/hello.txt
    KubeExplorer222
    

    至此,說明我們的 volume 功能是正常的。

    4. 小結

    本篇記錄了如何實現 mydocker run -v 參數,增加 volume 以實現容器中部分數據持久化。

    一些比較重要的點:

    首先要理解 linux 中的 bind mount 功能。

    bind mount 是一種將一個目錄或者文件系統掛載到另一個目錄的技術。它允許你在文件系統層級中的不同位置共享相同的內容,而無需復制文件或數。

    其次,則是要理解宿主機目錄和容器目錄之間的關聯關系。

    -v /root/volume:/tmp 參數為例:

    • 1)按照語法,-v /root/volume:/tmp 就是將宿主機/root/volume 掛載到容器中的 /tmp 目錄。

    • 2)由于前面使用了 pivotRoot 將 /root/merged 目錄作為容器的 rootfs,因此,容器中的根目錄實際上就是宿主機上的 /root/merged 目錄

      • 第四篇:
    • 3)那么容器中的 /tmp目錄就是宿主機上的 /root/merged/tmp 目錄。

    • 4)因此,我們只需要將宿主機/root/volume 目錄掛載到宿主機的 /root/merged/tmp 目錄即可實現 volume 掛載。

    在清楚這兩部分內容后,整體實現就比較容易理解了。


    如果你對云原生技術充滿好奇,想要深入了解更多相關的文章和資訊,歡迎關注微信公眾號。

    搜索公眾號【探索云原生】即可訂閱



    完整代碼見:https://github.com/lixd/mydocker
    歡迎 Star

    相關代碼見 feat-volume 分支,測試腳本如下:

    需要提前在 /root 目錄準備好 busybox.tar 文件,具體見第四篇第二節。

    # 克隆代碼
    git clone -b feat-volume https://github.com/lixd/mydocker.git
    cd mydocker
    # 拉取依賴并編譯
    go mod tidy
    go build .
    # 測試 查看文件系統是否變化
    ./mydocker run -it  /bin/ls
    ./mydocker run -it -v /root/volume:/tmp /bin/sh
    
    posted @ 2024-03-14 13:03  探索云原生  閱讀(256)  評論(0編輯  收藏  舉報
    免费视频精品一区二区_日韩一区二区三区精品_aaa在线观看免费完整版_世界一级真人片
    <bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>