一、背景
1.1 数据保护技术概述
快照(Snapshot)、复制(Replication)、备份(Backup)是存储领域中最为常见的数据保护技术。快照用于捕捉数据卷在某一个时刻的状态,用户可以随时回滚到这个状态,也可以基于该快照创建新的数据卷。备份就是对数据进行导出拷贝并传输到远程存储设备中。当数据损坏时,用户可以从远端下载备份的数据,手动从备份数据中恢复,从而避免了数据损失。快照类似于git的commit操作,我们可以随时reset/checkout到任意历史commit中,但一旦保存git仓库的磁盘损坏,提交的commit信息将永久丢失,不能恢复。而备份则类似于git的push操作,即使本地的数据损坏,我们也能从远端的git仓库中恢复。简而言之,快照主要用于快速回溯,而备份则用于容灾,还能避免误删除操作造成数据丢失。数据复制则类似于mysql的master/slave主从同步,通常只有master支持写操作,slave不允许用户直接写数据,它只负责自动同步master的数据,但一旦master出现故障,slave能够提升为master接管写操作。因此复制不仅提供了实时备份的功能,还实现了故障自动恢复(即高可用)。
1.2 Cinder数据保护功能介绍
Cinder是OpenStack中相对成熟的组件(总分为8分的成熟度评分中获得了8分满分),也是OpenStack的核心组件之一,为OpenStack云主机提供弹性的块存储服务,承载着用户大多数的业务数据,即使出现数据的丝毫损坏也将可能导致灾难性后果,因此数据的完整性保护至关重要。不得不说Cinder对数据卷保护方面支持度还是比较给力的,目前Cinder已经同时支持了对数据卷的快照、复制和备份功能。
快照应该是Cinder非常熟悉非常受欢迎的功能了,也是Cinder默认支持的功能,几乎所有的存储后端都支持快照。而备份作为Cinder的可选功能之一,由于数据卷的存储后端很多已经提供了多副本功能(比如Ceph存储后端默认为三副本),通常很少人会再部署一套备份存储集群,因此部署率并不是很高。复制也是Cinder的可选功能之一,目前支持的存储后端还非常有限,最常采用的RBD存储后端也是在Ocata版本才开始支持,并且要求Ceph版本需要支持rbd-mirror(jewel版本以上),因此受到用户的关注度还不是很高,部署率较低。
二、深入理解Cinder数据卷备份原理
2.1 cinder backup功能介绍
cinder磁盘备份为用户的数据卷实例提供备份和恢复功能,实现了基于块的容灾功能。从K版本开始,Cinder引入了增量备份功能,相对全量备份需要拷贝和传输整个数据卷,增量备份只需要传输变化的部分,大大节省了传输开销和存储开销。通常情况下,当用户执行备份或者恢复操作时,需要手动卸载数据卷,即数据卷不支持在挂载状态下热备份。从L版本开始,新增了force选项,当用户指定force选项时能够对挂载的数据卷强制执行备份操作,这样可能带来数据不一致的风险,不过社区针对这种情况做了些优化,比如在创建备份前先基于该数据卷快照创建临时数据卷,然后基于临时数据卷执行后续备份操作。
Cinder开启备份功能,需要单独部署cinder-backup服务。cinder-backup服务和cinder-volume服务类似,也支持各种不同的驱动,对接不同的存储后端,目前支持的存储驱动列表如下:
- swift,备份数据保存在OpenStack Swift对象存储中。
- google,备份数据保存在Google Cloud Storage(GCS)中。
- glusterfs,保存到glusterfs中。
- nfs,保存到NFS中。
- posix,保存到本地文件系统。
- tsm,保存在IBM Tivoli Storage Manager(TSM)。
- ceph,保存到ceph集群中。
从列表中看,目前cinder backup尚不支持备份数据到AWS S3中。
除了数据卷本身的备份,cinder backup还支持将元数据序列化导出(export record),这样即使数据库中的数据丢失了,也能从导出的元数据中快速恢复。
2.2 cinder backup原理剖析
前面提到cinder backup支持多种后端存储驱动,但大体可以分为两类:
- 存储系统本身就提供块存储服务,比如ceph。这种情况只需要直接导入到该存储系统即可。
- 存储系统不支持块存储服务,只支持基于文件的存储,以上除了ceph和tsm都属于此类。此时备份采取了分块备份策略,即首先把数据卷切割为一个个独立的文件,然后分别把这些文件存储到设备中。恢复时只需要重组这些小文件即可。
接下来我们针对此两种情况深入研究下cinder backup的实现原理。
2.2.1 分块备份策略在介绍之前先了解两个重要的参数
- chunk_size: 表示将volume切割成多大的块进行备份,一个块称为一个chunk。在NFS中这个值叫做backup_file_size,默认是1999994880Byte,大约1.8G。在Swift中这个值叫做backup_swift_object_size,默认是52428800Byte,也就是50M。这个参数决定数据卷备份后块的数量(Object Count),比如一个2GB的数据卷,如果chunk_size为500MB,则大约需要4个块,如果使用本地文件系统存储的话,对应就是4个文件。
- sha_block_size: 这个值用于增量备份,决定多大的块求一次hash,hash相同说明内容没有变化,不需要备份。它决定了增量备份的粒度。在NFS中,这个值叫做backup_sha_block_size_bytes,在Swift中,这个值叫做backup_swift_block_size。默认都是32768Byte,也就是32K。在Ceph,没有对应的概念。
对一个数据卷做全量备份时,每次从数据卷读入chunk_size字节的数据构成一个chunk,然后每sha_block_size个字节做一次sha计算,并将结果保存起来,最后把chunk_size的数据进行压缩(可以配置不压缩)后保存到对应的存储系统上,这就形成了NFS上的一个文件或者Swift中的一个object[6]。如此循环直到把整个数据卷都备份到存储系统。
那恢复的时候怎么重组呢?这就需要保存元数据信息,元数据信息包括:
- backup信息:其实就是数据库中的信息,或者说就是一个backup object实例的序列化,包括backup name、description、volume_id等。
- volume信息:数据卷信息,即volume实例的序列化,包括size、name等。
- 块信息:即objects信息,这是最重要的数据,记录了每一个块的长度、偏移量、压缩算法、md5值,备份恢复时主要通过这些块信息拼接而成。
- 版本:序列化和持久化必不可少的参数。
除了保存以上元数据信息,还会按顺序保存每一个block的sha256值。这些信息主要用于支持增量备份。做增量备份时,也是每次从数据卷读入chunk_size字节的chunk数据,然后计算该chunk的每个block的sha值。不同的是,Cinder会把每一个block的sha值与其父备份对应的sha值比较,仅当该block的sha值与父备份block的sha值不一样时,才保存对应的block数据。如果sha值和父备份的sha值相同,说明这个block的数据没有更新,不需要重新保存该block数据,而只需要保存sha值。当然,如果有多个连续block的sha值都不一样,则保存时会合并成一个object,通过元数据记录该object在原volume的偏移量以及长度。
如图1所示,假设一个chunk分为9个block,每个block为100KB,注意每个block都保存了sha256值,图中没有标识。基于该chunk做一次增量备份后,假设只有block 2、7、8有更新,则增量备份只会保存block 2、7、8,由于7和8是连续的,因此会自动合并成一个chunk,而block 2单独形成一个chunk,即原来的chunk分裂成了两个chunk,但总大小为300KB,节省了1/3的存储空间。
图1 增量备份原理图
备份的恢复参考文献[6]讲得非常清楚,这里直接引用:
全量备份的恢复很简单,只需要找到对应的备份,将其内容写回对应的volume即可。那么这里有个问题,每个备份都对应存储上哪些文件呢,每个文件又对于原始volume中哪些数据?还记得创建备份时生成的metadata文件吗,答案就在其中。恢复备份时,会读取这个文件,然后将每一个备份文件恢复到对应的位置。当然,如果有压缩也会根据metadata中的描述,先解压再写入对应的volume中。
增量备份的恢复稍微复杂一些,正如之前的描述,增量备份之间有依赖,会形成备份链,我们需要恢复所选备份及其在备份链上之前所有的数据。在恢复时,需要查询数据库,获取当前备份及备份链上之前的所有备份,其顺序是[所选备份,父备份,父父备份,…,全量备份],恢复的时候会按照相反的顺序依次进行,即首先恢复全量备份,接着创建的第一个增量备份,第二个增量备份,直到所选的备份。每个备份恢复时依赖创建备份时生成的metadata文件,将备份包含的文件,恢复到volume中。每个备份的恢复和全量备份的恢复方式一样。
从备份的原理可以看出,增量备份能够节省存储空间,但随着备份链长度越来越长,恢复时会越来越慢,性能越来越差,实际生产环境中应该权衡存储空间和性能,控制备份链的长度。
Swift、NFS、本地文件系统、GCS等都是使用以上的备份策略,实际上实现也是完全一样的,区别仅仅在于实现不同存储系统的Reader、Writer驱动。
2.2.2 直接导入策略
直接导入策略即把原数据卷导出后直接导入到目标存储系统中。对于支持差量导入的存储系统,增量备份时则可以进一步优化。
以Ceph为例,我们知道Ceph RBD支持将某个image在不同时刻的状态进行比较后导出(export-diff)补丁(patch)文件,然后可以随时将这个补丁文件打到某个image中(import-diff)。即ceph原生支持差量备份,利用该特性实现增量备份就不难了。不过有个前提是,必须保证cinder-volume后端和cinder-backup后端都是ceph后端,否则仍然是一块一块的全量拷贝。
如果是对volume进行第一次备份,则:
- 在用于备份的ceph集群创建一个base image,size和原volume一样,name为"volume-VOLUMD_UUID.backup.base" % volume_id。
- 在原volume创建一个新的快照,name为backup.BACKUP_ID.snap.TIMESTRAMP。
- 在原RBD image上使用export-diff命令导出与创建时比较的差量数据,然后通过管道将差量数据导入刚刚在备份集群上新创建的RBD image中。
如果不是对volume第一次备份,则:
- 在原volume中找出满足r"^backup\.([a-z0-9\-]+?)\.snap\.(.+)$"的最近的一次快照。
- 在原volume创建一个新的快照,name为backup.BACKUP_ID.snap.TIMESTRAMP。
- 在原RBD image上使用export-diff命令导出与最近的一次快照比较的差量数据,然后通过管道将差量数据导入到备份集群的RBD image中。
恢复时相反,只需要从备份集群找出对应的快照并导出差量数据,导入到原volume即可。
注意:
- volume和backup都使用ceph后端存储时,每次都会尝试使用增量备份,无论用户是否传递incremental参数值。
- 使用直接导入策略,不需要元数据信息以及sha256信息。
三、踩过的“坑”
虽然在前期做了大量关于cinder backup的调研工作,但实际部署过程中仍然踩了不少坑,PoC测试过程也非一帆风顺,还好我们在填坑的过程还是比较顺利的。本小节总结我们在实践过程中遇到的坑,避免后来者重复踩“坑”。
3.1 热备份导致quota值异常
我们知道备份是一个IO开销和网络开销都比较大的操作,非常耗时。当对已经挂载的数据卷执行在线备份时,Cinder为了优化性能,减少数据不一致的风险,首先会基于该数据卷创建一个临时卷,然后基于临时卷创建备份,备份完成时会自动删除临时数据卷。从代码中看,创建临时卷时并没有计算quota,换句话说,创建的临时磁盘是不占用quota值的。但删除时调用的是标准的删除接口,该接口会释放对应数据卷占用的数据卷quota值(主要影响gigabytes和volumes值)。也就是说,创建的临时磁盘使volume quota值只减不增,用户可以通过这种方式绕过quota限制。目前该问题社区还未修复,已提交bug:https://bugs.launchpad.net/cinder/+bug/1670636。
3.2 不支持ceph多后端情况
我们内部cinder对接了多个ceph集群,不同的ceph集群通过不同的配置文件区分。但cinder-backup服务向cinder-volume服务获取connection info时并没有返回ceph的配置文件路径,于是cinder-backup服务使用默认的配置文件/etc/ceph/ceph.conf,该ceph集群显然找不到对应volume的RBD image,因此在多backend情况下可能导致备份失败。不过该问题在新版本中不存在了。
3.3 使用ceph存储后端时不支持差量备份
我们前面提到如果cinder-volume和cinder-backup后端都是ceph,则会利用ceph内置的rbd差量备份实现增量备份。那cinder-backup服务怎么判断数据卷对应的后端是否ceph呢?实现非常简单,只需要判断数据卷的连接信息是否存在rbd_image属性,实现代码如下:
- def _file_is_rbd(self, volume_file):
- """Returns True if the volume_file is actually an RBD image."""
- return hasattr(volume_file, 'rbd_image')
社区从M版开始把与存储后端交互的代码独立出来,建立了一个新的项目–os-brick,与之前的ceph驱动存在不兼容,没有rbd_image这个属性。因此backup服务会100%判断数据卷不是ceph后端,因此100%执行全量备份。该问题社区还未完全修复,可参考https://bugs.launchpad.net/cinder/+bug/1578036。
四、我们的改进
4.1 获取父备份ID
当备份存在子备份时,用户无法直接删除该备份,而必须先删除所有依赖的子备份。目前Cinder API只返回备份是否存在依赖的子备份,而没有返回子备份的任何信息,也没有返回父备份的信息。当用户创建了很多备份实例时,很难弄清楚备份之间的父子关系。我们修改了Cinder API,向用户返回备份的父备份id(parent_id),并且支持基于parent_id过滤搜索备份。当用户发现备份存在依赖时,能够快速检索出被依赖的子备份。当然,如果存在很长的父子关系时,需要一层一层判断,仍然不太方便,并且不能很清楚的输出备份的父子关系。于是我们引入了备份链的概念,下节详细讨论。
4.2 引入备份链概念
为了方便查看备份之间的父子关系,我们引入了备份链(backup chain)的概念,一个数据卷可以有多个备份链,每条备份链包括一个全量备份以及多个增量备份组成。我们新增了两个API,其中一个API输出指定数据卷的备份链列表,另一个API输出指定备份链的所有备份点,按照父子关系输出。目前我们的备份链只支持线性链,暂时不支持分叉的情况。通过备份链,用户能够非常方便地查看备份之间的父子关系和备份时间序列,如图2。
图2 备份链展示
4.3 创建备份时指定备份链
创建增量备份时,默认是基于时间戳选择最新的备份点作为父备份,我们扩展了该特性,支持用户选择在指定备份链上创建备份,这样也可以避免备份链过长的情况。
五、后续工作
Cinder backup功能已经相对比较完善了,但仍然存在一些功能不能满足客户需求,我们制定了二期规划,主要工作包括如下:
5.1 级联删除
目前Cinder不支持备份的级联删除,即如果一个备份实例存在依赖的子备份,则不能删除该备份,必须先删除其依赖的所有子备份。如果备份链很长时,删除备份时非常麻烦。在二期规划中,我们将实现备份的级联删除功能,通过指定--force选项,支持删除备份以及其依赖的所有备份,甚至删除整个备份链。
5.2 获取增量备份大小
目前Cinder备份的实例大小是继承自原volume的大小,基于分块策略备份还有Object Count(chunk 数量)的概念,但这只是显示分成了几个chunk,每个chunk大小不一定是一样的,并不能根据chunk数量计算实际占用的存储空间。备份存储空间是我们计费系统的计量标准之一,全量备份和增量备份成本肯定是不一样的,如果价钱一样,则用户并不一定乐于使用增量备份。在二期规划中,我们将实现计算备份占用的实际存储空间的接口。
5.3 备份到S3
很多私有云用户考虑各种成本,不一定会部署额外用于备份的Ceph集群,也不一定需要Swift对象存储,而更倾向于将数据备份到价格低廉、稳定可靠的AWS S3中。目前Cinder backup后端还不支持S3接口,为了满足客户需求,我们计划在二期中实现S3接口,用户可以随时把volume数据备份到S3中。
六、总结
本文首先介绍了数据保护的几种常用技术,指出备份是数据保护的关键技术之一。接下来重点介绍了cinder backup的原理,对比了基于分块备份策略和直接导入策略。然后吐槽了我们在实践中踩到的各种“坑”。最后分享了我们做的一些优化改进工作以及后期工作。
【本文是51CTO专栏作者“付广平”的原创文章,如需转载请通过51CTO获得联系】