详细讲解Btrfs快照 vs 文件copy vs 备份

103 阅读7分钟

本文将探讨什么是 Btrfs 快照、它们的工作原理以及如何在日常情况下从快照中获益。这是详细介绍 Btrfs 的系列文章的一部分.

介绍

想象一下,您在很长一段时间内处理一个文件,反复添加更改并撤消更改。然后,在某个时候,您意识到:两个小时前撤消的某些更改现在非常有用。而且,在昨天您还更改了这个特定的部分,然后才删除了那个设计。但是,当然,由于您定期保存文件,因此旧的更改会丢失。很多人可能以前都遇到过这种情况。如果您可以恢复旧文件版本而不必定期手动复制它们,那不是很好吗?

这只是 Btrfs 快照可以帮到您的一种典型情况。如果使用得当,快照还可以为您的 PC 提供出色的备份解决方案。

下面您将看到许多与快照相关的示例。如果您想继续操作,您必须具有 Btrfs 文件系统和 root 访问权限。您可以使用以下命令检查目录的文件系统:

$ findmnt -no FSTYPE /home
btrfs

这里findmnt命令显示了/home/目录的文件系统类型。如果它显示btrfs,则一切就绪。让我们创建一个新目录来执行一些实验:

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

在下面的文本中,您将在框中找到许多命令响应,如上所示。

让我们先从一个简单的问题开始:什么是 Btrfs 快照?如果你查看文档[1]和 Wiki [2],你不会立即找到这个问题的答案。事实上,在“功能”部分根本找不到答案。如果你搜索一下,你会发现大量提到了快照以及 Btrfs 子卷[3]。那么现在该怎么办呢?

还记得本系列前两篇文章中都提到过快照吗?文章中写道:

CoW 有什么优势?简单来说:可以保留修改和编辑文件的历史记录。Btrfs 会将对旧文件版本 (inode) 的引用保存在可以轻松访问的地方。此引用是快照:某个时间点的文件系统状态的图像

以及:

分离/home的另一个优点是您可以单独拍摄快照。子卷是快照的边界,快照永远不会包含拍摄快照的子卷下方的其他子卷的内容。

快照似乎与 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 快照与文件副本

那么这一切的意义何在?到目前为止,快照似乎是一种复杂的文件复制方式。事实上,快照比我们看到的要复杂得多。让我们创建一个更大的文件:

$ 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

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

$ 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 格式化的外部驱动器,请随意替换以下命令中提到的所有路径以进行尝试!让我们创建一个新的 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

太好了,现在在备份驱动器下挂载了一个独立的 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)的引用吗?这正是问题所在:文件和内容存在于我们的主文件系统中,但不存在于新创建的备份驱动器_中。您必须找到一种方法将子卷连同其内容一起传输到新文件系统。

将快照存储在不同的 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_选项。这将创建一个只读快照,很容易验证:

$ 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

成功了!您已成功将我们原始子卷演示的只读快照传输到外部 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 文件系统都必须包含相同的、未修改的父子卷!确保新子卷确实存在:

$ 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 子卷的读/写和只读快照,并将它们发送到外部位置。但是,为了将其转变为备份策略,必须有一种方法将子卷发送回原始文件系统并使其再次可写。为此,让我们将演示子卷移动到其他地方,并尝试从最新的快照重新创建它。首先:重命名“损坏”的子卷。恢复成功后,它将被删除:

$ 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-ro_重命名为_demo_,因为它仍然是只读子卷!最后,您可以检查所需的一切是否都在那里:

$ 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 文件系统(外部媒体)上的快照中恢复_演示子卷。_

子卷作为快照的边界

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

$ 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 。发生这种情况的原因是__嵌套文件夹_是子卷:_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 wiki 上有一个专门针对 Btrfs 的备份工具列表[4]。在此页面上,您还可以找到另一个手动执行 Btrfs 备份的步骤摘要。就我个人而言,我对_btrbk_ [5]有很多很好的体验,我正在用它来执行自己的备份。除了备份之外,_btrbk_还可以在您的 PC 本地保留 Btrfs 快照列表。我用它来防止意外删除数据。

如果您想了解有关使用 Btrfs 执行备份的更多信息,请在下面发表评论,我会考虑撰写一篇专门讨论该主题的后续文章。

结论

本文研究了 Btrfs 快照,即 Btrfs 底层的子卷。您了解了如何创建读/写和只读快照,以及此机制如何帮助防止数据丢失。

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

  • 压缩——透明地节省存储空间
  • Qgroups – 限制文件系统大小
  • RAID – 替换 mdadm 配置

如果您想了解更多与 Btrfs 相关的其他主题,请查看 Btrfs Wiki [2]和文档[1]。如果您还没有看过本系列的前两篇文章,请不要忘记查看!如果您觉得本系列文章中缺少某些内容,请在下面的评论中告诉我。我们下一篇文章再见!