手写docker—数据卷挂载(五)

数据卷挂载

上一节实现了 docker 中宿主机和容器间的写操作隔离。但是容器一旦退出,容器可读写层的所有内容都会被删除。所以需要持久化容器中的数据,实现将宿主机的目录作为数据卷挂载到容器中,这样如果容器退出,容器中的文件依然能够持久化到宿主机。

持久化操作具体依赖于 bind mount 来实现,bind mount 是一种将一个目录或者文件挂载到另一个目录的技术,它允许不同文件系统层级中不同位置的文件共享。具体使用例如:

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

这样就将 volume 目录挂载到了容器中,这样容器中往该目录写的数据最终会共享到宿主机上,从而实现持久化。

docker 数据卷挂载的执行步骤如下:

  • 1)run 命令增加 -v 参数,格式为 -v 宿主机目录:容器目录
    • 例如 -v /root/volume:/tmp
  • 2)容器启动前,挂载 volume:
    • 首先准备好宿主机和容器需要的目录,然后 mount overlayfs,最后进行 volume 数据卷挂载;
  • 3)容器停止后,卸载 volume:
    • 首先 umount volume,接着 umount overlayfs,最终删除相关目录。

具体实现

NewWorkSpace

在容器进程完成初始化操作,创建并挂载完毕 overlayfs 文件系统后,执行对 volume 数据卷挂载操作。

注意拼接出容器目录在宿主机上的真正目录,格式为 $mntPath/$containerPath。接着将宿主机上的目录挂载到容器内的目录。

/**
 * create an Overlay fileSystem as container root workingspace
 * 1)create lower-dir;
 * 2)create upper-dir、work-dir;
 * 3)create merged-dir and mount as overlayFS;
 * 4)mount volume if exists;
 */
func NewWorkSpace(rootPath string, volume string) {
    // mount overlayfs
	createLower(rootPath)
	createDirs(rootPath)
	mountOverlayfs(rootPath)
    // mount volume
	if volume != "" {
		mntPath := path.Join(rootPath, "merged")
		hostDir, containerDir, err := volumeUrlExtract(volume)
		if err != nil {
			log.Errorf("volumeDir error: %v", err)
			return
		}
		if err := mountVolume(mntPath, []string{hostDir, containerDir}); err != nil {
			log.Errorf("mountVolume err:%v", err)
			return
		}
	} else {
		log.Infof("volume can't be empty")
	}
}

/**
 * parse mount information,
 */
func volumeUrlExtract(volume string) (sourcePath string, destinationPath string, err error) {
	parts := strings.Split(volume, ":")
	if len(parts) != 2 {
		return "", "", fmt.Errorf("invalid volume [%s], split by %s", volume, ":")
	}
	if parts[0] == "" || parts[1] == "" {
		return "", "", fmt.Errorf("invalid volume [%s], path can't be empty", volume)
	}
	return parts[0], parts[1], nil
}

/**
 * mount volumes of parent-dir to container-dir
 */
func mountVolume(mntPath string, volumes []string) error {
	// create parent-dir
	parentUrl := volumes[0]
	if err := os.Mkdir(parentUrl, Perm0755); err != nil {
		log.Infof("mkdir parent dir %s failed. %v", parentUrl, err)
	}
	containerUrl := volumes[1]
	// create container's exact mount-dir $mntPath/$containerUrl
	mntUrl := path.Join(mntPath, containerUrl)
	if err := os.Mkdir(mntUrl, Perm0755); err != nil {
		log.Infof("mkdir container dir %s failed. %v", mntPath, err)
	}
	// mount parent-dir to container-dir
	cmd := exec.Command("mount", "-o", "bind", parentUrl, mntUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("mount volume failed. %v", err)
	}
	log.Infof("mountVolume from %s to %s successfully", parentUrl, mntUrl)
	return nil
}

DeleteWorkSpace

删除容器时需要判断是否挂载了 volume 数据卷,如果发现了挂载,需要执行如下操作:

  • umount volume 取消数据卷挂载;
  • umount overlayfs 取消 rootfs 联合挂载;
  • 删除目录 upper-dirmerged-dirwork-dir
/**
 * Delete overlayfs workingPlace
 * 1)uninstall volume;
 * 2)uninstall and delete merged directory;
 * 3)uninstall and delete upper-dir、work-dir;
 */
func DeleteWorkSpace(rootUrl, volume string) {
	mntPath := path.Join(rootUrl, "merged")
	if volume != "" {
		_, containerPath, err := volumeUrlExtract(volume)
		if err != nil {
			log.Errorf("extract volume failed, volume : %s", volume)
			return
		}
		if err := unmountVolume(mntPath, containerPath); err != nil {
			log.Errorf("unmountVolume %s failed", mntPath+containerPath)
			return
		}
	}

	unmountOverlayfs(mntPath)
	removeDirs(rootUrl)
}

/**
 * umount volume of parentDir
 */
func unmountVolume(mntUrl, containerPath string) error {
	containerPathInHost := path.Join(mntUrl, containerPath)
	cmd := exec.Command("umount", containerPathInHost)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return meta.NewError(meta.ErrUnMount, fmt.Sprintf("umountVolume %s failed", containerPathInHost), err)
	}
	return nil
}

/**
 * unmount merged-dir of overlayfs
 */
func unmountOverlayfs(mntUrl string) {
	cmd := exec.Command("umount", mntUrl)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("unMountDir %s failed %v", mntUrl, err)
	}
	if err := os.RemoveAll(mntUrl); err != nil {
		log.Errorf("removeMountDir %s error %v", mntUrl, err)
	}
	log.Infof("volume::unmountOverlayfs unmountFS %s successfully", mntUrl)
}

/**
 * remove directories(merged-dir、upper-dir、work-dir) but save (lower-dir)
 */
func removeDirs(rootUrl string) {
	writeDir := rootUrl + "upper/"
	if err := os.RemoveAll(writeDir); err != nil {
		log.Errorf("volume::removeDirs remove upper-dir %s failed %v", writeDir, err)
	}
	workDir := rootUrl + "work/"
	if err := os.RemoveAll(workDir); err != nil {
		log.Errorf("volume::removeDirs remove work-dir %s failed %v", workDir, err)
	}
	log.Infof("volume::removeDirs upper-dir %s work-dir %s successfully", writeDir, workDir)
}

func getMerged(path string) string {
	return fmt.Sprintf(MergedDirFormat, path)
}

数据卷挂载测试

运行容器并挂载数据卷,向数据卷所在目录创建文件并写入内容:

# 宿主机执行
[root@localhost Mydocker]# ./Mydockker run -it -v /root/volume:/tmp /bin/sh
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-02-08T01:22:44+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-02-08T01:22:44+08:00"}
{"level":"info","msg":"mkdir container dir /root/merged failed. mkdir /root/merged/tmp: file exists","time":"2024-02-08T01:22:44+08:00"}
{"level":"info","msg":"mountVolume from /root/volume to /root/merged/tmp successfully","time":"2024-02-08T01:22:44+08:00"}
{"level":"info","msg":"run::sendInitCommands all commands:/bin/sh","time":"2024-02-08T01:22:44+08:00"}
{"level":"info","msg":"exec init command","time":"2024-02-08T01:22:44+08:00"}
{"level":"info","msg":"Current location is /root/merged","time":"2024-02-08T01:22:44+08:00"}
{"level":"info","msg":"init::ContainerResourceInit execuatble path=/bin/sh","time":"2024-02-08T01:22:44+08:00"}

# 容器内执行
/ # echo abcdefg > tmp/aaa.txt
/ # cat tmp/aaa.txt 
abcdefg

退出容器,查看宿主机下文件目录内容:

# 容器内执行
/ # exit
{"level":"info","msg":"volume::unmountOverlayfs unmountFS /root/merged successfully","time":"2024-02-08T01:24:08+08:00"}
{"level":"info","msg":"volume::removeDirs upper-dir /root/upper/ work-dir /root/work/ successfully","time":"2024-02-08T01:24:08+08:00"}

# 宿主机内执行
[root@localhost ~]# ls
0a                   busybox              notify_on_releasx~
1a                   busybox.tar          notify_on_releasy~
1a?1a?2a?2:q         cgroup               notify_on_releasz~
6.824                cmake-3.22.3         projects
anaconda-ks.cfg      cmake-3.22.3.tar.gz  stack.txt
boost_1_71_0         myimage.tar          volume
boost_1_71_0.tar.gz  notify_on_release~   vscode-cpptools
[root@localhost ~]# cat volume/aaa.txt 
abcdefg

可以发现,即使容器退出,宿主机上挂载的数据卷依旧存在。

容器信息收集及后台运行

docker 支持容器 docker run -d xxx 后台运行,并且在 docker 的早期版本中,所有容器的 init 进程都是从 docker daemon 这个进程 fork 出来的。如果 docker daemon 宕机后所有容器进程会交给 init 进程托管,但是 docker daemon 宕机后它的相关数据信息都会丢失,也会影响容器的实际运行。

为了避免单一 docker daemon 宕机对容器进程的影响,docker 使用了 containerd ,并由它来负责管理容器的生命周期,包括容器的创建、运行、停止等等。同时 containerd 进程为每个进程启动了一个 containerd-shim 进程,并由该进程来接收来自 containerd 的命令,启动容器中的进程并监控它的生命周期。那么即使 docker daemon 进程挂掉了,仍然不会影响容器的运行。

为了实现方便,这里将当前运行 Mydocker 的进程当作主进程;容器则是作为当前 Mydocker 进程 fork 出来的子进程。如果运行时不携带 -it 参数(命令行交互),而是携带 -d(后台运行),则容器进程启动后主进程退出,容器进程由 系统init进程来管理。

容器信息收集

查看运行中的容器

docker ps 命令查看运行中的容器信息

posted @ 2024-02-08 01:26  Stitches  阅读(98)  评论(0编辑  收藏  举报