第七章:快照和克隆

ZFS最强大的功能之一是快照。

快照是只读的,永远不能更改。

可以将数据集回滚到快照的状态。在升级系统之前,建议先创建快照(FreeBSD在做系统升级时会自动生成快照)。

快照是许多ZFS功能的根源。例如克隆,是基于快照的文件系统分支。新克隆不占用额外空间,因为它与快照共享所有数据集块。当更改克隆时,ZFS会分配新的存储以适应更改。

当需要搭建一个测试环境时,可以克隆生产文件系统,并在克隆上进行测试。

快照支持复制,可以将数据集从一个主机发送到另一个主机。

更重要的是,ZFS的写时复制特性意味着快照是免费的。创建快照是即时的,不消耗额外的空间。

写时复制(Copy-on-Write)

ZFS从不覆盖文件的现有块。当文件发生更改时,ZFS会识别必须更改的块,并将其写入磁盘的新位置。这被称为写时复制。缩短写入可能会丢失对文件的最新更改,单文件的先前版本仍然完整存在。

ZFS几乎是一个面向对象的文件系统。元数据、索引和数据都可以指向其他对象的不同类型的对象。ZFS池是一个巨大的对象树,根植于池标签。

池中的每个磁盘都包含ZFS标签的四个副本:两个在驱动器的前面,两个在末尾。

每个ZFS标签包含池名、GUID(全局唯一ID)、VDEV每个成员的信息。每个标签还包含128KB的uberblock(uberblock是访问pool中数据的入口。任意时刻,只有一个uberblock处于激活状态)。

uberblock是一个固定大小的对象,它包含:

MOS记录了池中所有内容的顶级信息,包括指向池中所有根数据集列表的指针。

反过来,这些列表中的每一个都指向其子列表的类似列表,以及描述数据集中存储的文件和目录的块。

ZFS根据数据的需要链接这些列表和指针对象。在树的底部,叶块包含存储在池中的实际数据。

每个对象都包含一个校验和和一个出生时间。校验和用于确保对象有效;出生时间是创建块的事务组(transaction group,txg)号。出生时间是快照基础架构的关键部分。

修改一个数据块会触及整个树。修改后的数据块被写入新位置,因此指向它的块被更新。这个指针块也被写入一个新的位置,因此树上的下一个对象需要更新。这一直渗透到uberblock。

uberblock是树的根,一切都源于它。ZFS不能在不违反写时复制规则的情况下修改uberblock,因此它会旋转(rotate)uberblock。

每个标签为uberblock保留128KB。具有512字节扇区的磁盘有128个uberblock,而具有4KB扇区的磁盘则有32个uberblock。

每次文件系统更新都会向此数组添加一个新的uberblock。当数组填满时,最旧的uberblock会被覆盖。

当系统启动时,ZFS扫描所有uberblock,找到具有有效校验和的最新uberblock,并使用它来导入池。即使最近的更新以某种方式搞砸了,最后一个数据就会丢失,但无论如何,这些数据都不会到达磁盘。

使用写时复制意味着ZFS不会遇到传统文件系统需要fsck的问题。

快照如何工作

创建ZFS快照时,ZFS复制文件系统的顶级元数据块。实时系统使用副本,将原始副本留给快照使用。

创建快照只需要复制一个块,这意味着ZFS几乎可以在瞬间完成创建快照。

ZFS不会修改快照中的数据或元数据,使快照称为只读。ZFS记录了关于快照的其他元数据,比如出生时间。

快照还需要一个新的ZFS元数据,即死亡列表(dead list)。数据集的死亡列表记录了最近快照使用但不再是数据集一部分的所有块。

从数据集中删除文件时,该文件使用的块将添加到数据集的死亡列表中。创建快照时,活动数据集的死亡列表会被传递给快照,而活动数据集会得到一个新的空死亡列表。

删除、修改或覆盖实时数据集上的文件,意味着为新数据分配新块,并断开包含旧数据的块。然而,快照需要一些旧数据块。在丢弃旧块之前,系统会检查快照是否仍然需要它。

ZFS将旧数据块的出生时间与最近快照的出生时间进行比较。早于快照的块不可能会被快照使用,可以扔进回收站。大于快照生成时间(即晚于快照)的块仍被快照使用,因此会被添加到活动数据集的死亡列表中。

快照只是在拍摄快照时,实时数据集中正在使用的块的列表。创建快照告诉ZFS保留这些块,即使使用这些块的文件已从活动文件系统中删除。

这意味着ZFS不会保留每个文件的每个版本的副本。当创建新文件并在创建快照之前删除它,则该文件将消失。每个快照都仅仅包含创建快照时存在的每个文件的副本。

ZFS没有像DragonFly的HAMMER那样保留历史。

删除快照需要比较出生时间,以确定哪些块可以释放、哪些块仍在使用。如果删除最近的快照,则数据集的当前死亡列表将更新,已删除仅该快照需要的块。

快照意味着数据可以保存很长时间。只有当没有文件系统、卷或快照使用块时,它才会被释放。

使用快照

以下示例,先创建一个新的文件系统数据集,并用一些文件填充它:

创建快照

使用zfs snapshot命令创建一个快照。指定数据集的完整路径,加上一个@符号,和快照名称:

使用zfs list -t snapshot命令查看快照,-r选项可以指定数据集:

注意,快照的USED列表示使用量为0,表示快照中的每个块仍然由实时数据集使用,因此此快照未使用额外的空间。

数据集变化和快照空间

现在更改数据集,看看它如何影响快照。以下示例将一个1MB的新crud附加到随机文件中,并更新date.txt文件:

随机数据文件增长了1M字节,但在旧快照中没有。

date.txt文件已经被替换,因此快照应保留在旧文件使用的块上。

看看这对快照的空间使用有什么影响:

快照用了72KB。快照消耗的唯一空间是date.txt文件被替换的块。快照不会因较大的随机文件占用的新空间而变大,因为没有覆盖任何块。

现在,创建第二个快照,看看它使用了多少空间:

REFER列显示,第一个快照允许访问1.1MB的数据,而第二个快照则允许查看2.11MB的信息。第一个快照使用72KB空间,而第二个快照不使用任何空间,因为它与实时数据集相同。

接下来通过覆盖随机文件的一部分来更改实时数据集,看看空间使用情况是如何变化的:

我们覆盖了一兆字节的随机数据文件。第一个快照的空间使用情况没有变,第二个快照显示,它使用了1MB的空间来保留覆盖的数据,外加一些元数据开销。

递归(recursive)快照

ZFS允许创建递归快照,对指定的数据集及其子数据集进行快照。

所有快照都有相同的名称。

使用-r选项递归创建快照。以下我们使用单个命令对启动池进行快照:

我们现在为这个池中的每个数据集都有一个单独的快照,每个快照都标记有@beforeupgrade:

我们现在可以肆意滥用此系统,因为我们知道快照中存在一个已知的良好版本。

高级数据集和快照查看

一旦习惯了ZFS,可能会创建很多数据集,每个数据集都有一堆快照。

试图找到想要的明确快照会很麻烦。ZFS命令行工具有非常强大的功能,可以查看和管理数据集和快照。

组合选项可以准确地瞄准想要的数据。

其中许多选项适用于其他类型的数据集以及快照。如果将文件系统堆叠19层,可能会想限制看到的内容。

许多功能也适用于zpool和池,尽管池并不像数据集那么复杂。

一个普通的zfs list显示文件系统和zvol数据集,但没有快照:

可以用名称指定单独的数据集:

使用-r选项和名称,可以查看池或数据集以及它的所有子集:

以上适合于有多个数据集的系统,可以进一步缩小范围。

查看数据集类型

要仅查看特定类型的数据集,可以使用-t选项指定数据集类型。可以查看文件系统、卷、快照和书签:

可以通过提供完整的快照名称来检查指定快照:

应确保提供完整的名称,包括快照部分。以下示例中,我们告诉zfs -list只显示快照,然后给它一个文件系统数据集的名称,zfs非常礼貌地告诉我们在要求什么时要始终如一:

-r选项不仅可以用了显示数据集和它地子集,也可以在快照上使用:

使用-t all选项,可以显示所有内容:

如果有多层数据集,可能需要一个部分递归的试图。虽然-r显示所有子项,但-d选项限制了查看的层数。将深度限制为1,将只获得单个数据集的快照:

将深度限制为2将显示指定的数据集、指定数据集中的快照和数据集的子数据集,但不显示其孙文件系统或子数据集的快照。

修改ZFS list输出

可以使用zfs list -o加属性选项来控制显示哪些信息。

可以同时显示多个属性,多个属性已逗号隔开:

文件系统的属性与快照无关。

最后,可以更改zfs list显示数据集的顺序。使用-s选项和属性,按属性值排序。-S和属性则按属性值的反序排列。

默认列出快照

zfs list默认隐藏快照和书签。如果想默认显示快照可以设置池的listsnapshots属性为on。

然而随着系统运行,快照会变得越来越多的,所以建议将此属性设置回off。

脚本和ZFS

系统管理员喜欢自动化。自动化的一个令人讨厌的事情是,必须运行命令并解析输出。

使输出更人性化通常会使其对自动化不那么友好。以下一些选项可消除大部分问题。

-p选项告诉zfs和zpool命令列出精确的值,而不是人性友好的值。

-H选项告诉zfs和zpool命令不显示头部信息,而是用一个tab分割列。

结合起来,这些选项将输出从人类易读的内容转换为可以直接输入脚本的内容:

这就是真正的间距。

有序的列是给人类看的。

每个快照空间使用

written属性是一个特别有用的属性,它可以让人了解快照包含多少新的数据块。

注意,快照按创建日期顺序显示。实时数据集首先出现——虽然它可能比任何快照都有更新的数据,但它是在任何快照之前创建的。@all快照是最旧的,然后是@snap2,以此类推。

第一个快照@all允许您访问2.11MB的数据(REFER列)。此快照还包含2.11个新写入的数据。这是此快照与其之前的快照之间的区别。

快照@snap2和@evenmore没有新数据。它们与第一张快照没有变化。

在@evenmore快照和@later快照之间的某个时间,数据增长了。快照@later允许您访问5.11 MB的数据。它有4.07 MB的新数据。

@rewrite快照还允许您访问5.11 MB的数据,但它写入了2.07 MB的新数据。由于您可以访问的数据量与上一个快照相同,因此必须覆盖一些旧数据。

实时文件系统还覆盖了1 MB的数据。该数据现在仅包含在@rewrite快照中。

访问快照

访问快照内容最方便的方法是通过快照目录或snapdir。

每个ZFS数据集的根目录里都有一个隐藏的.zfs目录。该目录有一个快照目录,还有一个用于每个快照的目录。

进入该目录,可以发现自己位于快照的根目录中。快照中的每个文件都与拍摄快照时完全相同,直到文件访问时间。

要从快照中恢复单个文件,可以将其直接复制到主文件系统中。

秘密的Snapdir

默认情况下.zfs快照目录时隐藏的。即使运行ls -lA也不会显示,这可以防止备份程序、rsync和类似软件遍历到它。

如果想显示.zfs目录,可以将数据集的snapdir属性设置为visible:

一旦有人在数据集上运行cp -R,递归地将所有快照复制到文件系统上,然后将所有内容都销毁,通过将snapdir属性设置为hidden再次隐藏它。

挂载快照

可以将快照挂载到系统,就像其文件系统一样:

当快照被手动挂载时,无法通过隐藏的.zfs访问它。

即使是挂载的快照也是只读的。

删除快照

快照会阻止它们使用的块被释放。这意味着,在停用这些块之前,通过删除所有引用它们的快照,将无法恢复该空间。

以下示例创建一个新的快照,然后删除它:

可以使用-v选项添加详细(verbose)的标记,以获取有关正在销毁的内容的更多详细信息。

虽然在销毁单个快照时,详细模式没有多大帮助,但随着销毁更多数据集,或者如果想看看命令在不实际运行的情况下会做什么,它会变得更有价值。

销毁预演

-n选项表示noop标志,执行删除操作的“dry run”(模拟运行)。它描述了如果删除快照(而没有实际删除它)会发生什么。

删除这个快照将回收72KB空间。组成此快照的块仍由实时文件系统和/或第二个快照使用。

第二个快照覆盖了第一个快照中的一些数据。这将更改删除快照的效果。

我们将释放用于存储文件覆盖版本的空间。

递归和范围

递归创建快照可能会创建一大堆快照。幸运的是,可以递归销毁快照。

递归销毁快照之前,最好使用-n做一次销毁预演。

我们不止一次地意识到,在删除快照两秒钟后,我们需要一个快照。

另一个方便的功能是销毁一系列快照。您为同一数据集提供两个快照,ZFS可以清除它们以及它们之间拍摄的所有快照。运行zfs destroy,但给出from快照的全名、百分号和to快照的名称。这两次快照和它们之间的所有快照都会被销毁。

使用范围销毁快照时,同样建议使用-n参数先做一次预演:

关于百分号%:

如果%前面没有指定快照,则销毁掉指定快照以及比它更早创建的快照:

如果%后面没有指定快照,则销毁掉指定快照以及比它更晚创建的快照:

如果%前后都没有指定快照,则销毁这个数据集的所有快照:

回滚

快照不仅显示文件系统在过去的某个时刻是如何存在的。

可以将这个数据集还原为快照状态。

使用zfs rollback命令将文件系统还原为快照。一旦回滚,就不能再回到现在的状态了。

以下示例创建了一个包含一系列更改的文件系统,并对每个更改进行快照:

/delorean/timecapsule.txt文件中有三组不同的文本,快照捕获了该文本的两个版本,第三个不在快照中。

运行zfs rollback并给出要使用的快照名称:

这需要的时间比较短,所有数据和元数据都已存储在磁盘上。ZFS只是切换它使用的元数据集。回滚完成后,实时文件系统将包含所选快照中的所有文件。

虽然这是一个简单的示例,但您可以对软件升级、数据库迁移或任何其他有风险的操作执行完全相同的操作。曾经需要从离线备份中恢复恼人的操作现在可以在一个命令中处理。

只能将文件系统回滚到最近的快照,如果要回滚到更早的快照,则必须先删除所有比目标更新的快照。

如果使用-r(递归),zfs rollback命令可以销毁所有中间快照。

这与创建和销毁快照时使用的多数据集递归不同。使用rollback -r不会回滚子项,必须分别回滚每个数据集。

差异(diff)快照

可以使用find命令查找自创建快照以来修改过的文件,也可以使用diff命令将快照中的文件与实时文件系统中的文件进行比较。

zfs diff命令则可以提供相同的功能。

在返回的信息中:

“-”表示文件被删除;

“+”表示文件是新建的;

“M”表示文件被修改了;

“R”表示文件被重命名了。

也可以获得更多的信息。如果添加-t标志,输出将包括来自索引点的更改时间戳,-F标志包括文件的类型(比如,B表示块设备、C表示字符设备、/表示目录、>表示通道、|表示命名管道、@表示符号链接、P表示事件端口、=表示套接字、F表示普通文件)。

自动快照机制

快照很有用,建议制订自动创建快照计划。

为避免过多的快照填满池,自动快照需要进行轮换(rotate)和丢弃(discard)。

轮换(rotation)计划

常见的设置是按每周、每天、每小时、每刻钟构建快照。比如:

每周拍摄快照,并保留两个月;

每日拍摄快照,并保留两周;

每小时拍摄快照,并保留三天;

每刻钟拍摄快照,并保留六消失。

根据实际情况(数据的重要性、时限性等)选择适合的方案。

ZFS Tools

ZFS Tools有许多脚本和软件可以用来管理ZFS快照。

它不使用配置文件,仅依赖cron。

ZFS Tools从ZFS中设置的用户自定义属性中获取配置。这意味着新数据集会自动从其父级继承快照配置。这为维护较深的数据集提供了便利。

使用pkg命令安装ZFS Tools:

ZFS Tools有很多脚本和应用程序,本书仅关注zfs-auto-snapshot命令。

zfs-auto-snapshot

zfs-auto-snapshot是一个Ruby脚本,可以用了创建和删除快照。

需要指定两个参数:快照名称、需保留的快照数量。

比如,zfs-auto-snapshot frequent 4,表示创建名称为frequent的递归快照,并为数据集保留4个快照。(所谓递归,是因为子数据集继承了父数据集的特殊属性,所以表现得像是zfs-auto-snapshot在执行递归操作)

结合cron,zfs-auto-snapshot允许管理员在任何需要的时间间隔创建任意指定的快照,然后在它们过期时丢弃它们。

ZFS Tools附带了一个默认的crontab,可用作参考创建快照。它首先设置$PATH,以使zfs-auto-snapshot能找到Ruby。然后,它有创建15分钟、每小时、每天、每周和每月的快照条目。以下逐一分析:

zfs-auto-snapshot在每小时的第15、第30、第45分钟运行一次。在每个数据集上创建一个名为frequent的快照。保留最新的4个快照,删除旧的。

每小时的第0分钟,zfs-auto-snapshot创建一个名为hourly的快照。保留最新的24个快照,删除旧的。

每天0:07,zfs-auto-snapshot创建一个名为daily的快照。保留最新的7个快照,删除旧的。

每周第7天的0:14,zfs-auto-snapshot创建一个名为weekly的快照。保留最新的4个快照,删除旧的。

每月的第一天的0:28,zfs-auto-snapshot创建一个名为monthly的快照。保留最新的12个快照,删除旧的。

以上这些crontab条目是为/etc/crontab设计的。如果在root的crontab中使用它们,则必须从每个条目中删除“root”关键字。

无论哪种情况,都需要包含PATH变量,以使zfs-auto-snapshot能找到Ruby。

调整名字和时间表,以适应实际的环境和个人喜好。

启用自动快照

zfs-auto-snapshot仅对com.sun:auto-snapshot属性为true的数据集进行快照。没有此属性或此属性值为true以外的数据集不会被快照。

在数据集上设置此属性可以让其子数据集继承它。

以下示例设置mypool池的根数据集的com.sun:auto-snapshot属性:

当zfs-auto-snapshot运行时,它为mypool中的每一个数据集创建快照,并按照/etc/crontab中的设置命名这些快照。

有些数据可能不需要快照。可以将这些数据集和它们的子集的com.sun:auto-snapshot属性设置为false:

也可以仅禁用特定类别的快照:

以上示例禁用了mypool/delorean和其子集的frequent和hourly快照,仅进行daily、weekly、monthly快照。

也可以重新设置mypool/delorean的某个子集进行frequent快照。

查看自动快照

自动快照的名称以zfs-auto-snap为开头,后面跟句点和时间戳:

使zfs-auto-snap变得聪明

在命令行运行zfs-auto-snapshot hourly 2,可以销毁除最后两个快照之外的hourly快照。

保留

如果希望保留特定的快照,可以使用zfs hold命令将其设置为保留:

其中,tag是一个易读的标签。

一个快照可以有多个保留,因此可以为不同的目的创建保留。

使用-r选项可以递归保留:

zfs holds命令可以显示现有的保留:

不能销毁保留的快照:

使用zfs release命令可以释放保留:

释放后就可以销毁该快照了,但释放不会被套用到子快照上,除非使用-r参数:

书签

书签是新版ZFS的功能。类似于快照,但不会保留旧数据。

书签只是创建它的快照的时间戳。书签是基于新的extensible_dataset特性标记构建的。

ZFS需要时间戳来进行增量复制。ZFS可以很容易地收集自书签时间戳以来发生变化的每个块。这允许增量复制,而不必像以前那样保留旧快照。

技术细节参阅FreeBSD Mastery: Advanced ZFS

克隆

克隆是从快照创建的新文件系统。

最初,它不使用新的空间,将其所有块与创建它的快照共享。

快照是只读的,但克隆和任何正常的文件系统一样是可写的。

克隆可以被认为是文件系统的“分叉”(fork)或“分支”(branch)。

克隆的文件系统可以作为应用程序的测试实例,在不接触生产实例和消耗额外磁盘的情况下应用补丁和更改。可以在克隆版本上运行测试,使其与实时版本一起运行。

克隆不会接收在原始数据集中进行的更新。它们基于静态快照。

如果原始数据集的内容发生变化,就必须重新创建快照并根据此新快照创建新的克隆。

克隆最初不使用磁盘空间。当克隆与快照分离时,对克隆的文件系统所做的任何更改都将作为克隆的一部分存储,并开始消耗空间。

创建克隆

使用zfs clone命令创建克隆。此命令需要两个参数:源快照和目的地。如果池没有挂载点,则需要在克隆上设置一个以便访问其内容。

现在查看数据集:

除了空间使用情况外,数据集dolly看起来就是个普通的数据集。REFER列显示它有2MB的数据,但USED下它只占用8KB。它包含的数据来自原始快照。克隆只会为新写入的数据占用空间,无论是新文件还是覆盖旧文件。

查看克隆

克隆看起来与常规数据集相同,在zfs list命令的输出结果中,克隆没有特别的地方。

但克隆会在其origin(起源,出身)属性中记录其源快照:

origin属性是判断克隆的唯一方法,其值是该克隆的来源快照。

要追踪系统上的所有克隆,可使用zfs list检查origin属性。以下示例我们检查所有没有以破折号结尾的条目:

这列出了所有源自快照的所有数据集。

删除克隆和快照

克隆依赖于源于快照中存储的块。克隆的存在会阻止删除源快照:

添加-R标志,销毁快照会带走所有依赖的克隆。可以像删除任何其他文件系统数据集一样删除克隆本身。

克隆从其父级继承了zfs-auto-snapshot属性,因此它也会被自动快照。如果不想对克隆自动快照,应该关闭此属性。

也可以使用-r标志递归销毁克隆及其快照。

现在我们可以删除源快照了:

克隆很强大,但它会使快照管理复杂化。

升级克隆

升级(promote)克隆的目的是反转原始数据集和克隆之间的父/子关系。

经过此过程,克隆成为文件系统,前一个父级将成为克隆,克隆所需的任何快照都会被移动,成为克隆的一部分。

在克隆的原始快照之后创建的快照仍然属于原始的父级。

一旦克隆成功地与父数据集交换位置,就可以删除原始数据集。

ZFS还会更改新父级和新克隆使用的空间。数据集不占用额外的空间,但对该空间的计算发生了变化。克隆仅按其与原始快照不同的空间量计算。新的父级数据集几乎涵盖了所有内容。

以下示例将数据集mypool/wooly克隆到一个名为mypool/bonnie的数据集,并修改克隆:

看看克隆的磁盘使用情况:

USED列显示8MB的新数据写入到了克隆。REFER列显示数据集包含12MB数据——4MB来自源快照,另外8MB新添加的数据。

我们要保留bonnie数据集,并删除原始的wooly数据集:

ZFS知道数据集mypool/bonnie及其原始快照依赖于mypool/wooly数据集。

因此,我们使用zfs promote命令使bonnie成为文件系统,并将旧数据集转换为克隆。

在升级克隆之前,运行zfs list并检查所涉及的两个数据集的空间使用情况:

现在升级mypool/bonnie:

升级操作静默运行。再次查看这两个数据集:

mypool/bonnie所基于的快照,以及所有比原始快照更早的快照,现在都属于mypool/bonnie。

在创建mypool/bonnie快照后拍摄的mypool/wooly的较新快照仍然属于mypool/wooly。

现在,可以销毁旧数据集及其所有快照:

注意,一旦克隆从主文件系统分叉(fork),它就不会从父文件系统获得任何更新。

应用程序需要的任何持久数据都应该放在不同的数据集中。

持久性数据应该放在一个完全无关的数据集中,这样递归删除就不会触及它。

安全管理克隆、快照、递归

-nv标志对于安全系统管理至关重要。任何时候,只要一想到要销毁数据集,就应该使用-nv做一个冗长的模拟。看看destroy命令实际上会消除什么。