字词数:5341   ✧   阅读量:   ✧  

以下按钮可供你方便地引用本文。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 等工具(不过本文实际上没用到这些工具):

1losetup -f /tmp/vda
2losetup -j /tmp/vda

/tmp/vda 格式化为 BTRFS,然后挂载:2

1mkfs.btrfs /tmp/vda
2mount -m /tmp/vda /tmp/mnt

BTRFS 卷的内部,有着它自己的目录结构,其中最顶级的就是 BTRFS 卷本身,在这个目录结构中的路径就是 /(不要与 Linux 系统本身3/ 混淆)。

因此上面的挂载命令,实际上等价于4

1mount -m /tmp/vda /tmp/mnt -o subvol=/

其中 subvol5的值 / ,就是 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-vol2my-vol3)看起来与我们用 mkdir 创建的普通文件夹 my-dir 并没有什么本质区别(也正因此,有一种命名习惯是将子卷名称用 @ 作为前缀以示区分):

  • 可以用 mv 来移动它。
  • 还可以用 cp -r 来复制它(但所得副本是普通目录而非子卷)。
  • 甚至与 stat /tmp/mnt/my-dir 一样,stat /tmp/mnt/my-vol 也会告诉你它是一个目录(directory)。
  • 想要删除它,也可以用 rm -r6

但对于 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

  • ID 为 256,
  • 上级子卷的 ID 为 5,8
  • 路径为 /my-vol9

而子卷 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 里建立一个空文件,再把顶级卷卸载:

1touch /tmp/mnt/my-vol/my-vol2/testfile
2umount /tmp/mnt

然后重新挂载,但这次用 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/vol1vol/vol2 等,或含有其他文件系统的挂载点,则在 vol 的快照中它们会变成普通的空目录。

例如,我们在 my-vol 中创建测试文件 testAtestB ,再来创建 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 的结果为

1/tmp/mnt1
2├── my-vol2
3│   └── testfile
4├── testA
5└── testB
6
72 directories, 3 files

现在,我们假装不小心删掉了 testB

1rm /tmp/mnt1/testB

这样, tree /tmp/mnt1 的结果变为

1/tmp/mnt1
2├── my-vol2
3│   └── testfile
4└── testA
5
62 directories, 2 files

幸运的是,之前我们给 my-vol 做过快照!现在我们就来“回滚”到它,最简单的方法就是改变 subvolid 为快照的 ID 即 259,这样重新挂载到 /tmp/mnt1

1umount /tmp/mnt1
2mount /tmp/vda /tmp/mnt1 -o subvolid=259

这样“回滚”之后, tree /tmp/mnt1 的结果变为

1/tmp/mnt1
2├── my-vol2
3├── testA
4└── testB
5
62 directories, 2 files

很简单,对吧?

同理,我们也可以改变 subvol 来指定快照子卷:

1umount /tmp/mnt1
2mount /tmp/vda /tmp/mnt1 -o subvol=/my-vol-snapshot

其效果是一致的。

而除了更改挂载参数,我们还可以改变子卷本身的路径。14

首先将子卷 my-vol2my-vol 中移动到快照 my-vol-snapshot15

1mv /tmp/mnt0/{my-vol/,}my-vol2
2mv /tmp/mnt0/{my-vol2,my-vol-snapshot/}

然后,我们将原本的子卷 /my-vol 移动到另一个位置,再把快照子卷移到它原本的位置来完成对它的代替:

1mv /tmp/mnt0/my-vol{,-bad}
2mv /tmp/mnt0/my-vol{-snapshot,}

这样,在“子卷的指定方法是指定其路径为 /my-vol”的前提条件下,我们就已经回滚成功了,以下进行验证(即指定子卷路径 /my-vol 来进行挂载):

1umount /tmp/mnt1
2mount /tmp/vda /tmp/mnt1 -o subvol=/my-vol

tree /tmp/mnt1 的结果为

1/tmp/mnt1
2├── my-vol2
3│   └── testfile
4├── testA
5└── testB
6
72 directories, 3 files

可见回滚操作确实成功了;你还可以用 rm -rf /tmp/mnt1/my-vol-bad 来删掉被我们抛弃的那个子卷,这不会影响你回滚的结果。

总结一下,一般地,我们有三种回滚快照的方法(任选其一):

  • 改变挂载参数中 subvol 的值为快照子卷的路径。
  • 改变挂载参数中 subvolid 的值为快照子卷的 ID。
  • (在挂载参数采用 subvol 且固定不变的前提下)用 mv 移动快照子卷来替换原本的子卷。

扩展:

  • 如果你的快照子卷是只读的,你需要对这个快照子卷本身再次进行快照(当然,这次不可以指定只读属性了),从而得到一个可读写的快照,用它来替代原本的子卷即可。
  • 直接从子卷用 rsynccp 复制文件来覆盖当前子卷,也不失为一种方法,但这并不总是可靠的。

子卷,可以跨设备传输

只读(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 的结果:

1/tmp/mntB
2└── my-recv
3    └── my-vol-ro
4        ├── testA
5        └── testB
6
73 directories, 2 files

上面我们传输了一个完整的只读子卷;之后如果原本的子卷有变动,也可以用 -p <parent> 执行增量传输。

例如在原本的子卷 my-vol 中添加文件 testC 然后创建只读快照 my-vol-ro2

1touch /tmp/mnt0/my-vol/testC
2btrfs subvolume snapshot -r /tmp/mnt0/my-vol /tmp/mnt0/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-rotree /tmp/mntB 的结果也是一样的。这个参数的作用,主要是节省传输所消耗的流量与空间占用。

BTRFS 资料指路

恭喜你完成了本指南!

本文的主要内容到这里也就结束了;17 想了解关于 BTRFS 的更多知识,这里推荐一些资料供参考。

最权威的资料:

一些发行版的 Wiki:


  1. 所谓的“卷”并不是 BTRFS 的独创,例如在 LVM 中也有卷的概念。这里的“卷”类似于“具有文件系统的磁盘分区”,是可以被挂载的对象。 ↩︎

  2. 其中 -m--mkdir 表示在需要时自动创建挂载点目录;部分发行版的 mount 可能不支持这个参数,此时去掉这个参数并手动创建挂载点目录即可。 ↩︎

  3. 严谨地说,是 VFS。 ↩︎

  4. 如果你修改了此 BTRFS 卷的默认子卷,则等价关系可能不再成立。至于什么是子卷,后面将进一步介绍。 ↩︎

  5. subvol 即 subvolume 的简称。 ↩︎

  6. 要删除只读子卷,需要使用 btrfs subvolume delete。 ↩︎

  7. 子卷的“代序数”(generation number)是指一个整数,用以标识每个子卷的创建顺序。这里没有译为“代数”是因为数学上已经有“代数”(algebra);另外这个译法也未必合适,因为 generation 亦有“生成”之意,后续可能会调整。 ↩︎

  8. 这个上级子卷没有出现在列表中,因为它就是 BTRFS 卷本身,ID 固定为 5。 ↩︎

  9. 前面的输出结果中省略了前置的 /。 ↩︎

  10. ID 为 256 的子卷就是 my-vol。 ↩︎

  11. 虽然你很可能用不到,BTRFS 也支持强制为子卷指定容量上限,这个功能叫做“配额”(quota),但目前不太稳定,不建议新手使用。 ↩︎

  12. 透明压缩是 BTRFS 的特色功能之一,它会以一定的 CPU 占用为代价,节省一定空间(具体的压缩率与文件内容有关),并可一定程度上提高读写性能。 ↩︎

  13. 要创建只读快照,需要带 -r 参数。 ↩︎

  14. 但你无法更改子卷的 ID,它是永久的,见 BTRFS 文档。 ↩︎

  15. 你可能注意到,这里嵌套(nesting)的子卷 my-vol2 给快照相关的一些操作带来了麻烦。因此,实践中笔者并不推荐在想要快照的子卷中嵌套子卷,而应改用“在挂载点下的子目录挂载其他子卷”的方案,这个方案同样可以起到避免某些目录被一同纳入快照的效果。 ↩︎

  16. ro 是 read only 的缩写。 ↩︎

  17. 测试时生成的文件在 /tmp 下,会被自动清除;如果你想立即清除它们,可运行: losetup -D /tmp/vda; umount /tmp/mnt{,0,1,B}; rm /tmp/vd{a,b} 。 ↩︎