北京建设厅网站查询,手机网站建设运营方案,微信小程序开发环境搭建,织梦做的网站如何修改本文为从零开始写 Docker 系列第六篇#xff0c;实现类似 docker -v 的功能#xff0c;通过挂载数据卷将容器中部分数据持久化到宿主机。 完整代码见#xff1a;https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识#xff1a; …
本文为从零开始写 Docker 系列第六篇实现类似 docker -v 的功能通过挂载数据卷将容器中部分数据持久化到宿主机。 完整代码见https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识
核心原理深入理解 Docker 核心原理Namespace、Cgroups 和 Rootfs基于 namespace 的视图隔离探索 Linux NamespaceDocker 隔离的神奇背后基于 cgroups 的资源限制 初探 Linux Cgroups资源控制的奇妙世界深入剖析 Linux Cgroups 子系统资源精细管理Docker 与 Linux Cgroups资源隔离的魔法之旅 基于 overlayfs 的文件系统Docker 魔法解密探索 UnionFS 与 OverlayFS基于 veth pair、bridge、iptables 等等技术的 Docker 网络揭秘 Docker 网络手动实现 Docker 桥接网络 开发环境如下
rootmydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
rootmydocker:~# 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 功能大致实现步骤如下
1run 命令增加 -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 limitmydocker 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.判断参数是否包含command2.获取用户指定的command3.调用Run function去准备启动容器:*/Action: func(context *cli.Context) error {if len(context.Args()) 1 {return fmt.Errorf(missing container command)}var cmdArray []stringfor _, 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 volumeif volume ! {mntPath : path.Join(rootPath, merged)hostPath, containerPath, err : volumeExtract(volume)if err ! nil {log.Errorf(extract volume failedmaybe volume parameter input is not correctdetail:%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 cant 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 /containerPathcmd : exec.Command(mount, -o, bind, hostPath, containerPathInHost)cmd.Stdout os.Stdoutcmd.Stderr os.Stderrif 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 failedmaybe volume parameter input is not correctdetail:%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/tmpcontainerPathInHost : path.Join(mntPath, containerPath)cmd : exec.Command(umount, containerPathInHost)cmd.Stdout os.Stdoutcmd.Stderr os.Stderrif err : cmd.Run(); err ! nil {log.Errorf(Umount volume failed. %v, err)}
}3.测试
下面来验证一下程序的正确性。
挂载不存在的目录
第一个实验是把一个宿主机上不存在的文件目录挂载到容器中。
首先还是要在 root 目录准备好 busybox.tar,作为我们的镜像只读层。
$ ls
busybox.tar启动容器把宿主机的 /root/volume 挂载到容器的 /tmp 目录下。
rootmydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
{level:info,msg:resConf:\u0026{ 0 },time:2024-01-18T16:47:2908:00}
{level:info,msg:busybox:/root/busybox busybox.tar:/root/busybox.tar,time:2024-01-18T16:47:2908: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:2908:00}
{level:info,msg:mkdir parent dir /root/volume error. mkdir /root/volume: file exists,time:2024-01-18T16:47:2908:00}
{level:info,msg:mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists,time:2024-01-18T16:47:2908:00}
{level:info,msg:command all is /bin/sh,time:2024-01-18T16:47:2908:00}
{level:info,msg:init come on,time:2024-01-18T16:47:2908:00}
{level:info,msg:Current location is /root/merged,time:2024-01-18T16:47:2908:00}
{level:info,msg:Find path /bin/sh,time:2024-01-18T16:47:2908:00}新开一个窗口查看宿主机 /root 目录
rootDESKTOP-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 目录
rootmydocker:~# ls /root/volume/
hello.txt
rootmydocker:~# cat /root/volume/hello.txt
KubeExplorer可以看到文件也在。
然后测试退出容器后是否能持久化。
退出容器
/ # exit宿主机中再次查看 volume 目录
rootmydocker:~# ls /root/volume/
hello.txt文件还在说明我们的 volume 功能是正常的。
挂载已经存在目录
第二次实验是测试挂载一个已经存在的目录这里就把刚才创建的 volume 目录再挂载一次
rootmydocker:~/feat-volume/mydocker# ./mydocker run -it -v /root/volume:/tmp /bin/sh
{level:info,msg:resConf:\u0026{ 0 },time:2024-01-18T17:02:4808:00}
{level:info,msg:busybox:/root/busybox busybox.tar:/root/busybox.tar,time:2024-01-18T17:02:4808: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:4808:00}
{level:info,msg:mkdir parent dir /root/volume error. mkdir /root/volume: file exists,time:2024-01-18T17:02:4808:00}
{level:info,msg:mkdir container dir /root/merged//tmp error. mkdir /root/merged//tmp: file exists,time:2024-01-18T17:02:4808:00}
{level:info,msg:command all is /bin/sh,time:2024-01-18T17:02:4808:00}
{level:info,msg:init come on,time:2024-01-18T17:02:4808:00}
{level:info,msg:Current location is /root/merged,time:2024-01-18T17:02:4808:00}
{level:info,msg:Find path /bin/sh,time:2024-01-18T17:02:4808:00}查看刚才的文件是否存在
/ # ls /tmp/hello.txt
/tmp/hello.txt
/ # cat /tmp/hello.txt
KubeExplorer还在说明目录确实挂载进去了。
接下来更新文件内容并退出:
/ # echo KubeExplorer222 /tmp/hello.txt
/ # cat /tmp/hello.txt
KubeExplorer222
/ # exit在宿主机上查看
rootmydocker:~# 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