Backup for Linux,或如何创建快照

大家好! 我在Veeam的Veeam Linux代理项目中工作。 使用此产品,您可以备份Linux计算机。 名称中的“ Agent”表示该程序允许您备份物理机。 Virtualalkans也可以备份,但是它位于来宾OS上。

本文的灵感来自我在Linux Piter会议上的报告,我决定将其作为所有感兴趣的habragiteli的文章发布。

在本文中,我将介绍创建快照的主题,该快照使您可以备份并讨论在创建自己的用于创建块设备快照的机制时遇到的问题。

有兴趣的请减价!



一开始有点理论


从历史上看,有两种创建备份的方法:文件备份和卷备份。 在第一种情况下,我们将每个文件复制为一个单独的对象,在第二种情况下,我们将卷的全部内容复制为一种图像。

两种方法都有很多优点和缺点,但是我们将从失败中恢复的角度来考虑它们:

  • 对于文件备份,要完全恢复整个服务器,我们需要先安装操作系统,然后安装必要的服务,然后再从备份中还原文件。
  • 对于卷备份,要进行完全恢复,只需还原计算机的所有卷就足够了,而无需人工进行任何操作。

显然,在卷备份的情况下,可以更快地还原系统,这是系统的重要特征 。 因此,对于我们自己,我们将卷备份作为首选选项。

我们如何获取并保存整个卷? 当然,简单地复制我们不会取得任何好处。 在复制过程中,卷上将发生一些与数据有关的活动,结果,不一致的数据将出现在备份中。 文件系统结构将被破坏,数据库文件以及将在复制期间执行操作的其他文件将被破坏。

为了避免所有这些问题,进步人类提出了快照技术-快照。 从理论上讲,一切都很简单:我们创建一个不变的副本-快照-并从中备份数据。 备份结束后-我们销毁快照。 听起来很简单,但是像往常一样有细微差别。

因此,诞生了该技术的许多实现。 例如,基于设备映射器的解决方案(例如LVM和Thin Provisioning)提供完整的卷快照,但是在系统安装阶段需要特殊的磁盘布局,这通常意味着它们不适合。

BTRFS和ZFS使得创建文件系统子结构的快照成为可能,这非常酷,但是目前它们在服务器上的份额很小,我们正在尝试提供一种通用的解决方案。

假设我们的块设备上有一个普通的EXT。 在这种情况下,我们可以使用dm-snap (顺便说一下,现在正在开发dm-bow ),但这是它自己的细微差别。 您需要准备一个空闲的块设备,以便可以将快照数据拖放到哪里。
在关注备用备份解决方案时,我们注意到,通常,它们使用其内核模块创建块设备的快照。 我们决定采用这种方式,编写我们的模块。 决定根据GPL许可证分发它,以便可以在github上公开获得它。

工作原理-理论上


显微镜快照


因此,现在我们将考虑模块的一般工作原理,并更详细地讨论关键问题。

实际上,veeamsnap(我们称为内核模块)是一个块设备驱动程序过滤器。



他的工作是拦截对块设备驱动程序的请求。

截获写请求后,模块将数据从原始块设备复制到快照数据区域。 我们称该区域为快照存储区。



快照本身是什么? 这是一个虚拟块设备,是原始设备在特定时间点的副本。 访问此设备上的数据块时,可以从管理单元或原始设备中读取它们。

我想指出的是,快照正是与删除快照时的原始设备完全相同的块设备。 因此,我们可以将文件系统挂载到快照上并执行必要的预处理。

例如,我们可以从文件系统中获取占用块的映射。 最简单的方法是使用ioctl GETFSMAP
繁忙块上的数据允许您仅读取快照中的最新数据。

您还可以排除一些文件。 好吧,这是一个完全可选的操作:为备份中的文件建立索引,以备将来使用精细的餐厅。

牛与猪


让我们先介绍一下快照算法。 这里的选择不是很广泛: 写时复制或写时重定向

拦截写请求时,按写重定向会将其重定向到快照,此后所有读取该块的请求也将到达那里。 对于基于B +树构建的存储系统(例如BTRFS,ZFS和自动精简配置)的一种出色算法。 该技术与世界一样古老,但是它在虚拟机管理程序中表现得尤为出色,您可以在其中创建新文件并在快照期间将新块写入其中。 与CoW相比,性能出色。 但是有一个很大的缺点-原始设备的结构发生了变化,并且在删除快照时,您需要将所有块从快照复制到原始位置。

写时复制功能在拦截请求时,会将数据复制到必须进行更改的快照存储中,然后再将其覆盖在原始位置。 用于为LVM卷和VSS的卷影副本创建快照。 显然,它更适合于创建块设备的快照,因为 不会更改原始设备的结构,并且在删除(或崩溃)快照时,可以简单地丢弃快照而不会冒数据风险。 这种方法的缺点是性能下降,因为将几个读/写操作添加到每个写操作中。

由于数据安全是我们的重中之重,因此我们专注于CoW。

到目前为止,一切看起来都很简单,因此让我们回顾一下现实生活中的问题。

工作原理-实践


条件一致


为了他的缘故,一切都设想了。
例如,如果在创建快照时(初步近似,我们可以假定它是立即创建的)记录将被记录在某个文件中,那么在快照中该文件将是不完整的,这意味着它将被损坏且毫无意义。 数据库文件和文件系统本身的情况与此类似。

但是我们生活在21世纪! 有防止此类问题的日志记录机制! 的确,事实是,有一个重要的“但是”:这种保护不是免受失败的影响,而是免受其后果的影响。 根据日志恢复到一致状态时,不完整的操作将被丢弃,这意味着它们将丢失。 因此,重要的是将重点放在从原因的保护上,而不是处理后果。

可以警告系统现在将创建快照。 为此,内核具有函数freeze_bdevthaw_bdev 。 他们拉出文件系统函数freeze_fs和unfreeze_fs。 调用第一个请求时,系统必须重置缓存,暂停对块设备的新请求创建,并等待所有先前生成的请求完成。 当调用unfreeze_fs时,文件系统将恢复其正常功能。

事实证明,我们可以警告文件系统。 那应用程序呢? 不幸的是,这里的一切都很糟糕。 在Windows上,有一种VSS机制,可以在Writers的帮助下与其他产品进行交互,而在Linux上则各有千秋。 目前,这导致系统管理员自行编写(复制, 窃取 ,购买等)预冻结和解冻后脚本的任务,这将为快照做准备。 在我们的下一个版本中,我们将介绍对Oracle应用处理的支持,这是客户最常要求的功能。 然后,可能会支持其他应用程序,但总的来说情况相当可悲。

在哪里放置按扣?


这是我们遇到的第二个问题。 乍一看,问题并不明显,但经过一番了解,我们发现这仍然是一个碎片。

当然,最简单的解决方案是将快照放置在RAM中。 对于开发人员而言,该选项非常棒! 一切都很快,调试起来非常方便,但是有一个障碍:RAM是一种宝贵的资源,没有人可以在那儿大放异彩。

好,让我们将快照文件设置为常规文件。 但是会出现另一个问题-您无法备份快照停止所在的卷。 原因很简单:我们拦截录制请求,这意味着我们将在管理单元中拦截自己的录制请求。 马匹科学地跑来跑去-僵局。 然后,迫切需要为此使用一个单独的磁盘,但是就我们而言,没有人将磁盘添加到我们的服务器中。 您必须能够处理什么。

远程定位管理单元是一个不错的主意,但是可以在具有高带宽和微观带宽的非常狭窄的网络圈中实现。 否则,将快照保留在计算机上时,将有一个基于回合的策略。

因此,您需要以某种方式棘手地将快照放置在本地磁盘上。 但是,通常,本地磁盘上的所有空间已经分配在文件系统之间,与此同时,您需要认真思考如何解决死锁问题。

原则上,反射的方向是一个:您需要以某种方式在文件系统中分配空间,但直接使用块设备。 该问题的解决方案是在服务中的用户空间代码中实现的。

有一个fallocate系统调用,它允许您创建所需大小的空文件。 但是,实际上,仅在文件系统上创建了描述文件在卷上位置的元数据。 ioctl FIEMAP允许我们获取文件块位置的地图。

瞧,我们使用fallocate在快照下创建一个文件,FIEMAP为我们提供了该文件块位置的地图,我们可以将其转换为在veeamsnap模块中使用。 此外,在访问快照程序时,该模块直接以我们已知的块形式向块设备发出请求,并且没有死锁。

但是有细微差别。 仅XFS,EXT4和BTRFS支持fallocate系统调用。 对于其他文件系统(例如EXT3),必须完全编写它才能分配文件。 功能受准备快照板时间增加的影响,但是别无选择。 同样,您需要能够处理什么。

如果也不支持ioctl FIEMAP怎么办? 这是NTFS和FAT32的现实,其中甚至不支持古老的FIBMAP。 我必须实现某种通用算法,该算法的操作不取决于文件系统的功能。 概括地说,该算法是:

  1. 该服务将创建文件并开始向其中写入特定的模式。
  2. 该模块拦截写请求,检查正在写的数据。
  3. 如果块数据与给定的模式匹配,则将该块标记为属于该快速停止。

是的,困难,是,缓慢,但总比没有好。 在极少数情况下,它用于不具有FIEMAP和FIBMAP支持的文件系统。

快照溢出


相反,我们在快照存储下分配的位置结束了。 问题的实质是没有地方可以丢弃新数据,这意味着快照变得不可用。
怎么办

显然,您需要增加快照程序的大小。 多少钱 设置快照大小的最简单方法是确定卷上可用空间的百分比(与VSS相同)。 对于20 TB的卷,10%将是2 TB-对于卸载的服务器来说这是很多。 对于200 GB的卷,10%为20GB,对于正在密集更新其数据的服务器来说可能太少了。 而且数量仍然很薄...

通常,只有服务器的系统管理员才能预先确定所需管理单元的最佳大小,也就是说,您必须让人们考虑并给出专家意见。 这不符合“有效”的原则。

为了解决这个问题,我们开发了拉伸快照算法。 想法是将按扣分成几部分。 同时,根据需要在创建快照之后创建新部分。



再次简要介绍一下算法:

  1. 在创建快照之前,将创建快照的第一部分并将其分配给模块。
  2. 创建快照后,该部分将开始填充。
  3. 一旦该部分的一半已满,就会向该服务发送一个请求以创建一个新请求。
  4. 服务创建它,然后将数据提供给模块。
  5. 模块开始填充下一批。
  6. 重复执行该算法,直到备份完成,或者直到遇到可用磁盘空间使用的限制为止。

重要的是要注意,模块必须有时间根据需要创建快照部分的新部分,否则-溢出,重置快照且无备份。 因此,只有在支持Fallocate的文件系统上才能使用这种算法,在该系统上您可以快速创建一个空文件。

在其他情况下该怎么办? 我们正在尝试猜测所需的大小并完全创建整个快照。 但是根据我们的统计,现在绝大多数Linux服务器都使用EXT4和XFS。 EXT3在较旧的计算机上找到。 但是在SLES / openSUSE中,您可能会偶然发现BTRFS。

变更区块追踪(CBT)


增量备份或差异备份(顺便说一句,萝卜辣根是否甜,我建议在这里阅读)-没有它,您将无法想象任何成人备份产品。 为此,您需要CBT。 如果有人错过:CBT允许您跟踪更改并将仅上次备份更改的数据写入备份。



许多人在这方面有自己的经验。 例如,在VMware vSphere中,此功能自2009年的版本4起可用。 在Hyper-V中,Windows Server 2016引入了支持,并且为了支持早期版本,早在2012年就开发了自己的VeeamFCT驱动程序。 因此,对于我们的模块,我们并不是原始的,而是使用了已经有效的算法。
关于它是如何工作的。



整个跟踪的卷分为多个块。 该模块仅跟踪所有写请求,在表中标记已更改的块。 实际上,CBT表是一个字节数组,其中每个字节对应一个块,并包含在其中进行更改的快照的编号。
在备份期间,快照编号记录在备份元数据中。 因此,了解了当前快照的编号以及上一次成功备份的编号,就可以计算出已更改块的位置图。

有两个细微差别。

就像我说的那样,CBT表中的快照编号分配了一个字节,这意味着增量链的最大长度不能超过255。达到此阈值时,将重置表并进行完全备份。 似乎不方便,但实际上,创建备份计划时以255为增量的链远不是最佳解决方案。
第二个功能是仅将CBT表存储在RAM中。 因此,当您重新引导目标计算机或卸载模块时,它将被重置,并且再次,您将需要创建完整备份。 这样的解决方案不允许解决系统启动时模块启动的问题。 此外,关闭系统时无需保存CBT表。

性能问题


备份始终是设备IO的良好负载。 如果上面已经有足够的活动任务,那么备份过程会将您的系统变成一种懒惰
让我们看看为什么。

想象一下,服务器只是线性地写入一些数据。 在这种情况下,记录速度最大,所有延迟都最小化,性能趋于最大。 现在我们在这里添加备份过程,在每次写入时仍需要完成写时复制算法,这是后续写入操作的附加读取操作。 并且不要忘记,对于备份,您仍然需要从同一卷读取数据。 简而言之,您美丽的线性访问会变成无情的随机访问,并带来所有后果。

我们显然需要为此做些事情,我们实现了一个管道来一次处理一个请求,而不是一次处理整个请求。 它是这样的。



拦截请求时,它们被放置在队列中,特殊的流将它们分成几部分。 这时,将创建CoW请求,这些请求也会被分批处理。 在处理CoW请求时,首先对整个部分执行所有读取操作,然后执行写入操作。 仅在完成CoW请求的整个部分的处理后,才会执行拦截的请求。 这样的传送带可以访问磁盘中的大量数据,从而最大程度地减少了时间损失。

节流


在调试阶段,另一个细微差别浮出水面。 在备份期间,系统变得无响应,即 系统I / O请求开始长时间运行。 但是,从快照读取数据的请求以接近最大的速度执行。
我必须通过实施限制机制来扼杀备份过程。 为此,如果拦截的请求队列不为空,则从快照映像读取的进程将进入等待状态。 可以预期,该系统得以实现。



结果,如果I / O系统上的负载急剧增加,那么将等待快照读取过程。 在这里,我们决定遵循以下原则:最好以错误结束备份而不是破坏服务器。

死锁


我认为我们需要更详细地解释它是什么。

在测试阶段,我们开始遇到系统完全死机的情况,并诊断出七个故障-重置了一个。

他们开始了解。 事实证明,例如,如果您创建了LVM卷所在的块设备的快照,并将快照放置在同一LVM卷上,则可以观察到这种情况。 让我提醒您,LVM使用设备映射器内核模块。



在这种情况下,当拦截写请求时,将数据复制到管理单元的模块会将写请求发送到LVM卷。 设备映射器会将这个请求重定向到块设备。 来自设备映射器的请求将再次被模块拦截。 但是,只有在处理前一个请求之后,才能处理新请求。 结果,请求处理被阻止,您将陷入僵局。

为避免这种情况,内核模块本身为将数据复制到管理单元的操作提供了超时。 这使您可以检测死锁和崩溃备份。 这里的逻辑是相同的:最好不要备份而不是挂起服务器。

循环数据库


在发布第一个版本后,这已经是用户抛出的问题。
事实证明,有些服务仅用于不断覆盖相同的块。一个明显的例子是监视服务,该服务不断生成有关系统状态的数据并围成一圈覆盖它们。对于此类任务,请使用专门的循环数据库(RRD)。
事实证明,使用这种基础的备份可以确保快照溢出。在对该问题的详细研究中,我们发现了CoW算法的实现存在缺陷。如果同一块被覆盖,则每次将数据复制到该管理单元。结果:快照中的数据重复。



自然,我们更改了算法。现在,将卷分为多个块,然后将数据复制到快照块中。如果块已被复制一次,则不会重复此过程。

块大小选择


现在,当按扣被分割成块时,就会出现一个问题:实际上,用来分割按扣的块的大小是多少?

问题是双重的。如果将块变大,则它们操作起来会更容易,但是如果至少改变一个扇区,则必须将整个块发送到钻机,结果,增加了钻机满溢的机会。



显然,块大小越小,发送到快照存储的有用数据的百分比就越大,但是它将如何影响性能?

他们凭经验搜索真相,并提出了16KiB。另请注意,Windows VSS也使用16个KiB块。

而不是结论


现在就这些了。我将留下许多其他同样有趣的问题,例如对内核版本的依赖性,模块分发选项的选择,kABI兼容性,在反向移植条件下的工作等。这篇文章篇幅如此之多,所以我决定专注于最有趣的问题。

现在我们正在准备发布版本3.0,模块代码位于github上,任何人都可以在GPL许可下使用它。

Source: https://habr.com/ru/post/zh-CN430770/


All Articles