BTRFS 实践式快速入门
以下按钮可供你方便地引用本文。URL 中即使域名变化,其后的路径也将始终指向原本的内容。
https://css.celestialy.top/p/25dbc0cc9
[BTRFS 实践式快速入门 | clsty 的网络空间站](https://css.celestialy.top/p/25dbc0cc9)
[[https://css.celestialy.top/p/25dbc0cc9][BTRFS 实践式快速入门 | clsty 的网络空间站]]
BTRFS 是 Linux 所支持的一个先进且主流的文件系统。
它的基础使用方法与 EXT4、XFS 等老前辈是相同的;但若你止步于此,就会错过 BTRFS 的高级功能。本文用具体实践带你来快速入门 BTRFS,以便后续进阶。
注:本文假设你具有关于 Linux 下文件系统的基本常识,例如知道如何创建 EXT4 系统,知道如何挂载文件系统。
引子
假如你将块设备(比如 /dev/sda2
)格式化为 BTRFS,你就得到了一个 BTRFS 文件系统,也同时得到了一个 BTRFS 卷。1
如常规的文件系统一样,之后运行 mount /dev/sda2 /mnt
就可以挂载这个 BTRFS 文件系统;不过从 BTRFS 的角度来看,被挂载的正是 BTRFS 卷。
下面我们通过具体的操作来介绍 BTRFS 文件系统。为了测试方便,我们不使用实际的块设备(例如 /dev/sda2
),而是临时创建一个文件 /tmp/vda
来充当块设备,这样对系统就几乎没有影响了。
请打开一个 Linux 下的终端并进入 Bash 或 Zsh,并确保已安装 btrfs-progs
(用于提供 btrfs
命令),下面我们一起实践操作。
格式化与挂载
先切换到 root 用户,方法自行决定,比如运行 sudo su
。
然后,创建一个 256MB 的文件 /tmp/vda
作为测试用的块设备。
1dd if=/dev/zero of=/tmp/vda bs=1M count=256
注:这里还可以用 losetup
将 /tmp/vda
映射为块设备(例如 /dev/loop0
),方便后续利用 lsblk
等工具(不过本文实际上没用到这些工具):
将 /tmp/vda
格式化为 BTRFS,然后挂载:2
BTRFS 卷的内部,有着它自己的目录结构,其中最顶级的就是 BTRFS 卷本身,在这个目录结构中的路径就是 /
(不要与 Linux 系统本身3的 /
混淆)。
因此上面的挂载命令,实际上等价于4
1mount -m /tmp/vda /tmp/mnt -o subvol=/
其中 subvol
5的值 /
,就是 BTRFS 卷本身在它所含目录结构中的路径。
这样,在挂载之后,你就能在挂载点下看到卷所含的目录结构了(这是刚刚新建的卷,所以是空的)。
子卷,是目录?
与其他常规文件系统类似,你可以在这个目录里直接存放文件、文件夹等;但 BTRFS 的特性就在于,你可以选择创建一个子卷(subvolume),例如
1# 创建 BTRFS 子卷 my-vol
2btrfs subvolume create /tmp/mnt/my-vol
3# 创建 BTRFS 子卷 my-vol2
4btrfs subvolume create /tmp/mnt/my-vol/my-vol2
5# 创建普通目录 my-dir,再创建 BTRFS 子卷 my-vol3
6mkdir -p /tmp/mnt/my-dir
7btrfs subvolume create /tmp/mnt/my-dir/my-vol3
小技巧:btrfs
支持子命令缩写,即子命令在避免歧义的前提下可以缩短为前面的一部分。例如创建 my-vol
的命令可简写为 btrfs su cr /tmp/mnt/my-vol
。
表面上,这个子卷 my-vol
(以及 my-vol2
、my-vol3
)看起来与我们用 mkdir
创建的普通文件夹 my-dir
并没有什么本质区别(也正因此,有一种命名习惯是将子卷名称用 @
作为前缀以示区分):
- 可以用
mv
来移动它。 - 还可以用
cp -r
来复制它(但所得副本是普通目录而非子卷)。 - 甚至与
stat /tmp/mnt/my-dir
一样,stat /tmp/mnt/my-vol
也会告诉你它是一个目录(directory)。 - 想要删除它,也可以用
rm -r
。6
但对于 BTRFS 卷来说,这个子卷与其他文件夹有着本质区别。
子卷,能被列出
btrfs
可列出卷中的所有子卷(区别于普通目录):
1btrfs subvolume list /tmp/mnt
输出结果如下:
1ID 256 gen 7 top level 5 path my-vol
2ID 257 gen 7 top level 256 path my-vol/my-vol2
3ID 258 gen 7 top level 5 path my-dir/my-vol3
这里的 gen 7
指的是代序数为 7,因为不是很重要就略过了;7
我们来看其它的部分:
对于子卷 my-vol
,
而子卷 my-vol2
位于 my-vol
下,
- ID 为 257,
- 上级子卷的 ID 为 256,10
- 路径为
/my-vol/my-vol2
。
至于子卷 my-vol3
则位于普通目录 my-dir
下,
- ID 为 258,
- 上级子卷的 ID 为 5,
- 路径为
/my-dir/my-vol3
。
子卷,能指定参数挂载
子卷的一个非常有用的特性是:你可以直接挂载它,而不需要先挂载它的上级(子)卷。
例如,先在 my-vol2
里建立一个空文件,再把顶级卷卸载:
然后重新挂载,但这次用 subvol
来选择子卷 my-vol2
:
1mount -m /tmp/vda /tmp/mnt -o subvol=/my-vol/my-vol2
再来看看挂载点 /tmp/mnt
下有什么:
1ls /tmp/mnt
输出结果如下:
1testfile
这就是我们刚才在子卷 my-vol2
里用 touch
创建的空文件。可见,我们成功地直接挂载了这个子卷(而没有挂载它的顶级卷)。
事实上,不仅挂载单个子卷时并不依赖其顶级卷,你还可以同时挂载 BTRFS 卷中的多个卷。例如,挂载我们刚才卸载的顶级卷 /
到 /tmp/mnt0
:
1mount -m /tmp/vda /tmp/mnt0 -o subvol=/
再来看看挂载点 /tmp/mnt0
下有什么:
1tree /tmp/mnt0
输出结果如下:
1/tmp/mnt0
2├── my-dir
3│ └── my-vol3
4└── my-vol
5 └── my-vol2
6 └── testfile
7
85 directories, 1 file
可见顶级卷也被成功挂载了,而已经被挂载的子卷 my-vol2
作为顶级卷下的子卷,也正常地出现了。
总之,同一个 BTRFS 卷中的各个子卷可以同时被分别挂载(即使它们之间有上下级关系),这有点类似于同一个磁盘上的各个分区;不同的是,对 BTRFS 子卷的调整非常方便快捷,且它们共享同一个分区的容量,因此你不需要像制作磁盘分区或 LVM 那样考虑容量的划分。11
注:你可以对不同子卷分别指定挂载选项,比如,我们可以重新挂载(remount) my-vol2
并开启 zstd 算法的透明压缩(compress=zstd
):12
1mount /tmp/vda /tmp/mnt -o remount,subvol=/my-vol/my-vol2,compress=zstd
作为挂载选项,它只对这次挂载生效,若要长期使用透明压缩则应当把挂载选项配置到 fstab
中。
子卷,能被快照
子卷还支持其他一些特性,其中最值得注意的特性之一就是快照(snapshot)。
当你为某个子卷创建快照时,你相当于复制了这个子卷;但得益于 BTRFS 的 COW(写时复制,Copy On Write),占用的容量并不会翻倍。
与 cp -r
不同,用快照获得的副本是一个内容相同的子卷,而不是普通的目录;换句话说,快照也是子卷。
一个子卷的快照仅针对它本身;若子卷 vol
的下面还包含其他子卷如 vol/vol1
、vol/vol2
等,或含有其他文件系统的挂载点,则在 vol
的快照中它们会变成普通的空目录。
例如,我们在 my-vol
中创建测试文件 testA
和 testB
,再来创建 my-vol
的快照:13
1touch /tmp/mnt0/my-vol/test{A,B}
2btrfs subvolume snapshot /tmp/mnt0/my-vol /tmp/mnt0/my-vol-snapshot
再来看看挂载点 /tmp/mnt0
下有什么:
1tree /tmp/mnt0
输出结果如下:
1/tmp/mnt0
2├── my-dir
3│ └── my-vol3
4├── my-vol
5│ ├── my-vol2
6│ │ └── testfile
7│ ├── testA
8│ └── testB
9└── my-vol-snapshot
10 ├── my-vol2
11 ├── testA
12 └── testB
13
147 directories, 5 files
可见 my-vol
被“复制”为 my-vol-snapshot
。
它们具有相同的内容(注意快照中的 my-vol2
是一个普通的空目录),但不是同一个子卷:
1btrfs subvolume list /tmp/mnt0
输出结果如下:
1ID 256 gen 12 top level 5 path my-vol
2ID 257 gen 10 top level 256 path my-vol/my-vol2
3ID 258 gen 11 top level 5 path my-dir/my-vol3
4ID 259 gen 12 top level 5 path my-vol-snapshot
可以看到它们具有不同的子卷 ID。同时,对它们之一作修改,不会影响到另一个。
子卷,可以“回滚”到快照
回滚(rollback)在一些版本管理系统中,指的是从当前版本回退到旧的版本。
由于我们可以把一个子卷的快照(在未经修改时)看作这个子卷的历史版本,BTRFS 事实上也是支持将子卷“回滚”到指定快照的。
BTRFS 文件系统自身并不提供直接的回滚命令。这是因为,快照本质上仍然是子卷,且与原本的子卷是相互独立的,BTRFS 文件系统本身并不会将它们标记为“具有快照关系”(虽然 COW 特性会使得它们具有的同一份文件不占用两份空间)。也就是说,虽然 BTRFS 支持回滚,但其具体实现是交由用户手动进行、或交给其他工具自动进行的。
那么,当你需要“回滚”子卷到某个快照时,应该怎么操作呢?很简单,就是“用快照子卷替代原本的子卷”。
当然,这需要你首先理解“原本的子卷是如何被指定的”这件事。
前面我们知道,在一个 BTRFS 卷下,每个子卷都有自己的 ID 与路径,用其中的任何一个都可以找到(指定)这个子卷。
例如,已知 /my-vol
的子卷 ID 为 256,可以用 subvolid=256
将它挂载到 /tmp/mnt1
:
1mount /tmp/vda /tmp/mnt1 -m -o subvolid=256
这与你指定 subvol=/my-vol
的效果是一致的。
而 tree /tmp/mnt1
的结果为
现在,我们假装不小心删掉了 testB
:
1rm /tmp/mnt1/testB
这样, tree /tmp/mnt1
的结果变为
幸运的是,之前我们给 my-vol
做过快照!现在我们就来“回滚”到它,最简单的方法就是改变 subvolid
为快照的 ID 即 259,这样重新挂载到 /tmp/mnt1
:
这样“回滚”之后, tree /tmp/mnt1
的结果变为
很简单,对吧?
同理,我们也可以改变 subvol
来指定快照子卷:
其效果是一致的。
而除了更改挂载参数,我们还可以改变子卷本身的路径。14
首先将子卷 my-vol2
从 my-vol
中移动到快照 my-vol-snapshot
中15
然后,我们将原本的子卷 /my-vol
移动到另一个位置,再把快照子卷移到它原本的位置来完成对它的代替:
这样,在“子卷的指定方法是指定其路径为 /my-vol
”的前提条件下,我们就已经回滚成功了,以下进行验证(即指定子卷路径 /my-vol
来进行挂载):
tree /tmp/mnt1
的结果为
可见回滚操作确实成功了;你还可以用 rm -rf /tmp/mnt1/my-vol-bad
来删掉被我们抛弃的那个子卷,这不会影响你回滚的结果。
总结一下,一般地,我们有三种回滚快照的方法(任选其一):
- 改变挂载参数中
subvol
的值为快照子卷的路径。 - 改变挂载参数中
subvolid
的值为快照子卷的 ID。 - (在挂载参数采用
subvol
且固定不变的前提下)用mv
移动快照子卷来替换原本的子卷。
扩展:
- 如果你的快照子卷是只读的,你需要对这个快照子卷本身再次进行快照(当然,这次不可以指定只读属性了),从而得到一个可读写的快照,用它来替代原本的子卷即可。
- 直接从子卷用
rsync
或cp
复制文件来覆盖当前子卷,也不失为一种方法,但这并不总是可靠的。
子卷,可以跨设备传输
只读(read only)子卷可以被跨设备传输;对于可读写的子卷,可手动对它进行只读快照,再传输这个快照子卷。
- 使用
btrfs send <只读子卷>
可以将子卷输出到 stdout, - 使用
btrfs receive <目录>
则从 stdin 接受数据,在新设备的目录下生成同名子卷。
通过管道就能将两者配合起来,甚至可以实现远程传输。
现在,我们为 my-vol
创建一个只读快照子卷:16
1btrfs subvolume snapshot -r /tmp/mnt0/my-vol /tmp/mnt0/my-vol-ro
创建另一个虚拟块设备 vdb
并格式化为 BTRFS:
1dd if=/dev/zero of=/tmp/vdb bs=1M count=256
2mkfs.btrfs /tmp/vdb
3mount -m /tmp/vdb /tmp/mntB
4mkdir -p /tmp/mntB/my-recv
接下来,将只读子卷存放到 vdb
的 BTRFS 卷里:
1btrfs send /tmp/mnt0/my-vol-ro | btrfs receive /tmp/mntB/my-recv
若目标设备在另一台计算机上,可用 btrfs send <只读子卷> | zstd | ssh root@<远程计算机地址> 'zstd -d | btrfs receive <接收目录>'
这种命令,利用 SSH 远程传输。
此时 tree /tmp/mntB
的结果:
上面我们传输了一个完整的只读子卷;之后如果原本的子卷有变动,也可以用 -p <parent>
执行增量传输。
例如在原本的子卷 my-vol
中添加文件 testC
然后创建只读快照 my-vol-ro2
:
利用 -p
指定以 my-vol-ro
为基准,到 my-vol-ro2
的增量传输:
1btrfs send -p /tmp/mnt0/my-vol-ro /tmp/mnt0/my-vol-ro2 | btrfs receive /tmp/mntB/my-recv
此时 tree /tmp/mntB
的结果:
1/tmp/mntB
2└── my-recv
3 ├── my-vol-ro
4 │ ├── testA
5 │ └── testB
6 └── my-vol-ro2
7 ├── testA
8 ├── testB
9 └── testC
10
114 directories, 5 files
当然,即使刚才没有指定 -p /tmp/mnt0/my-vol-ro
,tree /tmp/mntB
的结果也是一样的。这个参数的作用,主要是节省传输所消耗的流量与空间占用。
BTRFS 资料指路
恭喜你完成了本指南!
本文的主要内容到这里也就结束了;17 想了解关于 BTRFS 的更多知识,这里推荐一些资料供参考。
最权威的资料:
- BTRFS | wiki.kernel.org,原本的 BTRFS 官方文档,目前已被归档。
- BTRFS Documentation,最新的 BTRFS 官方文档,但旧文档的内容没有完全迁移过来。
一些发行版的 Wiki:
所谓的“卷”并不是 BTRFS 的独创,例如在 LVM 中也有卷的概念。这里的“卷”类似于“具有文件系统的磁盘分区”,是可以被挂载的对象。 ↩︎
其中
-m
即--mkdir
表示在需要时自动创建挂载点目录;部分发行版的 mount 可能不支持这个参数,此时去掉这个参数并手动创建挂载点目录即可。 ↩︎如果你修改了此 BTRFS 卷的默认子卷,则等价关系可能不再成立。至于什么是子卷,后面将进一步介绍。 ↩︎
subvol 即 subvolume 的简称。 ↩︎
要删除只读子卷,需要使用
btrfs subvolume delete
。 ↩︎子卷的“代序数”(generation number)是指一个整数,用以标识每个子卷的创建顺序。这里没有译为“代数”是因为数学上已经有“代数”(algebra);另外这个译法也未必合适,因为 generation 亦有“生成”之意,后续可能会调整。 ↩︎
这个上级子卷没有出现在列表中,因为它就是 BTRFS 卷本身,ID 固定为 5。 ↩︎
前面的输出结果中省略了前置的
/
。 ↩︎ID 为 256 的子卷就是
my-vol
。 ↩︎虽然你很可能用不到,BTRFS 也支持强制为子卷指定容量上限,这个功能叫做“配额”(quota),但目前不太稳定,不建议新手使用。 ↩︎
透明压缩是 BTRFS 的特色功能之一,它会以一定的 CPU 占用为代价,节省一定空间(具体的压缩率与文件内容有关),并可一定程度上提高读写性能。 ↩︎
要创建只读快照,需要带
-r
参数。 ↩︎你可能注意到,这里嵌套(nesting)的子卷
my-vol2
给快照相关的一些操作带来了麻烦。因此,实践中笔者并不推荐在想要快照的子卷中嵌套子卷,而应改用“在挂载点下的子目录挂载其他子卷”的方案,这个方案同样可以起到避免某些目录被一同纳入快照的效果。 ↩︎ro 是 read only 的缩写。 ↩︎
测试时生成的文件在
/tmp
下,会被自动清除;如果你想立即清除它们,可运行:losetup -D /tmp/vda; umount /tmp/mnt{,0,1,B}; rm /tmp/vd{a,b}
。 ↩︎