自己动手写Docker系列 -- 4.3实现volume数据卷

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

简介

在上篇中对容器和镜像实现了进一步的文件隔离,是容器内的修改不影响到宿主机。本篇中将实现docker中的volume,提供持久化存储能力

源码说明

同时放到了Gitee和Github上,都可进行获取

本章节对应的版本标签是:4.3,防止后面代码过多,不好查看,可切换到标签版本进行查看

代码实现

在上篇中,我们实现了容器和镜像的文件隔离,在容器内的修改不会影响到宿主机内

但我们也会有一些持久化的存储,在容器中操作后,想要保存下来,便于后序查看或者重启后进行加载,对应docker中的 -v 参数

这里的原理还是同上篇中的一样,也是使用文件挂载的方式,不同于上篇的是,这个-v的挂载只卸载卷,但不删除文件,这样文件就保留了下来

代码也不是太复杂,直接上代码,相比较书中的代码,稍微做了一些结构上的调整和优化

新增 -v 命令参数

我们在RunCommand中增加-v命令参数,和docker中的-v一样

需要注意的是,目前暂时单数据卷挂载,还不能像docker一样提供多个-v,但影响不大

var RunCommand = cli.Command{
	Name:  "run",
	Usage: `Create a container with namespace and cgroups limit mydocker run -ti [command]`,
	Flags: []cli.Flag{
		......
		// 添加-v标签
		cli.StringFlag{
			Name:  "v",
			Usage: "volume",
		},
	},
	/*
		这里是run命令执行的真正函数
		1.判断参数是否包含command
		2.获取用户指定的command
		3.调用Run function 去准备启动容器
	*/
	Action: func(context *cli.Context) error {
		......
		volume := context.String("v")
		run.Run(tty, cmdArray, resConfig, volume)
		return nil
	},
}
复制代码

上面加参数传入了Run函数中,在Run函数中,我们将其继续传递到进程启动的初始化进程和退出时的清理函数中

func Run(tty bool, cmdArray []string, config *subsystem.ResourceConfig, volume string) {
	......
	mntUrl := pwd + "/mnt/"
	rootUrl := pwd + "/"
	// 传入初始化进程中
	parent, writePipe := container.NewParentProcess(tty, rootUrl, mntUrl, volume)
	if err := parent.Start(); err != nil {
		log.Error(err)
		// 如果fork进程出现异常,但有相关的文件已经进行了挂载,需要进行清理,避免后面运行报错时,需要手工清理
		deleteWorkSpace(rootUrl, mntUrl, volume)
		return
	}

	......
	
	log.Infof("parent process run")
	_ = parent.Wait()
	// 传入退出时的清理函数中
	deleteWorkSpace(rootUrl, mntUrl, volume)
	os.Exit(-1)
}
复制代码

创建容器文件系统

在进程初始化函数中,会创建容器文件系统,和上篇文件中一样,我们只是在newWorkSpace函数中新增一个函数,来挂载持久化数据卷即可

回顾下,这个核心函数是功能大致如下:

1.创建只读层

2.创建容器读写层

3.创建挂载点并将只读层和读写层挂载到挂载点上

下面我们要增加的是:

4.在容器内创建对应的数据卷,并将其挂载到挂载点上

我们这步新增的需要在第三步后面,因为需要挂载点已经准备就绪

func newWorkSpace(rootUrl, mntUrl, volume string) error {
	if err := createReadOnlyLayer(rootUrl); err != nil {
		return err
	}
	if err := createWriteLayer(rootUrl); err != nil {
		return err
	}
	if err := createMountPoint(rootUrl, mntUrl); err != nil {
		return err
	}
	// 在容器内创建对应的数据卷,并将其挂载到挂载点上
	if err := mountExtractVolume(mntUrl, volume); err != nil {
		return err
	}
	return nil
}
复制代码

挂载数据卷的具体处理如下:

1.如果参数有效才进行挂载操作:空直接返回;参数错误则报错

2.如果宿主机中的文件路径不存在,需要进行创建(书中是使用mkdir,这样如果多级目录时,上级目录没有时会报错,这里mkdirall递归创建)

3.在容器读写层创建对应到容器内的文件

4.将宿主机文件进行挂载

具体实现如下:


func mountVolume(mntUrl string, volumeUrls []string) error {
	// 如果宿主机文件目录不存在则创建
	parentUrl := volumeUrls[0]
	exist, err := pathExist(parentUrl)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		// 使用mkdir all 递归创建文件夹
		if err := os.MkdirAll(parentUrl, 0777); err != nil {
			return fmt.Errorf("mkdir parent dir err: %v", err)
		}
	}

	// 在容器文件系统内创建挂载点
	containerUrl := mntUrl + volumeUrls[1]
	if err := os.Mkdir(containerUrl, 0777); err != nil {
		return fmt.Errorf("mkdir container volume err: %v", err)
	}

	// 把宿主机文件目录挂载到容器挂载点
	dirs := "dirs=" + parentUrl
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("mount volume err: %v", err)
	}
	return nil
}

func pathExist(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, err
	}
	return false, err
}
复制代码

容器退出时的清理

在上篇中清理动作是直接卸载了挂载点,并删除了读写层

我们这次的数据卷是需要持久化保存的,只需要进行将挂载点卸载即可

具体实现如下:

func deleteWorkSpace(rootUrl, mntUrl, volume string) {
	// 这里在删除挂载点之前,把数据卷卸载即可
	// 后面的删除挂载点和删除读写层后,不会影响宿主机的文件
	unmountVolume(mntUrl, volume)
	deleteMountPoint(mntUrl)
	deleteWriteLayer(rootUrl)
}
复制代码

unmountVolume 具体实现如下:

func unmountVolume(mntUrl string, volume string) {
	if volume == "" {
		return
	}
	volumeUrls := strings.Split(volume, ":")
	if len(volumeUrls) != 2 || volumeUrls[0] == "" || volumeUrls[1] == "" {
		return
	}

	// 卸载容器内的 volume 挂载点的文件系统
	containerUrl := mntUrl + volumeUrls[1]
	cmd := exec.Command("umount", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Errorf("ummount volume failed: %v", err)
	}
}
复制代码

运行测试

编译一波代码,在容器内创建一个文件

➜  dockerDemo git:(main) ✗ go build mydocker/main.go
➜  dockerDemo git:(main) ✗ ./main run -ti -v /root/volumn/test:/test sh
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"all command is : sh","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"parent process run","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"init come on","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"current location: /home/lw/code/go/dockerDemo/mnt","time":"2022-03-20T10:15:04+08:00"}
{"level":"info","msg":"find path: /bin/sh","time":"2022-03-20T10:15:04+08:00"}
/ # ls
bin   dev   etc   home  main  proc  root  sys   test  tmp   usr   var
/ # touch /test/test.txt
/ # ls /test/
test.txt
复制代码

我们新开一个sh看看宿主机的情况,可以看到文件在宿主机中也有

➜  ~ ls /root/volumn/test
test.txt
复制代码

然后我们退出容器,可以看到当前运行目录下的文件已经被清理掉了

/ # exit
➜  dockerDemo git:(main) ✗ ll
总用量 4.6M
drwxr-xr-x 12 root root 4.0K 3月  17 06:17 busybox
drwxrwxr-x  2 lw   lw   4.0K 3月  18 20:45 docs
drwxrwxr-x  3 lw   lw   4.0K 3月   7 04:55 example
-rw-rw-r--  1 lw   lw    382 3月  12 10:18 go.mod
-rw-rw-r--  1 lw   lw   2.0K 3月  12 10:18 go.sum
-rw-rw-r--  1 lw   lw    12K 3月  12 10:18 LICENSE
-rwxr-xr-x  1 root root 4.6M 3月  20 10:15 main
drwxrwxr-x  6 lw   lw   4.0K 3月  12 10:20 mydocker
-rw-rw-r--  1 lw   lw    473 3月  12 10:18 README.md
复制代码

我们再次去查看宿主机的文件,发现依旧存在,目的达成

➜  ~ ls /root/volumn/test
test.txt
复制代码

猜你喜欢

转载自juejin.im/post/7083203141440634916