Andreas Hartmann 发布的文章

这篇文章将探索 Btrfs 中的透明文件系统压缩,以及它如何帮助节省存储空间。这篇文章是《Btrfs 详解》系列文章中的一篇。从 Fedora Linux 33 开始,Btrfs 就是 Fedora Workstation 和 Fedora Silverblue 的默认文件系统。

如果你错过了,这里是本系列的上一篇文章:Btrfs 详解:快照

简介

很多人都经历过存储空间用完的情况。也许你想从互联网下载一个大文件,或者你需要快速从你的手机中复制些照片,然后操作突然失败。虽然存储空间成本正在稳步降低,但越来越多的设备要么制造时就是固定数量的存储容量,要么最终用户难以扩展其存储容量。

但当你的存储空间不足时你可以做什么呢?也许你会求助于云存储,或者你可以随身携带一些外部存储设备。

在这篇文章里我会研究该问题的另一种解决方案:透明的文件系统压缩,这是 Btrfs 的一个特性。理想情况下,这将解决你的存储问题,同时几乎不需要对你的系统进行修改!让我们来看看是如何做到的。

透明压缩的解释

首先,让我们来探寻 透明 压缩是什么意思。你可以通过像 gzip、xz 或者 bzip2 这些压缩算法去压缩文件。这通常是显式操作:你利用一个压缩工具并且让它操作你的文件。虽然根据文件的内容,节约了空间,这有一个主要的缺点:当你想读取文件或者修改的时候,你得先解压缩。

这不仅是一个乏味的过程,而且也暂时打破了你之前节省的空间。再者,你最终解压了你不想访问的那部分文件内容。明显有比这更好的方法!

相反,透明压缩发生在文件系统级别。在这里,压缩的文件对用户看起来像常规的未压缩文件一样。但是,它们是被压缩后存储在硬盘上的。这之所以可行,是因为操作系统仅仅选择性地访问那部分文件,并且确保在向磁盘写入更新时再次压缩它们。

这里的压缩是透明的在于它不被用户感知,除了在文件访问时可能的 CPU 负载小量增加。因此,你可以应用在已有的系统而不是进行硬件修改或者求助于云存储。

压缩算法对比

Btrfs 提供了多个压缩算法的选择。出于技术原因它不能选用任意的压缩算法。它现在支持:

  • zstd
  • lzo
  • zlib

好消息是,由于透明压缩的工作原理,你不需要安装这些程序供 Btrfs 使用。在下面的文章里,你会看到如何去运行一个简单的性能测试来对比压缩算法。但是,为了运行性能测试,你必须安装必要的可执行文件。事后不需要留着它们,所以你将使用 Podman 容器来确保不会在系统中留下任何痕迹。

注意 :因为 Btrfs 使用的压缩依赖于内核对这些压缩算法的(重新)实现,用户空间版本的算法得出的结果应该认为是粗略估计。

因为一次次敲重复的命令是枯燥的工作,我已经在 Gitlab 上准备了一个可以运行的 Bash 脚本 (https://gitlab.com/hartang/btrfs-compression-test)。这会用上面提到的每个算法在不同的压缩级别运行一次简单的压缩和解压缩。

首先,下载脚本:

$ curl -LO https://gitlab.com/hartang/btrfs-compression-test/-/raw/main/btrfs_compression_test.sh

下一步,启动一个 Fedora Linux 容器去挂载你当前的工作目录,以便你可以和主机交换文件同时在那里运行脚本:

$ podman run --rm -it --security-opt label=disable -v "$PWD:$PWD" \
    -w "$PWD" registry.fedoraproject.org/fedora:37

最后运行脚本:

$ chmod +x ./btrfs_compression_test.sh
$ ./btrfs_compression_test.sh

在我机器上的输出是这样:

[INFO] Using file 'glibc-2.36.tar' as compression target
[INFO] Target file 'glibc-2.36.tar' not found, downloading now...
################################################################### 100.0%
[ OK ] Download successful!
[INFO] Copying 'glibc-2.36.tar' to '/tmp/tmp.vNBWYg1Vol/' for benchmark...
[INFO] Installing required utilities
[INFO] Testing compression for 'zlib'

    Level | Time (compress) | Compression Ratio | Time (decompress)
-------+-----------------+-------------------+-------------------
        1 |         0.322 s |          18.324 % |           0.659 s
        2 |         0.342 s |          17.738 % |           0.635 s
        3 |         0.473 s |          17.181 % |           0.647 s
        4 |         0.505 s |          16.101 % |           0.607 s
        5 |         0.640 s |          15.270 % |           0.590 s
        6 |         0.958 s |          14.858 % |           0.577 s
        7 |         1.198 s |          14.716 % |           0.561 s
        8 |         2.577 s |          14.619 % |           0.571 s
        9 |         3.114 s |          14.605 % |           0.570 s

[INFO] Testing compression for 'zstd'

    Level | Time (compress) | Compression Ratio | Time (decompress)
-------+-----------------+-------------------+-------------------
        1 |         0.492 s |          14.831 % |           0.313 s
        2 |         0.607 s |          14.008 % |           0.341 s
        3 |         0.709 s |          13.195 % |           0.318 s
        4 |         0.683 s |          13.108 % |           0.306 s
        5 |         1.300 s |          11.825 % |           0.292 s
        6 |         1.824 s |          11.298 % |           0.286 s
        7 |         2.215 s |          11.052 % |           0.284 s
        8 |         2.834 s |          10.619 % |           0.294 s
        9 |         3.079 s |          10.408 % |           0.272 s
       10 |         4.355 s |          10.254 % |           0.282 s
       11 |         6.161 s |          10.167 % |           0.283 s
       12 |         6.670 s |          10.165 % |           0.304 s
       13 |        12.471 s |          10.183 % |           0.279 s
       14 |        15.619 s |          10.075 % |           0.267 s
       15 |        21.387 s |           9.989 % |           0.270 s

[INFO] Testing compression for 'lzo'

    Level | Time (compress) | Compression Ratio | Time (decompress)
-------+-----------------+-------------------+-------------------
        1 |         0.447 s |          25.677 % |           0.438 s
        2 |         0.448 s |          25.582 % |           0.438 s
        3 |         0.444 s |          25.582 % |           0.441 s
        4 |         0.444 s |          25.582 % |           0.444 s
        5 |         0.445 s |          25.582 % |           0.453 s
        6 |         0.438 s |          25.582 % |           0.444 s
        7 |         8.990 s |          18.666 % |           0.410 s
        8 |        34.233 s |          18.463 % |           0.405 s
        9 |        41.328 s |          18.450 % |           0.426 s

[INFO] Cleaning up...
[ OK ] Benchmark complete!

重要的是在根据脚本得出的数据做决定之前注意这些事情:

  • 不是所有的文件压缩效果都一样好。像图片或电影这种已经压缩过的现代多媒体格式不会压缩得更小。
  • 脚本中压缩和解压缩各进行一次。重复运行会产生稍微不同的输出。因此,时间应该被理解为是估计,而不是准确的测量。

鉴于输出的数据,我决定在我的系统上使用压缩级别 3 的 zstd 压缩算法。依据你的需求,你可能想使用更高的压缩级别(比如,如果你存储设备相当的慢)。要估算可达到的读/写速度,可以将源存档大小(约 260MB)除以(解)压缩时间。

压缩测试默认是对 GNU libc 2.36 源码进行的。如果你想看看对指定文件的效果,你可以通过第一个参数传递文件路径给脚本。记住文件一定要可以在容器内访问才行。

如果你想要测试其他东西或者执行更加详细的测试,可以阅读脚本的源码,根据需要修改它。

配置 Btrfs 压缩

Btrfs 里的透明文件系统压缩可以通过几种方式配置:

  • 作为挂载文件系统的挂载选项(可用于相同 Btrfs 文件系统的所有子卷)
  • 通过 Btrfs 文件属性
  • btrfs filesystem defrag 时(不是永久的,不在这里介绍)
  • 通过 chattr 文件属性接口(不在这里介绍)

我只会介绍其中前两个。

在挂载时开启压缩

有一个 Btrfs 挂载选项可以开启文件压缩:

$ sudo mount -o compress=<ALGORITHM>:<LEVEL> ...

例如,去挂载一个文件系统,并使用等级 3 的 ztsd 算法去压缩,你可以写成:

$ sudo mount -o compress=zstd:3 ...

设置压缩等级是可选的。重要的是注意到 compress 挂载选项应用到整个 Btrfs 文件系统和它所有的子卷。此外,这是目前唯一支持的指定压缩等级的方式。

为了对文件系统的根应用压缩,必须在 /etc/fstab 上指定。例如,Fedora Linux 安装器,默认启用级别 1 的 zstd 压缩,在 /etc/fstab 里是这样:

$ cat /etc/fstab
[ ... ]
UUID=47b03671-39f1-43a7-b0a7-db733bfb47ff  /  btrfs   subvol=root,compress=zstd:1,[ ... ] 0 0

启用单个文件压缩

另外一种方式指定压缩的方法是通过 Btrfs 文件系统属性。使用下面的命令去查看文件、目录或子卷的压缩设置:

$ btrfs property get <PATH> compression

类似的,你可以像这样配置压缩:

$ sudo btrfs property set <PATH> compression <VALUE>

例如,对在 /etc 下所有文件启用 zlib 压缩:

$ sudo btrfs property set /etc compression zlib

你可以通过 man btrfs-property 得到支持值的列表。记住这个接口不允许指定压缩级别。除此之外,如果设置了一个压缩属性,它会覆盖挂载时的其他压缩配置。

压缩已有文件

在这时,如果你对现有文件系统采用压缩,然后通过 df 或类似命令检查空间利用率,你会发现什么都没变。这是因为 Btrfs 自身不会 “重新压缩” 所有已有的文件。压缩只会发生在往磁盘写新数据的时候。有一些方式去执行显式的重压缩:

  1. 等待,什么都不做:只要文件被修改并被写回磁盘,Btrfs 根据配置压缩新写入的文件内容。如果我们等待足够长,越来越多的文件被重写,在某个时间点就会被压缩。
  2. 移动文件到另一个文件系统然后移动回来:取决于你想压缩哪些文件,这可能是相当乏味的选项。
  3. 执行一次 Btrfs 碎片整理。

最后一个选项可能是最方便的,但是它会对已经包含快照的 Btrfs 文件系统提出警告:它会破坏快照间的共享范围。换句话来说,两个快照间所有的共享内容,或者一个快照和它的父子卷,在碎片整理操作后将保存多份。

因此,如果你在你的文件系统里已经有很多快照,你不应该对整个文件系统运行碎片整理。这也没有必要,因为如果你想的话,Btrfs 可以对特定的目录或者单个文件进行碎片整理。

你可以使用以下命令去执行一次碎片整理:

$ sudo btrfs filesystem defragment -r /path/to/defragment

例如,你想像这样去整理你主目录的碎片:

$ sudo btrfs filesystem defragment -r "$HOME"

如果有疑问,最好从碎片整理单个大文件开始,并在监视文件系统上的可用空间的同时继续处理越来越大的目录。

测量文件系统压缩

有时,你可能会想,文件系统压缩为你节省了多少空间。但如何判断呢?首先,要知道一个 Btrfs 文件系统是否在挂载时启用了压缩,你可以使用以下命令:

$ findmnt -vno OPTIONS /path/to/mountpoint | grep compress

如果你得到了结果,那么给定挂载点的文件系统就使用了压缩!下一步,compsize 命令会告诉你你的文件需要多少空间:

$ sudo compsize -x /path/to/examine

在我的主目录,结果是这样:

$ sudo compsize -x "$HOME"
Processed 942853 files, 550658 regular extents (799985 refs), 462779 inline.
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       81%       74G          91G         111G
none       100%       67G          67G          77G
zstd        28%      6.6G          23G          33G

每一行告诉你应用到文件的压缩 “类型” 。* TOTAL 是下面所有行的总计。

另一方面,这些列告诉你我们的文件需要多少空间:

  • Disk Usage 是实际分配在硬盘上的空间,
  • Uncompressed 是如果没有压缩,文件所需要的空间,
  • Referenced 是所有未压缩文件加起来的总大小。

Referenced 可以与数据 Uncompressed 不同,比如一个文件之前被重复了,或者有快照共享内容。在上面的例子,你可以看到在我的硬盘上总计 91 GB 的未压缩文件仅占据了 74 GB 的存储。取决于在目录里存储的文件类型和应用的压缩等级,这些数字可以有很大差异。

文件压缩的其它注意事项

Btrfs 使用启发式算法去探测压缩文件。这是因为压缩文件通常效果不好,所以没有必要浪费 CPU 周期去尝试进一步的压缩。为了这个目的,Btrfs 在写入压缩数据到磁盘之前测量压缩率。如果文件的第一部分压缩效果不好,文件被标记为不可压缩并且不会有后续的压缩。

如果出于某些原因,你想 Btrfs 压缩所有写入的数据,你可以通过 compress-force 选项挂载一个 Btrfs 文件系统,像这样:

$ sudo mount -o compress-force=zstd:3 ...

当像这样配置,Btrfs 会用等级 3 的 zstd 算法压缩所有写入磁盘的数据。

一个重要的注意事项是挂载一个有很多数据并开启压缩的 Btrfs 文件系统会比没开启压缩耗时更长。这是有技术上的原因的,而且这是一个不会影响文件系统操作的正常行为。

总结

本文详细介绍了 Btrfs 中的透明文件系统压缩。这是一种内置的、相对廉价的方法,可以在不需要修改的情况下从现有硬件中获得一些额外的存储空间。

本系列文章的下一篇将讨论:

  • Qgroups - 限制文件系统大小
  • RAID - 替换 mdadm 配置

(LCTT 译注:后继文章尚未发布,一旦发布我们会尽快翻译。)

如果你想了解与 Btrfs 相关的其他主题,请查看 Btrfs 维基 [1] 和文档 [2] 。如果你还没有阅读本系列的前三篇文章,请不要忘记去看看!如果你觉得本文缺少某些内容,请在下面的评论中让我知道。我们下篇文章见!

参考资料

  1. https://btrfs.wiki.kernel.org/index.php/Main_Page ↩︎
  2. https://btrfs.readthedocs.io/en/latest/Introduction.html ↩︎

(题图:MJ/1a45064c-8da5-4b60-87f2-9886d6a3299e)


via: https://fedoramagazine.org/working-with-btrfs-compression/

作者:Andreas Hartmann 选题:lujun9972 译者:A2ureStone 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇文章会探讨什么是 Btrfs 快照,它们如何工作的,你在日常生活中进行快照的好处。这篇文章是《Btrfs 详解》系列文章中的一篇。从 Fedora Linux 33 开始,Btrfs 就是 Fedora Workstation 和 Fedora Silverblue 的默认文件系统。

如果你错过了,这里是本系列的上一篇文章:Btrfs 详解:子卷

简介

想象一下,你长时间处理一个文件,反复添加和撤销修改。然后,在某个时刻你意识到:两小时前你撤销的部分修改,现在会非常有用。而昨天在你销毁那个设计之前,你也已经修改了这个特殊的部分。当然,由于你会定期保存文件,所以旧的改动会丢失。很多人可能都遇到过这样的情况。如果能恢复旧版本的文件,而无需定期手动复制,岂不美哉?

这是一个 Btrfs 快照可以帮助你的特别场景。当你使用正确的话,快照同时也为你的电脑提供了很好的备份方案。

下面你会找到一些关于快照的例子。如果你想跟着操作,你必须拥有访问某些 Btrfs 文件系统的权限和 root 权限。你可以通过下面命令来验证一个目录的文件系统。

$ findmnt -no FSTYPE /home
btrfs

这个命令会输出你 /home/ 目录的文件系统名称。如果它是 btrfs,那就可以了。让我们创建一个新的目录去做实验:

$ mkdir ~/btrfs-snapshot-test
$ cd ~/btrfs-snapshot-test

在下面的文本中,你会看到很多像上面显示的那样的命令输出框。请在阅读/比较命令输出时请记住,框中的内容在行末会被换行。这使得识别跨多行的长行变得困难,降低了可读性。如果有疑问,试着调整浏览器窗口的大小,看看文本的变化!

Btrfs 快照

让我们从一个基本的问题开始:什么是 Btrfs 快照?如果你在文档 [1] 和维基 [2] 中查找,你不会立刻找到这个问题的答案。事实上,从“功能”一节里是找不到的。如果你搜索一下,你会发现快照和 Btrfs 子卷一起被大量地提及 [3] 。所以现在做什么呢?

还记得快照在系列前面的文章里两次被提到吗?是这样说的:

CoW 的优势在哪里?简单的说:文件被修改和编辑的历史被保存了下来。Btrfs 保存文件旧版本的引用(inode)可以轻易地被访问。这个引用就是快照:文件系统在某个时间点的状态镜像。这将是这系列文章里的单独的一篇,所以暂时留到后面介绍。

—— Btrfs 详解:基础概念

以及:

另外一个分离 //home 的优势是我们可以分别进行 快照 。子卷是快照的边界,对一个子卷的快照永远不会包含该子卷下面的其他子卷的内容。快照的更多细节会在后续的文章中介绍。

—— Btrfs 详解:子卷

看起来快照是和 Btrfs 子卷相关的。你可能之前在其他地方听到过快照,比如说 LVM(逻辑卷管理器)。虽然技术角度上它们都是为了同一个目的,但它们在实现方面有所不同。

每个 Btrfs 快照是一个子卷。但是,不是每个子卷都是一份快照。区别在于子卷里面包含的内容。一个快照是子卷加上一些内容:它包含对现在和过去版本的文件的引用(inode)。让我们看看快照是从哪来的!

创建 Btrfs 快照

想使用快照功能,你需要一个 Btrfs 子卷来进行快照。让我们在测试目录(~/btrfs-snapshot-test)里创建一个:

$ cd ~/btrfs-snapshot-test
$ sudo btrfs subvolume create demo
Create subvolume './demo'
$ sudo chown -R $(id -u):$(id -g) demo/
$ cd demo

因为 Btrfs 子卷默认是被 root 所有的,你必须用 chown 去修改子卷里的文件的所有权到普通用户上。现在我们在里面新加一些文件:

$ touch foo bar baz
$ echo "Lorem ipsum dolor sit amet, " > foo

你的目录现在看起来像这样:

$ ls -l
total 4
-rw-r--r--. 1 hartan hartan  0 Dec 20 08:11 bar
-rw-r--r--. 1 hartan hartan  0 Dec 20 08:11 baz
-rw-r--r--. 1 hartan hartan 29 Dec 20 08:11 foo

让我们从这里创建第一次快照:

$ cd ..
$ sudo btrfs subvolume snapshot demo demo-1
Create a snapshot of 'demo' in './demo-1'

这就好了。让我们看看发生了什么:

$ ls -l
total 0
drwxr-xr-x. 1 hartan hartan 18 Dec 20 08:11 demo
drwxr-xr-x. 1 hartan hartan 18 Dec 20 08:11 demo-1
$ tree
.
├── demo
│   ├── bar
│   ├── baz
│   └── foo
└── demo-1
    ├── bar
    ├── baz
    └── foo

2 directories, 6 files

这看起来是一份拷贝!为了验证,我们从快照里读取 foo 的内容:

$ cat demo/foo
Lorem ipsum dolor sit amet,
$ cat demo-1/foo
Lorem ipsum dolor sit amet,

当我们修改原始文件时,真正的效果变得明显:

$ echo "consectetur adipiscing elit, " >> demo/foo
$ cat demo/foo
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
$ cat demo-1/foo
Lorem ipsum dolor sit amet,

这表明快照仍然持有“旧”版本的数据:foo 的内容没有改变。到目前为止,你可以通过一个简单的文件复制来实现完全相同的目标。现在你也可以继续处理旧文件了。

$ echo "sed do eiusmod tempor incididunt" >> demo-1/foo
$ cat demo-1/foo
Lorem ipsum dolor sit amet,
sed do eiusmod tempor incididunt

但是在底层,我们的快照实际上是一个新的 Btrfs 子卷。你可以通过下面的命令来验证这一点:

$ sudo btrfs subvolume list -o .
ID 259 gen 265 top level 256 path home/hartan/btrfs-snapshot-test/demo
ID 260 gen 264 top level 256 path home/hartan/btrfs-snapshot-test/demo-1

Btrfs 子卷 vs. 文件复制

这一切有什么意义呢?到目前为止快照看起来是一个更加复杂的复制文件的方式。事实上,快照不仅仅是表面上看起来那么简单。让我们来创建一个更大的文件:

$ dd if=/dev/urandom of=demo/bigfile bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 1.3454 s, 399 MB/s

现在有一个512 MB 大小的新文件 demo/bigfile 。让我们创建另一个快照,这样在你修改数据的时候就不会丢失:

$ sudo btrfs subvolume snapshot demo demo-2
Create a snapshot of 'demo' in './demo-2'

现在我们通过追加少量字符串到文件来模拟变化:

$ echo "small changes" >> demo/bigfile

这是生效后的文件结构:

$ tree
.
├── demo
│   ├── bar
│   ├── baz
│   ├── bigfile
│   └── foo
├── demo-1
│   ├── bar
│   ├── baz
│   └── foo
└── demo-2
    ├── bar
    ├── baz
    ├── bigfile
    └── foo

3 directories, 11 files

但是真正的神奇的发生在其他地方。你已经复制了 demo/bigfile ,你现在拥有了两个大约 512 MiB 的文件。但是,因为它们是不同的拷贝,它们应该会占据共 1 GiB 的空间。记住两个文件的差异不超过 10 字节 —— 和原文件大小相比这几乎没什么差别。

Btrfs 快照工作原理与文件复制不同:而是它们保持对当前和过去的 inode 的引用。当你在文件追加更新时,在底层 Btrfs 分配更多的空间去存储更新,同时在原来的 inode 增加对新数据的引用。之前的内容保持不变。为了便于理解,你可以认为这是仅仅“存储”原文件和修改版本的差异。

让我们看看这个效果:

$ sudo compsize .
Processed 11 files, 5 regular extents (9 refs), 3 inline.
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL      100%      512M         512M         1.0G
none       100%      512M         512M         1.0G

这个有趣的数字出现在 TOTAL 一行:

  • Referenced 是当前目录下所有文件大小的总和
  • Disk Usage 是用于在磁盘上存储文件分配空间的大小

你有一共 1 GiB 的文件,但存储它们仅仅占据了 512 MiB。

Btrfs 快照和备份

目前为止,在这篇文章中,你已经看到如何创建 Btrfs 快照和它们的特别之处。有人可能会想:如果我在我的 PC 本地进行一系列的快照,我就有一个可靠的备份策略。 其实不是这样的 。如果 Btrfs 子卷共享的底层数据被偶然破坏了(被 Btrfs 之外的东西影响,比如宇宙射线),所有指向这些数据的子卷都会存在相同的错误。

为了让快照成为真正的备份,你应该将它们存储到一个不同的 Btrfs 系统上,例如在一个外部驱动器上。为了本文的目的,让我们在一个文件里创建一个新的 Btrfs 系统,并挂载它来模拟一个外部驱动。如果你有一个格式为 Btrfs 的外部驱动器,请随意替换以下命令中提到的所有路径来试试!让我们创建一个新的 Btrfs 文件系统:

注意:下面的命令会在你的文件系统上创建一个 8 GB 大小的新文件。如果你想跟着下面的步骤,请确保你的磁盘空间至少有 8 GB 剩余。请不要分配小于 8 GB 到这个文件,否则 Btrfs 可能在挂载时会遇到问题。

$ truncate -s 8G btrfs_filesystem.img
$ sudo mkfs.btrfs -L "backup-drive" btrfs_filesystem.img
btrfs-progs v5.18
See http://btrfs.wiki.kernel.org for more information.

[ ... ]

Devices:
    ID        SIZE  PATH
    1     8.00GiB  btrfs_filesystem.img

这些命令创建了名为 btrfs_filesystem.img 的 8 GB 新文件,同时在上面格式化了一个 Btrfs 文件系统。现在你可以像外部驱动器一样挂载它:

$ mkdir backup-drive
$ sudo mount btrfs_filesystem.img backup-drive
$ sudo chown -R $(id -u):$(id -g) backup-drive
$ ls -lh
total 4.7M
drwxr-xr-x. 1 hartan hartan    0 Dec 20 08:35 backup-drive
-rw-r--r--. 1 hartan hartan 8.0G Dec 20 08:37 btrfs_filesystem.img
drwxr-xr-x. 1 hartan hartan   32 Dec 20 08:14 demo
drwxr-xr-x. 1 hartan hartan   18 Dec 20 08:11 demo-1
drwxr-xr-x. 1 hartan hartan   32 Dec 20 08:14 demo-2

妙,现在挂载在 backup-drive 下面有一个独立的 Btrfs 文件系统!让我们尝试进行快照并且把快照放进去:

$ sudo btrfs subvolume snapshot demo backup-drive/demo-3
Create a snapshot of 'demo' in 'backup-drive/demo-3'
ERROR: cannot snapshot 'demo': Invalid cross-device link

发生了什么?噢,你尝试对 demo 进行一次快照并把它存在不同的 Btrfs 文件系统里(从 Btrfs 视角来看是一个不同的设备)。还记得一个 Btrfs 子卷仅持有对文件和内容的引用(inode)?这正是问题所在:文件和内容存在于我们的 home 文件系统,但不在新创建的 backup-drive 。你得找到一种方式去传输子卷和其内容到新的文件系统里。

在不同的 Btrfs 文件系统存储快照

针对这个目的 Btrfs 工具有两个特殊的命令。让我们首先来看看它们是如何工作的:

$ sudo btrfs send demo | sudo btrfs receive backup-drive/
ERROR: subvolume /home/hartan/btrfs-snapshot-test/demo is not read-only
ERROR: empty stream is not considered valid

另一个错误!这时它告诉你我们想要传输的子卷不是只读的。这是对的:你可以写入新内容到所有目前为止创建的快照/子卷。你可以像这样创建一个只读的快照:

$ sudo btrfs subvolume snapshot -r demo demo-3-ro
Create a readonly snapshot of 'demo' in './demo-3-ro'

不像之前那样,这里 -r 选项被加到了 snapshot 子命令里。这创建一个只读的快照,这很容易去验证:

$ touch demo-3-ro/another-file
touch: cannot touch 'demo-3-ro/another-file': Read-only file system

现在你可以重新尝试传输子卷:

$ sudo btrfs send demo-3-ro | sudo btrfs receive backup-drive/
At subvol demo-3-ro
At subvol demo-3-ro
$ tree

├── backup-drive
│   └── demo-3-ro
│       ├── bar
│       ├── baz
│       ├── bigfile
│       └── foo
├── btrfs_filesystem.img
├── demo
[ ... ]
└── demo-3-ro
    ├── bar
    ├── baz
    ├── bigfile
    └── foo

6 directories, 20 files

成功了!你成功传输原来子卷 demo 的一个只读快照到一个外部的 Btrfs 文件系统。

在非 Btrfs 文件系统存储快照

上面你已经看到你如何能存储 Btrfs 子卷/快照到其他的 Btrfs 文件系统。但如果你没有其他的 Btrfs 文件系统并且不能新创建一个,比如说外部驱动器需要一个和 Windows 或 MacOS 兼容的文件系统,你可以做什么呢?在这种情况下你可以存储子卷在文件里:

$ sudo btrfs send -f demo-3-ro-subvolume.btrfs demo-3-ro
At subvol demo-3-ro
$ ls -lh demo-3-ro-subvolume.btrfs
-rw-------. 1 root root 513M Dec 21 10:39 demo-3-ro-subvolume.btrfs

文件 demo-3-ro-subvolume.btrfs 现在包含了随后重建 demo-3-ro 子卷需要的所有东西。

增量地发送快照

如果你对不同的子卷重复执行这个操作,你会发现在某些时间点不同的子卷不再共享它们的文件内容。这是因为像上面一样发送一个子卷,去重建这个单独的子卷的所有数据将被传送到目标位置。但是,你可以引导 Btrfs 只向目标位置发送不同子卷的差异!所谓的增量发送将保证共享的引用在子卷中仍然共享。为了展示这一点,新增一些变动到我们原来的子卷:

$ echo "a few more changes" >> demo/bigfile

然后创建另一个只读子卷:

$ sudo btrfs subvolume snapshot -r demo demo-4-ro
Create a readonly snapshot of 'demo' in './demo-4-ro'

然后现在发送它:

$ sudo btrfs send -p demo-3-ro demo-4-ro | sudo btrfs receive backup-drive
At subvol demo-4-ro
At snapshot demo-4-ro

在上面的命令,-p 选项指定了一个父子卷用来计算差异。重要的是记住原 Btrfs 文件系统和目标 Btrfs 文件系统都必须包含相同的、未被修改过的父子卷!确保新的子卷真的在那里:

$ ls backup-drive/
demo-3-ro  demo-4-ro
$ ls -lR backup-drive/demo-4-ro/
backup-drive/demo-4-ro/:
total 524296
-rw-r--r--. 1 hartan hartan         0 Dec 20 08:11 bar
-rw-r--r--. 1 hartan hartan         0 Dec 20 08:11 baz
-rw-r--r--. 1 hartan hartan 536870945 Dec 21 10:49 bigfile
-rw-r--r--. 1 hartan hartan        59 Dec 20 08:13 foo

但你怎样知道增量发送只传输了子卷间的差异呢?让我们传输数据流到一个文件里然后看看它有多大:

$ sudo btrfs send -f demo-4-ro-diff.btrfs -p demo-3-ro demo-4-ro
At subvol demo-4-ro
$ ls -l demo-4-ro-diff.btrfs
-rw-------. 1 root root 315 Dec 21 10:55 demo-4-ro-diff.btrfs

根据 ls ,这个文件仅仅只有 315 字节大小!这意味着增量传输只传输子卷间的差异,和额外的 Btrfs 相关的元数据。

从快照中恢复子卷

在继续之前,让我们清理掉这时候不再需要的东西:

$ sudo rm -rf demo-4-ro-diff.btrfs demo-3-ro-subvolume.btrfs
$ sudo btrfs subvolume delete demo-1 demo-2 demo-3-ro demo-4-ro
$ ls -l
total 531516
drwxr-xr-x. 1 hartan hartan         36 Dec 21 10:50 backup-drive
-rw-r--r--. 1 hartan hartan 8589934592 Dec 21 10:51 btrfs_filesystem.img
drwxr-xr-x. 1 hartan hartan         32 Dec 20 08:14 demo

到目前为止你已经成功创建了可读/写和只读的 Btrfs 子卷快照,并把它们发送到外部。但是,为了把这作为备份策略,还要有一种方式去发送子卷回原来的文件系统和让它们再次变为可写。出于这个目的,我们移动 demo 子卷到其他地方并且尝试从最近的快照中重建它。第一步:重命名为 broken 子卷。一旦恢复成功它会被删除:

$ mv demo demo-broken

第二步: 传回最近的快照到这个文件系统:

$ sudo btrfs send backup-drive/demo-4-ro | sudo btrfs receive .
At subvol backup-drive/demo-4-ro
At subvol demo-4-ro
[hartan@fedora btrfs-snapshot-test]$ ls
backup-drive  btrfs_filesystem.img  demo-4-ro  demo-broken

第三步: 从快照创建一个可读写的子卷:

$ sudo btrfs subvolume snapshot demo-4-ro demo
Create a snapshot of 'demo-4-ro' in './demo'
$ ls
backup-drive  btrfs_filesystem.img  demo  demo-4-ro  demo-broken

上一步非常重要:你不能重命名 demo-4-rodemo ,因为这仍然是一个只读子卷!最后你可以检查你所有你想要的东西是不是在那里:

$ tree demo
demo
├── bar
├── baz
├── bigfile
└── foo

0 directories, 4 files
$ tail -c -19 demo/bigfile
a few more changes

最后的命令告诉你 bigfile 的最后 19 个字符实际上是上次变更执行的结果。这个时候,你可能想从 demo-broken 复制最近的更新到新的 demo 子卷。因为你没有执行任何更新,你可以过时的子卷:

$ sudo btrfs subvolume delete demo-4-ro demo-broken
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-4-ro'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-broken'

就是这样!你成功从一个之前存在不同 Btrfs 文件系统(外部介质)的快照中恢复 demo 子卷。

子卷作为快照的边界

在本系列的第二篇文章中我提到子卷作为快照的边界,但这到底是什么意思呢?简单来说,子卷的一份快照仅包含这个子卷的内容,而不是下面嵌套的子卷。让我们来看看这个:

$ sudo btrfs subvolume create demo/nested
Create subvolume 'demo/nested'
$ sudo chown -R $(id -u):$(id -g) demo/nested
$ touch demo/nested/another_file

让我们像以前一样进行一次快照:

$ sudo btrfs subvolume snapshot demo demo-nested
Create a snapshot of 'demo' in './demo-nested'

然后查看里面的内容:

$ tree demo-nested
demo-nested
├── bar
├── baz
├── bigfile
├── foo
└── nested

1 directory, 4 files

$ tree demo
demo
├── bar
├── baz
├── bigfile
├── foo
└── nested
    └── another_file

1 directory, 5 files

注意到 another_file 不见了,仅仅是目录 nested 还在。这是因为 nested 是一个子卷:demo 的快照包含嵌套子卷的目录(挂载点),但里面的内容是缺失的。目前没有方法递归地执行快照去包含嵌套子卷。但是,我们可以利用这个优势去从快照中排除一些目录!这通常对那些你容易再现的数据有用,或者它们很少变化。例子有虚拟机或者容器镜像,电影,游戏文件等等。

在总结之前,我们移除所有测试过程中创建的东西:

$ sudo btrfs subvolume delete demo/nested demo demo-nested
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo/nested'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-nested'
$ sudo umount backup-drive
$ cd ..
$ rm -rf btrfs-snapshot-test/

基于 Btrfs 备份最后思考

如果你决定使用 Btrfs 来执行数据的定期备份,那么你可能需要使用一个工具来自动完成这项任务。Btrfs 维基有一个专门针对 Btrfs [4] 的备份工具列表。在那里,你还将看到另一个手动执行 Btrfs 备份步骤的摘要。就我个人而言,我对 btrbk [5] 有很多很好的体验,我正在使用它来执行我自己的备份。除了备份之外,btrbk 还可以在你的 PC 本地保存 Btrfs 快照列表。我使用它来防止意外的数据删除。

如果你想要了解更多有关使用 Btrfs 进行备份的内容,欢迎在下面评论,我会考虑写一篇专门讨论这个话题的后续文章。

总结

本文研究了 Btrfs 快照,它们本质上是 Btrfs 子卷。你了解了如何创建可读写和只读快照,以及这种机制如何有助于防止数据丢失。

本系列的后续文章将讨论:

  • 压缩 - 透明地节省存储空间
  • 配额组 - 限制文件系统大小
  • RAID - 替代 mdadm 配置

如果你还想了解与 Btrfs 相关的其他主题,请查看 Btrfs 维基 [2:1] 和文档 [1:1] 。不要忘记查看本系列的前两篇文章!如果你认为本文缺少了一些内容,请在下面的评论中告诉我们。再会!

参考资料

  1. https://btrfs.readthedocs.io/en/latest/Introduction.html ↩︎ ↩︎
  2. https://btrfs.wiki.kernel.org/index.php/Main_Page ↩︎ ↩︎
  3. https://btrfs.readthedocs.io/en/latest/Subvolumes.html ↩︎
  4. https://btrfs.wiki.kernel.org/index.php/Incremental_Backup#Available_Backup_Tools ↩︎
  5. https://github.com/digint/btrbk ↩︎

(题图:MJ/ad8a61ed-ce6f-409e-b503-69104dd71149)


via: https://fedoramagazine.org/working-with-btrfs-snapshots/

作者:Andreas Hartmann 选题:lujun9972 译者:A2ureStone 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇文章是《Btrfs 详解》系列文章中的一篇。从 Fedora Linux 33 开始,Btrfs 就是 Fedora Workstation 和 Fedora Silverblue 的默认文件系统。

以防你忘记,这是系列文章中的前一篇:Btrfs 详解:基础概念

简介

子卷 Subvolume 允许将一个 Btrfs 文件系统划分成多个独立的子文件系统。这意味着你可以从 Btrfs 文件系统挂载子卷,就好像它们是独立的文件系统。除此之外,例如,你还可以通过 限额组 qgroup (我们将在本系列的另一篇文章里介绍)定义子卷能够占据的最大空间,或者用子卷去包含或排除快照中的文件(我们会后面的文章中会讲到)。自 Fedora Linux 33 后每个 Fedora Workstation 和 Fedora Silverblue 默认安装过程中会利用子卷。在这篇文章中我们会介绍它是如何工作的。

下面你会找到很多关于子卷的例子。如果你想跟着操作,你必须拥有访问某些 Btrfs 文件系统的权限和 root 权限。你可以通过下面命令来验证你的 /home/ 目录是否是 Btrfs 。

$ findmnt -no FSTYPE /home
btrfs

这个命令会输出你 /home/ 目录的文件系统名。如果它是 btrfs,那就可以了。让我们创建一个新的目录去做实验:

$ mkdir ~/btrfs-subvolume-test
$ cd ~/btrfs-subvolume-test

在下面的文本中,你会看到很多像上面显示的那样的命令输出框。请在阅读/比较命令输出时请记住,框中的内容在行末会被换行。这使得识别跨多行的长行变得困难,降低了可读性。如果有疑问,试着调整浏览器窗口的大小,看看文本的变化!

创建和使用子卷

我们可以通过以下命令创建一个 Btrfs 子卷:

$ sudo btrfs subvolume create first
Create subvolume './first'

当我们检查当前目录,我们可以看到现在有一个名为 first 的新目录。注意到下面输出的第一个字符 d

$ ls -l
total 0
drwxr-xr-x. 1 root root 0 Oct 15 18:09 first

我们可以像常规目录一样操作它:我们可以重命名它,移动它,在里面创建新文件和目录,等等。注意到目录属于 root,所以我们必须以 root 身份去做这些事情。

如果它表现和看起来就像个目录,那我们如何知道这是不是一个 Btrfs 子卷呢?我们可以使用 btrfs 工具去列出所有子卷:

$ sudo btrfs subvolume list .
ID 256 gen 30 top level 5 path home
ID 257 gen 30 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 29 top level 256 path hartan/btrfs-subvolume-test/first

如果你安装的是最新的 Fedora Linux,且未修改过,你很可能会看到和上面一样的输出。我们会在之后检查 homeroot ,还有全部数字的含义。现在,我们看到在我们指定的路径下有一个子卷。我们可以将输出限制在我们当前位置下面的子卷:

$ sudo btrfs subvolume list -o .
ID 259 gen 29 top level 256 path home/hartan/btrfs-subvolume-test/first

让我们重命名子卷:

$ sudo mv first second
$ sudo btrfs subvolume list -o .
ID 259 gen 29 top level 256 path home/hartan/btrfs-subvolume-test/second

我们还可以嵌套子卷:

$ sudo btrfs subvolume create second/third
Create subvolume 'second/third'
$ sudo btrfs subvolume list .
ID 256 gen 34 top level 5 path home
ID 257 gen 37 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 37 top level 256 path hartan/btrfs-subvolume-test/second
ID 260 gen 37 top level 259 path hartan/btrfs-subvolume-test/second/third

我们也可以移除子卷,就像移除目录一样:

$ sudo rm -r second/third

或者通过特殊的 Btrfs 命令:

$ sudo btrfs subvolume delete second
Delete subvolume (no-commit): '/home/hartan/btrfs-subvolume-test/second'

像单独的文件系统一样操作子卷

前面的简介里说 Btrfs 子卷就好像单独的文件系统。这意味着我们可以挂载子卷并且传递一些挂载选项给它。我们先创建一个小的目录结构去更好的理解发生了什么:

$ mkdir -p a a/1 a/1/b
$ sudo btrfs subvolume create a/2
Create subvolume 'a/2'
$ sudo touch a/1/c a/1/b/d a/2/e

这就是目录结构的样子:

$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   └── d
    │   └── c
    └── 2
        └── e

4 directories, 3 files

验证现在这里有一个新的 Btrfs 子卷:

$ sudo btrfs subvolume list -o .
ID 261 gen 41 top level 256 path home/hartan/btrfs-subvolume-test/a/2

为了挂载子卷,我们必须知道 Btrfs 子卷所在的块设备路径。下面的命令会告诉我们:

$ findmnt -vno SOURCE /home/
/dev/vda3

现在我们挂载子卷。确保你将参数替换成你 PC 上的:

$ sudo mount -o subvol=home/hartan/btrfs-subvolume-test/a/2 /dev/vda3 a/1/b

观察到我们使用 -o 参数去提供额外的选项去挂载程序。在这里我们告诉它挂载在设备 /dev/vda3 上 btrfs 文件系统里名为 home/hartan/btrfs-subvolume-test/a/2 的子卷。这是 Btrfs 特有的选项,在其他文件系统里没有的。

我们可以看到目录结构变化了:

$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   └── e
    │   └── c
    └── 2
        └── e

4 directories, 3 files

现在文件 e 出现了两次, d 不见了。我们现在可以用两个不同的路径访问相同的 Btrfs 子卷。在一个路径的所有变化会被立刻反应在其他的位置:

$ sudo touch a/1/b/x
$ ls -lA a/2
total 0
-rw-r--r--. 1 root root 0 Oct 15 18:14 e
-rw-r--r--. 1 root root 0 Oct 15 18:16 x

让我们尝试更多的挂载选项。例如我们可以像这样以只读方式挂载子卷到 a/1/b(插入你 PC 的参数):

$ sudo umount a/1/b
$ sudo mount -o subvol=home/hartan/btrfs-subvolume-test/a/2,ro /dev/vda3 a/1/b

我们和上面使用相同的命令,除了我们加上了 ro 在末尾。现在我们不能在这个挂载点上创建文件:

$ sudo touch a/1/b/y
touch: cannot touch 'a/1/b/y': Read-only file system

但直接访问子卷仍然像之前一样:

$ sudo touch a/2/y
$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   ├── e
    │   │   ├── x
    │   │   └── y
    │   └── c
    └── 2
        ├── e
        ├── x
        └── y

4 directories, 7 files

在下一步之前不要忘记进行清理:

$ sudo rm -rf a
rm: cannot remove 'a/1/b/e': Read-only file system
rm: cannot remove 'a/1/b/x': Read-only file system
rm: cannot remove 'a/1/b/y': Read-only file system

天啊,发生了什么?噢,因为我们在上面挂载只读子卷,所以不能删除它。从文件系统的角度来看,删除是一种写入操作:为了删除 a/2/b/e,我们从父目录 a/1/b 的内容中删除目录项 e。换句话来说,我们必须 写入 a/1/b 去表明 e 不复存在。所以我们先卸载子卷,然后移除目录:

$ sudo umount a/1/b
$ sudo rm -rf a
$ tree
.

0 directories, 0 files

子卷 ID

还记得 btrfs subvolume list 命令的第一次输出吗?那包含了很多数字,让我们看看这些究竟什么。我在这里复制了输出,以便再次查看:

ID 256 gen 30 top level 5 path home
ID 257 gen 30 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 29 top level 256 path hartan/btrfs-subvolume-test/first

我们看到有三列数字,每个前面有一些字母来描述它们的作用。第一列是子卷 ID 。子卷 ID 在 Btrfs 文件系统是唯一的,而且唯一地标识子卷。这意味着名为 home 的子卷也可以用它的 ID 256 来引用。之前的挂载命令是这样写的:

$ sudo mount -o subvol=hartan/...

另外一个完全合法的选择是使用子卷 ID :

$ sudo mount -o subvolid=...

子卷 ID 从 256 开始,每创建一个子卷依次递增 1 。但是在这里有一个例外:文件系统的根的子卷名称总是为 /,并且子卷 ID 是 5 。没错,即使文件系统的根技术上也是一个子卷。这是不言而喻的,因此不会出现在 btrfs subvolume 的输出列表里。如果你没有用 subvolsubvolid 参数去挂载一个 Btrfs 文件系统,subvolid=5 的顶级子卷就是默认的挂载对象。下面我们会看到一个想要显式挂载文件系统根的例子。

第二列的数字是生成号,并且在每次 Btrfs 事务中递增。这几乎是一个内部的计数器,我们不会在这里讨论。

最后,第三列数字是 子卷的子卷 ID。在上面的输出我们可以看到子卷 homeroot 的父子卷 ID 都是 5。记住 ID 5 的特殊含义:这是文件系统的根。所以我们知道 homeroot 都是顶级子卷的子卷。另一方面 hartan/btrfs-subvolume-test.first 是子卷 ID 256(也就是 home)的子卷。

在下一节我们会看看子卷 roothome 是怎么来的。

检查 Fedora Linux 的默认子卷

当你从头创建一个新的 Btrfs 文件系统,里面是没有子卷的(当然,除了顶级子卷)。所以 Fedora Linux 里的 homeroot 子卷是哪里来的?

它们是安装程序在安装时创建的。传统的安装经常会为 //home 目录包含单独的文件系统分区。在启动时,它们通过恰当的挂载组成一个完整的文件系统。但这个方法有一个问题:除非你使用像 lvm 这样的技术,想在将来改变分区的大小是非常难的。因而你可能出现 //home 用完空间的情况,然而还有很多其他没被使用的分区和空间剩余。

因为 Btrfs 子卷全都是相同文件系统的一部分,它们共享底层文件系统提供的空间。还记得我们在上面创建的子卷吗?我们从未告诉 Btrfs 它们多大:一个子卷可以占据文件系统拥有的全部空间,默认是不会阻止这种行为的。但是,我们 可以 通过 Btrfs 的 限额组 qgroup 动态地约束其大小,同时也可以在运行时修改(我们将在后续的文章中了解如何做的)。

另外一个分离 //home 的优势是我们可以分别进行 快照 。子卷是快照的边界,对一个子卷的快照永远不会包含该子卷下面的其他子卷的内容。快照的更多细节会在后续的文章中介绍。

理论已经足够了!我们来看看这是怎么回事。首先确保你的根文件系统类型是 Btrfs :

$ findmnt -no FSTYPE /
btrfs

然后我们获取它所在的分区:

$ findmnt -vno SOURCE /
/dev/vda3

记住我们可以通过特殊的子卷 ID 5 挂载文件系统的根(适应文件系统分区!):

$ mkdir fedora-rootsubvol
$ sudo mount -o subvolid=5 /dev/vda3 ./fedora-rootsubvol
$ ls fedora-rootsubvol/
home  root

而且还有 Fedora Linux 安装的子卷!但 Fedora Linux 是如何知道子卷 root 属于 / ,而 home 属于 /home 的呢?

文件 /etc/fstab 包含了所谓的文件系统的静态信息。简而言之,在你系统启动的时候会一行一行地读取这个文件,然后挂载那里列出的所有文件系统。在我的系统上,这个文件长这样:

$ cat /etc/fstab
# [ ... ]
# /etc/fstab
# Created by anaconda on Sat Oct 15 12:01:57 2022
# [ ... ]
#
UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /                       btrfs   subvol=root,compress=zstd:1 0 0
UUID=e3a798a8-b8f2-40ca-9da7-5e292a6412aa /boot                   ext4    defaults        1 2
UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /home                   btrfs   subvol=home,compress=zstd:1 0 0

(注意上面的 “UUID” 开头行的内容被换行成两行)

每行开头的 UUID 用于标识你系统上的硬盘和文件系统分区(大概相当于我在上面使用的 /dev/vda3 )。第二列是文件系统应该挂载在文件系统树上的路径。第三列是文件系统类型。我们可以看到 //home 都是 btrfs 类型,正如我们期望的那样!最后,第四列是:这些是挂载选项,这里说通过 subvol=root 选项去挂载 / 。这正是我们一直在 btrfs subvolume list / 里看到的输出!

有了这些信息,我们可以重新构建创建这个文件系统项的 mount 命令

$ sudo mount -o subvol=root,compress=zstd:1 UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /

(再次,上面的 “UUID” 开头行的内容被换行成两行)

这就是 Fedora Linux 如何使用 Btrfs 子卷!如果你对好奇 Fedora Linux 为什么选择 Btrfs 作为默认的文件系统,请参阅下面链接的更改提议 [1]

Btrfs 子卷的更多内容

Btrfs 维基提供了关于子卷的更多信息,其中最重要的是可应用于 Btrfs 子卷的挂载选项。有些选项,比如 compress 只能应用到文件系统的层面,因而会影响一个 Btrfs 文件系统的所有子卷。你可以通过下面的链接找到entry [2]

如果你对哪些目录是普通目录和哪些是子卷有困惑,你可以对你的子卷采用特殊的命名约定。例如,你可以给子卷名加上 @ 前缀去方便区分。

现在你知道子卷表现得就像文件系统,有人可能会问如何才能最好地将子卷放置在特定位置。比如你想要一个 Btrfs 子卷在 ~/games 下面,然而你的主目录(~)本身就是一个子卷,你该如何实现呢?鉴于上面的例子,你可以使用像 sudo btrfs subvolume create ~/games 的命令。这样,你创建了所谓的 嵌套 子卷:在你的子卷 ~ 里,有一个子卷 games 。这正是一种达成目的的方法。

其他有效的方法就是如同 Fedora 默认行为那样:在根子卷下创建所有子卷(也就是它们的父子卷 ID 是 5 ),然后挂载它们到特定的位置。Btrfs 维基有这些方法的概述和对于各自文件系统管理影响的简短讨论 [3]

总结

在本文中,我们探索了 Btrfs 子卷,它们像是 Btrfs 文件系统内部的独立的 Btrfs 文件系统。我们学习了如何创建、挂载和删除子卷。最后,我们探讨了 Fedora Linux 如何在我们完全没有注意到的情况下使用子卷。

本系列的下一篇文章将讨论:

  • 快照 - 回到过去
  • 压缩 - 透明地节省存储空间
  • 配额组 - 限制文件系统大小
  • RAID - 替代 mdadm 配置

如果你还想了解与 Btrfs 相关的其他主题,请查看 Btrfs 维基 [4] 和文档 [5] 。不要忘记查看本系列的第一篇文章(如果你还没有看过的话)!如果你认为本系列文章缺少了一些内容,请在下面的评论中告诉我们。再会!

参考资料

  1. https://fedoraproject.org/wiki/Changes/BtrfsByDefault#Benefit_to_Fedora ↩︎
  2. https://btrfs.readthedocs.io/en/latest/Subvolumes.html ↩︎
  3. https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Layout ↩︎
  4. https://btrfs.wiki.kernel.org/index.php/Main_Page ↩︎
  5. https://btrfs.readthedocs.io/en/latest/Introduction.html ↩︎

(题图:MJ/f047ea87-2490-40e5-9f91-d48d236675e5)


via: https://fedoramagazine.org/working-with-btrfs-subvolumes/

作者:Andreas Hartmann 选题:lujun9972 译者:A2ureStone 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇文章是《Btrfs 详解》系列文章中的一篇。从 Fedora Linux 33 开始,Btrfs 就是 Fedora Workstation 和 Fedora Silverblue 的默认文件系统。

介绍

文件系统是现代计算机的基础之一。它是任何操作系统必不可少的一部分,且通常不为人注意。但是,像 Btrfs 这样的现代文件系统提供了许多很棒的特性,使计算机的使用更加方便。例如,它可以无感地为你压缩文件,或者为增量备份建立可靠的基础。

这篇文章将带你高屋建瓴地了解 Btrfs 文件系统是如何工作的,有什么特性。本文既不会过多涉及技术细节,也不会研究其底层实现,系列后续的文章会详细介绍一些重要特性。

什么是文件系统

如果你基本了解过文件系统是如何工作的,那么下面的内容对你应该是不陌生的,你可以直接跳到下一节。否则,请先阅读下面对文件系统的简短介绍。

简单来说,文件系统允许你的 PC 去寻找存储在磁盘上的数据。这听起来像是微不足道的工作,但实际上时至今日各种类型的非易失性存储设备(比如机械硬盘、固态硬盘、SD 卡等等)仍然与 1970 年代 PC 被发明时基本相同:一个(巨大的)存储块集合。

Block ” 是最小的可寻址存储单元。PC 上的每个文件内容被存储在多个块中。一个块通常是 4096 字节的大小。这取决于你的硬件和在这之上的软件(即文件系统)。

文件系统允许我们从海量的存储块中查找文件的内容,这是通过所谓的 inode 去实现的。一个 inode 在特殊格式的存储块里记录了文件的信息。这包含文件的大小,哪里去寻找组成文件内容的存储块,访问规则(即谁可读,可写,可执行)等等。

下面是 inode 的示意图:

A text file “myfile.txt” and a hypothetical example of its representation on disk. All the squares are individual storage blocks.

inode 的结构对文件系统的功能有巨大的影响,因此它是各种文件系统诸多的重要数据结构之一。出于这个原因,每个文件系统有各自的 inode 结构。如果你想了解更多信息,看看下面 链接 关于 Btrfs 文件系统 inode 结构的内容。如需更详细地了解各个字段的含义,你可以 参考 ext4 文件系统的 inode 结构。

写时复制(CoW)文件系统

相比 ext4,Btrfs 拥有的杰出特性之一是,它是一个 写时复制 Copy-on-Write (CoW)文件系统。当一个文件被改变和回写磁盘,它不会故意写回它原来的位置,而是被复制和存储在磁盘上的新位置。从这个意义上,可以简单地认为 Cow 是一种 “重定向”,因为文件写入被重定向到不同的存储块上。

这听起来很浪费,但实际上并不是。这是因为被修改的数据无论如何一定会被写到磁盘上,不管文件系统是如何工作的。Btrfs 仅仅是确保了数据被写入在之前没被占据的块上,所以旧数据保持完整。唯一真正的缺点就是这种行为会导致文件碎片化比其他文件系统要快。在日常的电脑使用中,你不太可能会注意到这点差异。

CoW 的优势在哪里?简单的说:文件被修改和编辑的历史被保存了下来。Btrfs 保存文件旧版本的引用(inode)可以轻易地被访问。这个引用就是快照:文件系统在某个时间点的状态镜像。这将是这系列文章里的单独的一篇,所以暂时留到后面介绍。

除了保存文件历史,CoW 文件系统永远处于一致的状态,即使之前的文件系统事务(比如写入一个文件)由于断电等原因没有完成。这是因为文件系统的元数据更新也是写时复制的:文件系统本身永远不会被覆写,所以中断不会使其处于部分写入的状态。

对文件的写时复制

你可以将文件名视为对 inode 的指针。在写入文件的时候,Btrfs 创建一个被修改文件内容(数据)的拷贝,和一个新的 inode(元数据),然后让文件名指向新的 inode,旧的 inode 保持不变。下面是一个假设示例来阐述这点:

Continuation of the example above: 3 more bytes of data were added

这里 myfile.txt 增加了三个字节。传统的文件系统会更新中间的 Data 块去包含新的内容。CoW 文件系统不会改变旧的数据块(图中灰色),写入(复制)更改的数据和元数据在新的地方。值得注意的是,只有被改变的数据块被复制,而不是全部文件。

如果没有空闲的块去写入新内容,Btrfs 将从被旧文件版本占据的数据块中回收空间(除非它们是快照的一部分,本系列后续文章会看到)。

对目录的写时复制

从文件系统的角度看,目录只是特殊类型的文件。与常规文件不同,文件系统直接解释数据块的内容。一个目录有自身的元数据(inode,就像上面说的文件一样)去记录访问权限或修改时间。最简单的形式,存在目录里的数据(被叫作目录项)是一个 inode 引用的列表,每个 inode 又是另外的文件或目录。但是,现代文件系统在目录项中至少会存储一个文件名和对应的 inode 引用。

之前已经指出,写入一个文件会创建之前 inode 的副本,并相应修改其内容。从根本上,这产生了一个和之前无关的新的 inode 。为了让被修改的文件对文件系统可见,所有包含这个文件引用的目录项都会被更新。

这是一个递归的过程!因为一个目录本身是一个带有 inode 的文件。修改目录里的任何一项都会为这个目录文件创建新的 inode 。这会沿着文件系统树递归直到文件系统的根。

所以,只要保留对任何旧目录的引用,并且这些目录没有被删除和覆写,就可以遍历之前旧状态的文件系统树。这就是快照的功能。

后续文章可以期待的内容

Btrfs 不只是一个 Cow 文件系统。它目标是实现高级特性的同时关注容错、修复和易于管理(参见 文档)。本系列未来的文章将会专门介绍这些特性。

  • 子卷 – 文件系统中的文件系统
  • 快照 – 回到过去
  • 压缩 – 透明节省存储空间
  • 配额组 – 限制文件系统大小
  • RAID – 替代 mdadm 配置

这远非 Btrfs 特性的详尽列表。如果你想全面地了解可用特性,查看 维基文档

总结

我希望我已能激起你进一步了解计算机文件系统的兴趣。如果目前你有任何疑问,请在评论区留言讨论以便在日后文章中探讨,同时,你也可以自行学习文中提供的相关资源。如果你发现 Btrfs 中某项特别有趣的功能,也欢迎在评论区提出。如果某个主题收到足够的关注,我可能会在系列文章中新增相关内容。下一篇文章再见!

(题图:MJ/35fa1970-1806-4026-8d58-095a56206ec9)


via: https://fedoramagazine.org/working-with-btrfs-general-concepts/

作者:Andreas Hartmann 选题:lkxed 译者:A2ureStone 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出