从零开始写 Docker(十四)---重构:实现容器间 rootfs 隔离

本文为从零开始写 Docker 系列第十四篇,实现容器间的 rootfs 隔离,使得多个容器间互不影响。


完整代码见:https://github.com/lixd/mydocker

欢迎 Star

推荐阅读以下文章对 docker 基本实现有一个大致认识:



开发环境如下:

bash 复制代码
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户

1. 概述

虽然之前通过 pivotRoot、overlayfs 实现了容器和宿主机的 rootfs 隔离,但是多个容器还是共用的一个rootfs,多容器之间会互相影响。

之前容器都是用的宿主机上的 /root/merged 目录作为自己的 rootfs,当启动多个容器时可写层会互相影响。

本篇通过为每个容器单独准备一个 rootfs 来实现隔离,使得我们多个容器之间互不影响。

2. 实现

为了实现该功能,需要做以下工作:

  • 修改 mydocker commit 命令,实现对不同容器进行打包镜像的功能。
  • 修改 mydocker run 命令,用户可以指定不同镜像,并为每个容器分配单独的隔离文件系统
    • 根据镜像名称找到对应 tar 文件,解压后作为overlay 中的 lower 目录进行挂载
  • 修改 mydocker rm 命令,删除容器时顺带删除文件系统

这三处调整实际上都是对宿主机上容器 rootfs 目录的调整,把 rootfs 从原来的 /root/merged 调整为 /var/lib/mydocker/overlay2/{containerID}/merged ,这样实现容器之间的隔离。

docker 也是使用的var/lib/docker/overlay2/{containerID}/merged 目录作为 rootfs.可以使用docker inspect {containerID} -f '{``{json .GraphDriver}}' 命令查看。

2.1 commit 命令更新

之前 commit 命令直接把/root/merged 目录压缩为 tar 作为镜像,现在需要根据 containerID 以/var/lib/mydocker/overlay2/{containerID}/merged 格式来拼接目录。

首先,在 main_command.go 文件中修改 commitCommand,将用户输入参数改为 containerID 和 imageName,并调用 commitContainer 方法实现 commit 操作。

go 复制代码
var commitCommand = cli.Command{
	Name:  "commit",
	Usage: "commit container to image,e.g. mydocker commit 123456789 myimage",
	Action: func(context *cli.Context) error {
		if len(context.Args()) < 2 {
			return fmt.Errorf("missing container name and image name")
		}
		containerID := context.Args().Get(0)
		imageName := context.Args().Get(1)
		return commitContainer(containerID, imageName)
	},
}

然后 commitContainer 中调整一下压缩路径,根据 containerID 拼接要压缩的目录

go 复制代码
var ErrImageAlreadyExists = errors.New("Image Already Exists")

func commitContainer(containerID, imageName string) error {
	mntPath := utils.GetMerged(containerID)
	imageTar := utils.GetImage(imageName)
	exists, err := utils.PathExists(imageTar)
	if err != nil {
		return errors.WithMessagef(err, "check is image [%s/%s] exist failed", imageName, imageTar)
	}
	if exists {
		return ErrImageAlreadyExists
	}
	log.Infof("commitContainer imageTar:%s", imageTar)
	if _, err = exec.Command("tar", "-czf", imageTar, "-C", mntPath, ".").CombinedOutput(); err != nil {
		return errors.WithMessagef(err, "tar folder %s failed", mntPath)
	}
	return nil
}

2.2 run 命令更新, 实现隔离文件系统

run 命令改动比较大, 需要把涉及到目录的都进行调整。

改动点:

  • 1)runCommand 命令中添加 imageName 参数,让用户可以指定镜像启动容器
  • 2)启动容器时, rootfs 部分需要根据 containerID 拼接目录
runCommand

runCommand 命令中添加 imageName 作为第一个参数输入

go 复制代码
var runCommand = cli.Command{
	Action: func(context *cli.Context) error {
		// 省略其他内容
		// get image name
		imageName := cmdArray[0]
		cmdArray = cmdArray[1:]

		tty := context.Bool("it")
		detach := context.Bool("d")

		// Run方法增加对应参数
		Run(tty, cmdArray, resConf, volume, containerName, imageName)
		return nil
	},
}

相关方法都要增加 imageName 参数:

go 复制代码
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume, containerName, imageName string) {
	containerId := container.GenerateContainerID() // 生成 10 位容器 id

	// start container
	parent, writePipe := container.NewParentProcess(tty, volume, containerId, imageName)
	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
	}

	// record container info
	err := container.RecordContainerInfo(parent.Process.Pid, comArray, containerName, containerId)
	if err != nil {
		log.Errorf("Record container info error %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)
	if tty { // 如果是tty,那么父进程等待,就是前台运行,否则就是跳过,实现后台运行
		_ = parent.Wait()
		container.DeleteWorkSpace(containerId, volume)
		container.DeleteContainerInfo(containerId)
	}
}
rootfs 相关调整

rootfs 相关目录定义成变量,并提供相应的 Get 方法,调用时指定 containerID 即可拿到对应目录。

go 复制代码
// 容器相关目录
const (
	ImagePath       = "/var/lib/mydocker/image/"
	RootPath        = "/var/lib/mydocker/overlay2/"
	lowerDirFormat  = RootPath + "%s/lower"
	upperDirFormat  = RootPath + "%s/upper"
	workDirFormat   = RootPath + "%s/work"
	mergedDirFormat = RootPath + "%s/merged"
	overlayFSFormat = "lowerdir=%s,upperdir=%s,workdir=%s"
)

func GetRoot(containerID string) string { return RootPath + containerID }

func GetImage(imageName string) string { return fmt.Sprintf("%s%s.tar", ImagePath, imageName) }

func GetLower(containerID string) string {
	return fmt.Sprintf(lowerDirFormat, containerID)
}

func GetUpper(containerID string) string {
	return fmt.Sprintf(upperDirFormat, containerID)
}

func GetWorker(containerID string) string {
	return fmt.Sprintf(workDirFormat, containerID)
}

func GetMerged(containerID string) string { return fmt.Sprintf(mergedDirFormat, containerID) }

func GetOverlayFSDirs(lower, upper, worker string) string {
	return fmt.Sprintf(overlayFSFormat, lower, upper, worker)
}

另外则是 NewWorkSpace 和 DeleteWorkSpace 这两个方法以及其内部的一系列方法涉及到的路径全改成动态的,根据 containerID 进行拼接:

这里贴一下 NewWorkSpace 和 DeleteWorkSpace 两个方法:

go 复制代码
// NewWorkSpace Create an Overlay2 filesystem as container root workspace
/*
1)创建lower层
2)创建upper、worker层
3)创建merged目录并挂载overlayFS
4)如果有指定volume则挂载volume
*/
func NewWorkSpace(volume, imageName, containerName string) {
	err := createLower(imageName)
	if err != nil {
		log.Errorf("createLower err:%v", err)
		return
	}
	err = createUpperWorker(containerName)
	if err != nil {
		log.Errorf("createUpperWorker err:%v", err)
		return
	}
	err = mountOverlayFS(containerName)
	if err != nil {
		log.Errorf("mountOverlayFS err:%v", err)
		return
	}
	if volume != "" {
		volumeURLs := volumeUrlExtract(volume)
		if len(volumeURLs) == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
			err = mountVolume(containerName, volumeURLs)
			if err != nil {
				log.Errorf("mountVolume err:%v", err)
				return
			}
		} else {
			log.Infof("volume parameter input is not correct.")
		}
	}
}
go 复制代码
// DeleteWorkSpace Delete the OverlayFS filesystem while container exit
/*
和创建相反
1)有volume则卸载volume
2)卸载并移除merged目录
3)卸载并移除upper、worker层
*/
func DeleteWorkSpace(volume, containerName string) error {
	// 如果指定了volume则需要先umount volume
	if volume != "" {
		volumeURLs := volumeUrlExtract(volume)
		length := len(volumeURLs)
		if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
			err := umountVolume(containerName, volumeURLs)
			if err != nil {
				return errors.Wrap(err, "umountVolume")
			}
		}
	}
	// 然后umount整个容器的挂载点
	err := umountOverlayFS(containerName)
	if err != nil {
		return errors.Wrap(err, "umountOverlayFS")
	}
	// 最后移除相关文件夹
	err = removeUpperWorker(containerName)
	if err != nil {
		return errors.Wrap(err, "removeUpperWorker")
	}
	return nil
}

至此,基本改动完成了,创建出的每个容器都会单独在/var/lib/mydocker/overlay2/ 目录下生成一个 rootfs 目录,这样就避免了多个容器之间互相影响。

2.3 更新 rm 命令

之前,由于对应的文件系统因为是共用的,所以没有删除, rm 命令只把容器信息删了,这次对 rm 命令进行调整,删除时也把文件系统删了。

go 复制代码
func removeContainer(containerId string, force bool) {
	containerInfo, err := getInfoByContainerId(containerId)
	if err != nil {
		log.Errorf("Get container %s info error %v", containerId, err)
		return
	}

	switch containerInfo.Status {
	case container.STOP: // STOP 状态容器直接删除即可
		// 先删除配置目录,再删除rootfs 目录
		if err = container.DeleteContainerInfo(containerId); err != nil {
			log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err)
			return
		}
		container.DeleteWorkSpace(containerId, containerInfo.Volume)
	case container.RUNNING: // RUNNING 状态容器如果指定了 force 则先 stop 然后再删除
		if !force {
			log.Errorf("Couldn't remove running container [%s], Stop the container before attempting removal or"+
				" force remove", containerId)
			return
		}
		log.Infof("force delete running container [%s]", containerId)
		stopContainer(containerId)
		removeContainer(containerId, force)
	default:
		log.Errorf("Couldn't remove container,invalid status %s", containerInfo.Status)
		return
	}
}

增加了下面这一句:

go 复制代码
container.DeleteWorkSpace(containerId, containerInfo.Volume)

3. 测试

rootfs 调整

用 busybox.tar 镜像启动一个容器,然后查看/var/lib/mydocker/overlay2/ 目录下是否生成对应内容。

首先在/var/lib/mydocker/image/目录准备好镜像

bash 复制代码
root@mydocker:~# mv busybox.tar /var/lib/mydocker/image/

然后使用该镜像启动容器

shell 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# go build .
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name rootfs busybox top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/5341624332/lower image.tar:/var/lib/mydocker/image/busybox.tar","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/5341624332/lower,upperdir=/var/lib/mydocker/overlay2/5341624332/upper,workdir=/var/lib/mydocker/overlay2/5341624332/work /var/lib/mydocker/overlay2/5341624332/merged]","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:34:12+08:00"}

查看容器

shell 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
5341624332   rootfs      219016      running     top         2024-02-22 13:34:12

查看/var/lib/mydocker/overlay2 目录下是否生成对应内容

bash 复制代码
root@mydocker:/var/lib/mydocker/overlay2# cd /var/lib/mydocker/overlay2/5341624332
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls
lower  merged  upper  work
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls lower
bin  dev  etc  home  proc  root  sys  tmp  usr  var
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
bin  dev  etc  home  proc  root  sys  tmp  usr  var

可以看到,在/var/lib/mydocker/overlay2/{containerID} 目录下生成了,lower、merged、upper、work 等 overlay2 目录。

其中 lower 中的内容由镜像解压得到,merged 则是容器 rootfs 挂载点。

然后进入容器创建文件

bash 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 5341624332 /bin/sh
{"level":"info","msg":"container pid:219016 command:/bin/sh","time":"2024-02-22T13:37:42+08:00"}
got mydocker_pid=219016
got mydocker_cmd=/bin/sh
/ # echo KubeExplorer > a.txt
/ # cat a.txt
KubeExplorer

接着到对应 merged 目录查看文件是否存在

bash 复制代码
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
a.txt  bin  dev  etc  home  proc  root  sys  tmp  usr  var
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cat merged/a.txt
KubeExplorer

至此,说明 rootfs 调整一切正常。

commit 命令

接下来测试一下 mydocker commit 命令,把刚才启动的容器提交为镜像。

shell 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
5341624332   rootfs      219016      running     top         2024-02-22 13:34:12
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker commit 5341624332 busybox-with-custom
{"level":"info","msg":"commitContainer imageTar:/var/lib/mydocker/image/busybox-with-custom.tar","time":"2024-02-22T13:43:33+08:00"}

然后查看 var/lib/mydocker/image/ 目录是否生成了对应的镜像文件

shell 复制代码
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cd /var/lib/mydocker/image/
root@mydocker:/var/lib/mydocker/image# ls
busybox-with-custom.tar  busybox.tar

busybox-with-custom.tar 就是 commit 命令生成的镜像。

接下来使用该镜像启动一个容器,查看之前创建的文件是否存在

bash 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name rootfs2 busybox-with-custom top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/8118341786/lower image.tar:/var/lib/mydocker/image/busybox-with-custom.tar","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/8118341786/lower,upperdir=/var/lib/mydocker/overlay2/8118341786/upper,workdir=/var/lib/mydocker/overlay2/8118341786/work /var/lib/mydocker/overlay2/8118341786/merged]","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:45:53+08:00"}

进入容器查看内容

bash 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
5341624332   rootfs      219016      running     top         2024-02-22 13:34:12
8118341786   rootfs2     219109      running     top         2024-02-22 13:45:53
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 8118341786 /bin/sh
{"level":"info","msg":"container pid:219109 command:/bin/sh","time":"2024-02-22T13:46:14+08:00"}
got mydocker_pid=219109
got mydocker_cmd=/bin/sh
/ # cat a.txt
KubeExplorer

可以看到,提交的镜像中包含了我们新建的 a.txt 文件,说明 commit 命令也是正常的。

rm 命令

最后测试一下 mydocker rm 命令,能否删除镜像配置和对应的 rootfs 目录。

ps 命令拿到 id

bash 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
5341624332   rootfs      219016      running     top         2024-02-22 13:34:12
8118341786   rootfs2     219109      running     top         2024-02-22 13:45:53

根据 id 删除容器

bsah 复制代码
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker rm 5341624332 -f
{"level":"info","msg":"force delete running container [5341624332]","time":"2024-02-22T13:47:36+08:00"}
{"level":"info","msg":"umountOverlayFS,cmd:/usr/bin/umount /var/lib/mydocker/overlay2/5341624332/merged","time":"2024-02-22T13:47:36+08:00"}

查看一下 /var/lib/mydocker/overlay2 中的 rootfs 目录是否删除

shell 复制代码
cd /var/lib/mydocker/overlay2
root@mydocker:/var/lib/mydocker/overlay2# ls

可以看到,容器相关目录都被移除了。

4. 小结

本小节主要完善了容器的文件系统,在/var/lib/mydocker/overlay2/ 目录下为每个容器单独分配一个 rootfs,避免了多容器之间互相影响。


**【从零开始写 Docker 系列】**持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。



完整代码见:https://github.com/lixd/mydocker

欢迎关注~

相关代码见 refactor-isolate-rootfs 分支,测试脚本如下:

需要提前在 /var/lib/mydocker/image 目录准备好 busybox.tar 文件,具体见第四篇第二节。

bash 复制代码
# 克隆代码
git clone -b refactor-isolate-rootfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 
./mydocker run -d -name c1 busybox top
# 查看容器 Id
./mydocker ps
# stop 停止指定容器
./mydocker rm ${containerId} -f
相关推荐
文火冰糖的硅基工坊11 小时前
《投资-107》价值投资者的认知升级与交易规则重构 - 上市公司的估值,估的不是当前的净资产的价值,而是未来持续赚钱的能力,估的是公司未来所有赚到钱的价值。
重构·架构·投资·投机
文火冰糖的硅基工坊11 小时前
《投资-99》价值投资者的认知升级与交易规则重构 - 什么是周期性股票?有哪些周期性股票?不同周期性股票的周期多少?周期性股票的买入和卖出的特点?
大数据·人工智能·重构·架构·投资·投机
一水鉴天11 小时前
整体设计 逻辑系统程序 之18 Source 容器(Docker)承载 C/P/D 三式的完整设计与双闭环验证 之2
docker·架构·认知科学·公共逻辑
飞快的蜗牛13 小时前
利用linux系统自带的cron 定时备份数据库,不需要写代码了
java·docker
火星MARK13 小时前
k8s面试题
容器·面试·kubernetes
香吧香14 小时前
Docker Registry 使用总结
docker
赵渝强老师14 小时前
【赵渝强老师】Docker容器的资源管理机制
linux·docker·容器·kubernetes
haicome15 小时前
deepseek部署
docker·ragflow·deepseek 部署
乄bluefox16 小时前
保姆级docker部署nacos集群
java·docker·容器
每天进步一点_JL16 小时前
Docker 是什么?
后端·docker·容器