手写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-dir
、merged-dir
、work-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
命令查看运行中的容器信息