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


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

欢迎 Star


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


开发环境如下:

  1. root@mydocker:~# lsb_release -a
  2. No LSB modules are available.
  3. Distributor ID: Ubuntu
  4. Description: Ubuntu 20.04.2 LTS
  5. Release: 20.04
  6. Codename: focal
  7. root@mydocker:~# uname -r
  8. 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,它们分别有两个文件:

  1. $ tree
  2. .
  3. ├── A
  4. ├── a
  5. └── x
  6. └── B
  7. ├── b
  8. └── x

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

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

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

  1. $ tree ./C
  2. ./C
  3. ├── a
  4. ├── b
  5. └── 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。

  1. // NewWorkSpace Create an Overlay2 filesystem as container root workspace
  2. func NewWorkSpace(rootPath string) {
  3. createLower(rootPath)
  4. createDirs(rootPath)
  5. mountOverlayFS(rootPath)
  6. }
  7. // createLower 将busybox作为overlayfs的lower层
  8. func createLower(rootURL string) {
  9. // 把busybox作为overlayfs中的lower层
  10. busyboxURL := rootURL + "busybox/"
  11. busyboxTarURL := rootURL + "busybox.tar"
  12. // 检查是否已经存在busybox文件夹
  13. exist, err := PathExists(busyboxURL)
  14. if err != nil {
  15. log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
  16. }
  17. // 不存在则创建目录并将busybox.tar解压到busybox文件夹中
  18. if !exist {
  19. if err := os.Mkdir(busyboxURL, 0777); err != nil {
  20. log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
  21. }
  22. if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
  23. log.Errorf("Untar dir %s error %v", busyboxURL, err)
  24. }
  25. }
  26. }
  27. // createDirs 创建overlayfs需要的的upper、worker目录
  28. func createDirs(rootURL string) {
  29. upperURL := rootURL + "upper/"
  30. if err := os.Mkdir(upperURL, 0777); err != nil {
  31. log.Errorf("mkdir dir %s error. %v", upperURL, err)
  32. }
  33. workURL := rootURL + "work/"
  34. if err := os.Mkdir(workURL, 0777); err != nil {
  35. log.Errorf("mkdir dir %s error. %v", workURL, err)
  36. }
  37. }
  38. // mountOverlayFS 挂载overlayfs
  39. func mountOverlayFS(rootURL string, mntURL string) {
  40. // mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
  41. // 创建对应的挂载目录
  42. if err := os.Mkdir(mntURL, 0777); err != nil {
  43. log.Errorf("Mkdir dir %s error. %v", mntURL, err)
  44. }
  45. // 拼接参数
  46. // e.g. lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/merged
  47. dirs := "lowerdir=" + rootURL + "busybox" + ",upperdir=" + rootURL + "upper" + ",workdir=" + rootURL + "work"
  48. // dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "busybox"
  49. cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", dirs, mntURL)
  50. cmd.Stdout = os.Stdout
  51. cmd.Stderr = os.Stderr
  52. if err := cmd.Run(); err != nil {
  53. log.Errorf("%v", err)
  54. }
  55. }

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

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

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。

  1. // DeleteWorkSpace Delete the AUFS filesystem while container exit
  2. func DeleteWorkSpace(rootURL string, mntURL string) {
  3. umountOverlayFS(mntURL)
  4. deleteDirs(rootURL)
  5. }
  6. func umountOverlayFS(mntURL string) {
  7. cmd := exec.Command("umount", mntURL)
  8. cmd.Stdout = os.Stdout
  9. cmd.Stderr = os.Stderr
  10. if err := cmd.Run(); err != nil {
  11. log.Errorf("%v", err)
  12. }
  13. if err := os.RemoveAll(mntURL); err != nil {
  14. log.Errorf("Remove dir %s error %v", mntURL, err)
  15. }
  16. }
  17. func deleteDirs(rootURL string) {
  18. writeURL := rootURL + "upper/"
  19. if err := os.RemoveAll(writeURL); err != nil {
  20. log.Errorf("Remove dir %s error %v", writeURL, err)
  21. }
  22. workURL := rootURL + "work"
  23. if err := os.RemoveAll(workURL); err != nil {
  24. log.Errorf("Remove dir %s error %v", workURL, err)
  25. }
  26. }

4. 测试

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

  1. $ ls
  2. busybox.tar

然后启动我们的容器

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

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

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

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

在容器中新建一个文件:

  1. / # echo KubeExplorer > tmp/hello.txt
  2. / # ls /tmp
  3. hello.txt
  4. / # cat /tmp/hello.txt
  5. KubeExplorer

然后切换到宿主机:

  1. root@mydocker:~# ls busybox/tmp
  2. root@mydocker:~# ls upper/tmp
  3. hello.txt
  4. root@mydocker:~# ls merged/tmp
  5. hello.txt

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

这就是 overlayfs 的作用了。

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

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

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

  1. / # exit

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

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

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

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

  1. root@mydocker:~# ls /root/busybox
  2. 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 文件,具体看上一篇文章第二节。

  1. # 克隆代码
  2. git clone -b feat-overlayfs https://github.com/lixd/mydocker.git
  3. cd mydocker
  4. # 拉取依赖并编译
  5. go mod tidy
  6. go build .
  7. # 测试 查看文件系统是否变化
  8. ./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. Couldn't launch Python exit code 9009

    Couldn't launch Python exit code 9009 start stable-diffusion-webui,发现,python 环境没有,我本地其实是已经安装完毕的,后来发现 ...

  2. Midjourney|文心一格prompt教程[技巧篇]:生成多样性、增加艺术风格、图片二次修改、渐进优化、权重、灯光设置等17个技巧等你来学

    Midjourney|文心一格prompt教程[技巧篇]:生成多样性.增加艺术风格.图片二次修改.渐进优化.权重.灯光设置等17个技巧等你来学 1.技巧一:临摹 我认为学习图片类的 prompt,跟学 ...

  3. LyScript 实现Hook改写MessageBox

    LyScript 可实现自定义汇编指令的替换功能,用户可以自行编写一段汇编指令,将程序中特定的通用函数进行功能改写与转向操作,此功能原理是简单的Hook操作. 插件地址:https://github. ...

  4. 1.29 深痛教训 关于 unsigned

    unsigned long long 无符号长长整型,常用于比 long long 大一倍的整数范围或自然溢出 \(\bmod 2^{64}\) unsigned long long 范围为 \(0\ ...

  5. P4093 [HEOI2016/TJOI2016] 序列 题解

    题目链接:序列 对于 LIS 问题,很显而易见的有 dp方程为: \[dp_i=\max{dp_j}+1 \ (j<i,a_j \le a_i) \text{ dp表示以某个位置结尾的最长 LI ...

  6. 零基础入门Vue之To be or not to be——条件渲染

    温故 上一节:零基础入门Vue之皇帝的新衣--样式绑定 在前面的内容能了解到,Vue不仅仅能进行数据渲染还可以对样式进行绑定 并且他能随意的切换样式,但Vue的初衷就是尽量少让使用者操作dom节点 加 ...

  7. CentOS7的udev的绑定规则

    客户一套RAC环境是华为的存储,共享盘是/dev/sd*,咋一看还怀疑是没有进行多路径配置,实际和主机工程师是已经配置好的,我们使用upadmin show vlun命令可以查看到: [root@xx ...

  8. win10远程桌面连接,使用正确的用户名和密码仍然不能成功连接

    最近笔记本重置后,台式使用"远程桌面连接"远程笔记本失败了,总是提示"登录没有成功". 开始自查:win10专业版,允许远程的相关设置也都开了,连接的ip正确, ...

  9. 从零开始学正则(七:终章),详解常用正则API与你可能不知道的正则坑

     壹 ❀ 引 花了差不多半个月的晚上时间,正则入门学习也步入尾声了,当然正则的学习还将继续.不得不说学习成效非常明显,已能看懂大部分正则以及写出不太复杂的正则,比如帮组长写正则验证文件路径正确性,再如 ...

  10. NC235745 拆路

    题目链接 题目 题目描述 有 \(n\) 个城镇,城镇之间有 \(m\) 条道路相连,道路可以看成无向边.每一个城镇都有自己的一个繁荣度 \(v_i\) ,一个城镇 \(u\) 受到的影响 \(p\) ...