本文为从零开始写 Docker 系列第五篇,在 pivotRoot 基础上通过 overlayfs 实现写操作隔离,达到容器中写操作和宿主机互不影响。


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

欢迎 Star


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


开发环境如下:

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. 概述

上一篇中已经实现了使用宿主机 /root/busybox 目录作为容器的根目录,但在容器内对文件的操作仍然会直接影响到宿主机的 /root/busybox 目录。

本节要进一步进行容器和镜像隔离,实现在容器中进行的操作不会对镜像(宿主机/root/busybox目录)产生任何影响的功能

什么是 overlayfs?

overlayfs 是 UFS 的一种实现,UnionFS 全称为 Union File System ,是一种为 Linux FreeBSD NetBSD 操作系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务

它使用 branch 不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。

这些 branches 或者是 read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一个重要的资管管理技术叫写时复制。

写时复制(copy-on-write,下文简称 CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。

它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源,这个资源可以被新旧实例共享。

创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。

UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。

比如,我现在有两个目录 A 和 B,它们分别有两个文件:

$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x

然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C

这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

$ tree ./C
./C
├── a
├── b
└── x

可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。

这就是联合文件系统,目的就是将多个文件联合在一起成为一个统一的视图

UFS 有多种实现,例如 AUFS、Overlayfs 等,这里使用比较主流的 Overlayfs。

关于 Overlayfs 详细介绍可以看一下这篇文章:Docker 魔法解密:探索 UnionFS 与 OverlayFS

里面详细介绍了 overlayfs 各个特性,以及 docker 中是如何使用 Overlayfs 的。

这里对需要用到部分做简要说明:

首先,overlayfs 一般分为 lower、upper、merged 和 work 4个目录。

  • lower 只读层,该层数据不会被修改
  • upper 可读写层,所有修改都发生在这一层,即使是修改的 lower 中的数据
  • merged 视图层,可以看到 lower、upper 中的所有内容
  • work 则是 overlayfs 内部使用

在本文实现中使用我们的镜像目录(busybox 目录) 作为 lower 目录,这样可以保证镜像内容部被修改。

merged 目录由于可以看到全部内容,因此作为容器 rootfs 目录,即 pivotRoot 会切换到 merged 目录。

upper 目录则是用于保存容器中的修改,因为 overlayfs 中所有修改都会发生在这里。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


2. Mount Overlayfs

Docker 在使用镜像启动一个容器时,会新建2个layer: write layer和 container-init layer。

write layer是容器唯一的 可读写层;而 container-init layer 是为容器新建的只读层,用来存储容器启动时传入的系统信息(不过在实际的场景下,它们并不是以write layer和container-init layer命名的)。最后把write layer、container-init layer 和相关镜像的 layers 都 mount 到一个 mnt 目录下,然后把这个 mnt 目录作为容器启动的根目录。

同样的,我们在容器启动前,也需要先 mount 好 overlayfs 目录,然后执行 privotRoot 时直接切换到 mount 好的 overlayfs merge 目录即可。

NewWorkSpace 函数是用来创建容器文件系统的,它包括 createLower、createDirs和mountOverlayFS。

分为以下步骤:

  • 1)准备 busybox 目录,之前都是手动解压准备 /root/busybox 目录,这次把解压逻辑加入到代码中。只需要准备好 busybox.tar 文件即可。容器启动时自动将 busybox.tar 解压到 busybox 目录下,作为容器的只读层。

  • 2)准备 overlayfs 目录,创建好挂载 overlayfs 需要的 upper、work 和 merged 目录

  • 3)实现 mount overlayfs,将 merged 目录作为挂载点,然后把 busybox、upper 挂载到 merged 目录。

  • 4)更新 pivotRoot 调用目录,将 rootfs 从宿主机目录 root/busybox 切换到上一步中挂载的/root/merged 目录

  • 最后 NewParentProcess 函数中将容器使用的宿主机目录 root/busybox 替换成/root/merged。

// NewWorkSpace Create an Overlay2 filesystem as container root workspace
func NewWorkSpace(rootPath string) {
createLower(rootPath)
createDirs(rootPath)
mountOverlayFS(rootPath)
} // createLower 将busybox作为overlayfs的lower层
func createLower(rootURL string) {
// 把busybox作为overlayfs中的lower层
busyboxURL := rootURL + "busybox/"
busyboxTarURL := rootURL + "busybox.tar"
// 检查是否已经存在busybox文件夹
exist, err := PathExists(busyboxURL)
if err != nil {
log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
}
// 不存在则创建目录并将busybox.tar解压到busybox文件夹中
if !exist {
if err := os.Mkdir(busyboxURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
}
if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", busyboxURL, err)
}
}
} // createDirs 创建overlayfs需要的的upper、worker目录
func createDirs(rootURL string) {
upperURL := rootURL + "upper/"
if err := os.Mkdir(upperURL, 0777); err != nil {
log.Errorf("mkdir dir %s error. %v", upperURL, err)
}
workURL := rootURL + "work/"
if err := os.Mkdir(workURL, 0777); err != nil {
log.Errorf("mkdir dir %s error. %v", workURL, err)
}
} // mountOverlayFS 挂载overlayfs
func mountOverlayFS(rootURL string, mntURL string) {
// mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
// 创建对应的挂载目录
if err := os.Mkdir(mntURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", mntURL, err)
}
// 拼接参数
// e.g. lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/merged
dirs := "lowerdir=" + rootURL + "busybox" + ",upperdir=" + rootURL + "upper" + ",workdir=" + rootURL + "work"
// dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "busybox"
cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", dirs, mntURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
}

接下来,在 NewParentProcess 函数中将容器使用的宿主机目录/root/busybox 替换成root/mnt 。这样 ,使用 OverlayFS 系统启动容器的代码就完成了。

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
// 省略其他代码
cmd.ExtraFiles = []*os.File{readPipe}
mntURL := "/root/merged/"
rootURL := "/root/"
NewWorkSpace(rootURL, mntURL)
cmd.Dir = mntURL
return cmd, writePipe
}

3. Unmount Overlayfs

Docker 会在删除容器的时候,把容器对应 WriteLayer 和 Container-init Layer 删除,而保留镜像所有的内容。本节中在容器退出的时候也会删除 upper、work 和 merged 目录只保留作为镜像的 lower 层目录即 busybox。

具体步骤如下:

  • 1)unmount overlayfs:将/root/merged目录挂载解除
  • 2)删除其他目录:删除之前为 overlayfs 准备的 upper、work、merged 目录

由于 overlayfs 的特性,所有修改操作都发生在 upper 目录,因此目录删除后容器对文件系统的更改,就都已经抹去了。

DeleteWorkSpace 函数包括 umountOverlayFS 和 deleteDirs。

// DeleteWorkSpace Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string) {
umountOverlayFS(mntURL)
deleteDirs(rootURL)
} func umountOverlayFS(mntURL string) {
cmd := exec.Command("umount", mntURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
if err := os.RemoveAll(mntURL); err != nil {
log.Errorf("Remove dir %s error %v", mntURL, err)
}
} func deleteDirs(rootURL string) {
writeURL := rootURL + "upper/"
if err := os.RemoveAll(writeURL); err != nil {
log.Errorf("Remove dir %s error %v", writeURL, err)
}
workURL := rootURL + "work"
if err := os.RemoveAll(workURL); err != nil {
log.Errorf("Remove dir %s error %v", workURL, err)
}
}

4. 测试

首先将busybox.tar 放到 /root 目录下:

$ ls
busybox.tar

然后启动我们的容器

root@mydocker:~/feat-overlayfs/mydocker# ./mydocker run -it /bin/sh
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"enter NewWorkSpace","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"enter createLower","time":"2024-01-16T13:36:38+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-16T13:36:38+08:00"}

再次查看宿主机的 /root 目录:

root@mydocker:~# ls /root
busybox busybox.tar merged upper work

可以看到,多了几个目录:busybox、merged、upper、work。

在容器中新建一个文件:

/ # echo KubeExplorer > tmp/hello.txt
/ # ls /tmp
hello.txt
/ # cat /tmp/hello.txt
KubeExplorer

然后切换到宿主机:

root@mydocker:~# ls busybox/tmp
root@mydocker:~# ls upper/tmp
hello.txt
root@mydocker:~# ls merged/tmp
hello.txt

可以发现,这个新创建的文件居然不在 busybox 目录,而是在 upper 中,然后 merged 目录中也可以看到。

这就是 overlayfs 的作用了。

写操作不会修改 lower 目录(busybox),而是发生在 upper 中,即在 upper 中 tmp 目录并创建了 hello.txt 文件。

而 merged 作为挂载点自然是能够看到 hello.txt 文件的。

最后在容器中执行 exit 退出容器。

/ # exit

然后再次查看宿主机上的 root 文件夹内容。

root@mydocker:~# ls /root
busybox busybox.tar

可以看到,upper、work 和 merged 目录被删除,作为镜像的 busybox 层仍然保留。

并且 busybox 中的内容未被修改:

root@mydocker:~# ls /root/busybox
bin dev etc home proc root sys tmp usr var

至此,基本实现了 Docker 的效果:

  • 1)镜像中的文件不会被修改
  • 2)容器中的修改不会影响宿主机
  • 3)容器退出后,修改内容丢失

5. 小结

overlayfs 引入具体流程如下:

  • 1)自动解压 busybox.tar 到 busybox 作为 lower 目录,类似 docker 镜像层
  • 2)容器启动前准备好 lower、upper、work、merged 目录并 mount 到 merged 目录
  • 3)容器启动后使用 pivotRoot 将 rootfs 切换到 merged 目录
    • 后续容器中的修改由于 overlayfs 的特性,都会发生在 upper 目录中,而不会影响到 lower 目录
  • 4)容器停止后 umount 并移除upper、work、merged 目录


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


最后在推荐一下 Docker 魔法解密:探索 UnionFS 与 OverlayFS


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

欢迎 Star

相关代码见 feat-overlayfs 分支,测试脚本如下:

需要提前在 /root 目录准备好 busybox.tar 文件,具体看上一篇文章第二节。

# 克隆代码
git clone -b feat-overlayfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 查看文件系统是否变化
./mydocker run -it /bin/ls

从零开始写 Docker(五)---基于 overlayfs 实现写操作隔离的更多相关文章

  1. [转]基于overlayfs的硬盘资源隔离工具troot

    原文在这里:http://blog.donghao.org/tag/overlayfs/ 某些开发测试团队会有这样的需求:多个开发或测试人员在一台物理机上搭环境.装rpm包.测试等,目录很可能互相干扰 ...

  2. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  3. docker从零开始 存储(五)存储驱动介绍

    关于存储驱动程序 要有效地使用存储驱动程序,了解Docker如何构建和存储镜像以及容器如何使用这些镜像非常重要.您可以使用此信息做出明智的选择,以确定从应用程序中保留数据的最佳方法,并避免在此过程中出 ...

  4. linux设备驱动归纳总结(五):4.写个简单的LED驱动【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-84693.html linux设备驱动归纳总结(五):4.写个简单的LED驱动 xxxxxxxxxxx ...

  5. 自己写的中间层..基于通讯组件 RTC

    273265088 我用原生Listbox与你的组件组合...创造了奇迹..搞了一个非常复杂的 UI .. 每个item高度 包括里面的元素 以及事件都是动态的搞了好几个小时感觉UI 非常完美比客户要 ...

  6. 自己动手写CPU(基于FPGA与Verilog)

    大三上学期开展了数字系统设计的课程,下学期便要求自己写一个单周期CPU和一个多周期CPU,既然要学,就记录一下学习的过程. CPU--中央处理器,顾名思义,是计算机中最重要的一部分,功能就是周而复始地 ...

  7. 【Linux开发】linux设备驱动归纳总结(五):4.写个简单的LED驱动

    linux设备驱动归纳总结(五):4.写个简单的LED驱动 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  8. 基于File NIO写的一个文件新增内容监控器

    基于File NIO写的一个文件新增内容监控器 需求说明 监控一个文件,如果文件有新增内容,则在控制台打印出新增内容. 代码示例 FileMoniter文件监控器类 package com.black ...

  9. 【C++】从零开始的CS:GO逆向分析3——写出一个透视

    [C++]从零开始的CS:GO逆向分析3--写出一个透视 本篇内容包括: 1. 透视实现的方法介绍 2. 通过进程名获取进程id和进程句柄 3. 通过进程id获取进程中的模块信息(模块大小,模块地址, ...

  10. 基于APIView&ModelSerializer写接口

    目录 基于APIView&ModelSerializer写接口 一.首先准备前提工作 1.模型代码 2.路由代码 3.视图代码 二.继承Serializer序列化定制字段的三种方法 1.通过s ...

随机推荐

  1. Go中sync.map使用小结

    sync.map 前言 深入了解下 查看下具体的实现 Load Store Delete LoadOrStore 总结 流程图片 参考 sync.map 前言 Go中的map不是并发安全的,在Go1. ...

  2. sed文本处理工具常见用法

    sed的全称是stream editor, 表示它是一个流编译器.可以处理文本内容和终端命令的流标准输出,对文本做查找,替换,插入,删除操作. 它是把文件中的内容逐行copy到缓冲区,然后在缓冲区中进 ...

  3. PHP截取文章内容

    <?php /** * 实现中文字串截取无乱码的方法. */ function getSubstr($string, $start, $length) { if (mb_strlen($stri ...

  4. PicGo + Gitee 实现 Markdown 图床

    最近再研究图床,注册的阿里云域名备案还在审批,所以七牛云图床暂时没用,所以试下用PicGo+ Gitee PicGo - 基于 electron-vue 开发的图床工具 PicGo目前支持了微博图床, ...

  5. 基于OpenEuler的信创国产瘦客户机软件系统 DoraOS

    DoraOS是一款瘦客户机系统软件,最新版本基于OpenEuler开发.可以将主机转化为专业的瘦客户机.目前支持x86架构的硬件. 软件下载地址为: https://www.doracloud.cn/ ...

  6. spring boot jar混淆加密

    最近在做一个智能床垫的机构版项目,客户要求部署到客户那边要做代码混淆防止代码被反编译. 一:在需要加密的jar的pom.xml文件添加依赖 <!-- 设置 jitpack.io 仓库 --> ...

  7. 小知识:统计Oracle的日归档量

    首先这对于Oracle DBA来说是个初级问题,即使不熟悉的初级DBA也可以快速在网上搜索到现成的SQL语句. 网上搜到的查询SQL基本类似这样的逻辑: select trunc(completion ...

  8. [JVM] CPU缓存一致性协议

    CPU缓存一致性协议 CPU高速缓存 CPU缓存是位于cpu和内存之间的临时数据交换器,它的容量比内存小的夺但是交换速度要比内存快得多,主要是为了解决cpu运行时的处理速度与内存读写速度不匹配的问题. ...

  9. C# 二十年语法变迁之 C# 2,C# 3 ,C# 4参考

    C# 二十年语法变迁之 C# 2,C# 3 ,C# 4参考 https://benbowen.blog/post/two_decades_of_csharp_i/ 自从 C# 于 2000 年推出以来 ...

  10. Mobx与Redux的异同

    Mobx与Redux的异同 Mobx与Redux都是用来管理JavaScript应用的状态的解决方案,用以提供在某个地方保存状态.修改状态和更新状态,使我们的应用在状态与组件上解耦,我们可以从一个地方 ...