写点什么

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

作者:
  • 2022 年 3 月 20 日
  • 本文字数:3448 字

    阅读完需:约 11 分钟

简介

在上篇中对容器和镜像实现了进一步的文件隔离,是容器内的修改不影响到宿主机。本篇中将实现 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"}/ # lsbin   dev   etc   home  main  proc  root  sys   test  tmp   usr   var/ # touch /test/test.txt/ # ls /test/test.txt
复制代码


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


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


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


/ # exit➜  dockerDemo git:(main) ✗ ll总用量 4.6Mdrwxr-xr-x 12 root root 4.0K 3月  17 06:17 busyboxdrwxrwxr-x  2 lw   lw   4.0K 3月  18 20:45 docsdrwxrwxr-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 maindrwxrwxr-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/testtest.txt
复制代码


发布于: 刚刚阅读数: 2
用户头像

关注

还未添加个人签名 2018.09.09 加入

代码是门手艺活,也是门艺术活

评论

发布
暂无评论
自己动手写Docker系列 -- 4.3实现volume数据卷_Docker_萧_InfoQ写作平台