最近在写一个小工具,依赖的库比较多,但是这些依赖都不常用,不想为了开发而装在常用的机器上。想用以前用 chroot 搞的一个开发环境,可惜更新源验证总出问题,搜索了几个方案后还是没解决。我只是想搞个隔离的测试环境,不需要限制资源,docker 又庞大配置又麻烦(其实根本原因是我讨厌 golang),又不想重新配置一份环境,于是搜了下 docker 相关的底层技术,最后发现 overlayfs+chroot 就能完美解决我的需求,在这里记录一下,内核版本是 5.6。
overlayfs 介绍
顾名思义,overlayfs 就是把多个目录组合在一起形成一个新的文件系统,在 2014 年被合并进 kernel v3.18。先来看下使用时的命令行参数:
mount -t overlay overlay -o lowerdir=<lowerdir1>:<lowerdir2>:<...>,upperdir=<upperdir>,workdir=<workdir> <mountpoint>
大概介绍下这里的三个参数:
lowerdir
:只读目录,多个目录用 ":" 分隔。upperdir
:可读可写目录。如果upperdir
和lowerdir
中有同名的文件,会优先展示upperdir
中的文件(换言之就是lowerdir
中的文件被屏蔽了)。workdir
:必须是空目录,作用未知。
例如下面的例子:
mkdir low1 low2 low3 upper work overlaydir
touch low1/1.txt low2/2.txt low3/3.txt
mount -t overlay overlay -o lowerdir=./low1:./low2:./low3,upperdir=./upper,workdir=./work overlaydir
执行完上面的命令后:
# ls overlaydir
1.txt 2.txt 3.txt
可以看到三个 lower 目录下的内容被合并到 overlaydir
中。如果这时在 overlaydir
中创建一个新文件:
touch overlaydir/4.txt
用 ls
查看三个 lower 目录都没有改变,但是在 upper
中出现了新建的文件。
接着修改下 3.txt
的内容:
echo "hello, world!" > overlaydir/3.txt
cat overlaydir/3.txt
可以发现的确写入了内容,但是 cat low3/3.txt
可以发现这个文件还是空的,而 upper
目录下多了个 3.txt
,里面的内容正是我们刚才写入的内容。
在 upper
中新建一个文件(根据 参考资料 [1],在目录被挂载的时候修改被挂载的目录可能导致不可预知的问题,这里只是做个实验):
touch upper/5.txt
新建的文件也出现在了 overlaydir
中。接着往新建的文件中写入内容:
echo "ouonline" > overlaydir/5.txt
通过查看可以发现在 upper/5.txt
中保存了我们写入的内容。
overlayfs 的简单用法就这样,知道这些已经可以很好地满足我的需求了,实现细节就没有去深究。
overlayfs+chroot
具体到我的需求,就是先把宿主的环境挂载为只读(不污染宿主环境):
# 创建工作用的目录
mkdir upper work overlaydir
# 把根目录挂载为只读,overlaydir 是最终的挂载点
mount -t overlay overlay -o lowerdir=/,upperdir=./upper,workdir=./work overlaydir
这里将根目录作为 lowerdir。由于我的 /home
在另一个分区,虽然直接把根目录作为 lowerdir,但是进入 overlaydir/home
是看不到 /home
目录下的内容的。如果需要在 chroot 后访问 home
需要重新手动挂载:
mount -t <fs_type> <dev_of_home> overlaydir/home
或者想共享某个目录:
mount --bind /path/to/shared/dir overlaydir/path/to/mount/point
准备好环境后就切换到 overlaydir
:
chroot overlaydir /bin/bash
# 这里是 chroot 到 overlaydir 之后的一些环境初始化
mount -t devpts devpts /dev/pts
mount -t proc proc /proc
mount -t sysfs sysfs /sys
然后像宿主一样正常使用就可以了。做完实验后
umount /sys
umount /proc
umount /dev/pts
exit
umount overlaydir
然后把 upper
中的内容都删掉,重新挂载后就跟实验前的环境一样了。当然也可以新建一个 upperdir 然后挂载到另一个目录中,就可以同时拥有多个不同的环境了,但是存储空间不会线性增长,因为只有被修改过的内容会保存下来。同时内核和其它基础工具都是用的宿主系统的环境,不需要单独维护。
缺点
使用这个方案的好处是轻量而且不污染宿主环境,但是缺点也是很明显的,就是当宿主环境有改动,例如更新了某个包,实验环境可能因为依赖法生变化导致不可用,这种情况下就需要删掉 upper
的内容重新来一遍,挺麻烦的。
要解决这个问题,要么把当前系统打个包(参考 使用 tar 备份 linux)然后解压到某个目录下,或者用 debootstrap
生成一个基础系统,相当于一个固定的 lower
环境,后面宿主怎么改都不影响,但是这样占用空间会多一些;要么是用一些支持快照的文件系统如 bcachefs 和 btrfs,对根目录做个快照,这样会方便些。不过到目前(内核版本 6.11)为止,bcachefs 还没稳定,而 btrfs 总有这样那样的小问题。至于 zfs 每次更新内核都要重新编译,而且用在根目录上还需要一番折腾,就更不推荐了。
其它
由于 overlayfs 不能嵌套,如果需要在 overlayfs 中创建另一个 overlayfs,要把 upper 和 work 放到外部存储(例如上面映射的 /path/to/shared/dir
)或者放到 tmpfs 上(例如放到 /dev/shm
里)。
这样在 overlay 环境内就可以执行
mount -t overlay overlay -o lowerdir=/,upperdir=/path/to/mount/point/upper,workdir=/path/to/mount/point/work /path/to/merged
挂载点 merged
可以位于任意位置。
mount 一般都需要 root 权限才可以执行。如果我们访问挂载的目录不需要 root 权限(例如 lower
是我们有权限读写的一个目录),那么可以用 linux 的 unshare 命令 来切换到一个新的 namespace,然后再执行需要的 mount 操作。