首页 > 代码库 > 基于Ceph快照的异地灾备设计

基于Ceph快照的异地灾备设计

引子

技术改变生活。
越来越方便的手机让大家能够更有效的利用碎片时间,我很享受在上下班的公交车上或在阳光明媚的花园里梳理思路,并写成文字上传到简书;要是搁在几年前的PC时代画风可能是坐在星巴克靠窗的桌子上边喝咖啡边敲键盘;如果时间再久远点,应该有间书房有张书桌,书桌边有个磨墨的小书童…

第一种情形跟后两种情形有着本质的区别。咖啡可能洒在键盘上烧坏主板,辛苦一下午写出的文字再也找不回来了;后院可能起火将写了两天的文字烧成灰烬,再也找不回来了。花园散步也可能摔一跤手机掉进水里开不了机,但不必担心因为我的文字早已经实时同步到了简书的服务器,并且不止保存了一份,并且不只保存在一个地区。所以不用担心跌倒,也不用担心地震,因为背后有异地灾备技术。

背景

这是去年初(2016年)为一紧急项目而设计,20K+代码量,陆陆续续花了大半年时间。异地灾备讲究两地三中心,两地指距离较远的两个地方,比如北京到上海一千多公里。三中心指3个数据中心,两个数据中心在同城,另一个在异地。同城两个数据中心间的数据采用同步方式,以IO为粒度。异地数据中心间一般采用异步复制。

Ceph社区有两种方法可以做站点间的数据复制:一种基于快照,主站点备份时为存储块打快照,将快照的差异部分发送到备站点重新生成新快照;另一种是当时社区正在开发的基于Rbd Journal的Rbd Mirror方案。

当时两种方案都有问题。Mirror方案正在开发不成熟,跟数据库配合也是个问题。快照已经很成熟,但有先天的缺陷:一方面打快照对原块的性能有很大影响,尤其是随机IO;另一方面,快照间的差异部分是在备份时计算出来的,因此很耗时,即使两个快照间没有差异也要花上很长一段时间来扫描差异部分。经过权衡,采用了第一种方案。现在社区还有个叫Ceph-backup的开源项目也是基于快照,不过与本方案相比要简陋很多。

总体架构

概念。
主站点,生产环境的数据中心;
备站点,备份环境的数据中心;
备份策略,确定备份的时间段、频率以及快照回收的频率;
备份任务,包括备份哪个存储块,由谁来备份。

网络结构。
主备站点间有独立的灾备网络,灾备网络为千兆带宽,延迟为33毫秒左右。站点内部除了灾备网外还有3个网络:前端、后端和管理网络。前端网络用于连接存储节点和数据库服务器,数据库服务器使用存储集群提供的Lun。管理网络连接管理节点和各个存储节点,管理节点和其中一个存储节点部署在同台服务器。

备份过程主要可以分为3大步:
(1)主站点创建快照,导出两个快照间的差异到文件;
(2)将差异文件拷贝到备站点;
(3)备站点导入差异文件生成和主战点同名的快照,备份过程结束。

这个过程中最重要的问题是数据完整性。快照本身无法保证数据完整性,也就是说,采用应用程序运行过程中创建的快照的数据来重启应用程序并不能保证应用程序回到打快照那一时刻的状态,甚至可能让应用程序出现异常无法启动。这是因为应用程序运行时所依赖的数据包括两部分内容,一部分已经持久化到硬盘,另一部分尚停留在各级缓存中。快照只能恢复出Lun的第一部分数据,第二部分数据对快照来说已经丢失。

第二重要的问题是备份效率,主要来自4个方面。首先,两个数据中心间相距一千多公里,网络延迟33毫秒左右。千兆网络中,如果延迟为0,那么带宽可以达到100兆,可使用Scp命令拷贝大文件来测速。33毫秒延迟的情况下只有原来的30%左右,也就是30兆。其次,扫描快照间差异非常耗时,对10TB的存储块即使只有5M的差异数据也要从头到尾扫描整个存储块。第三,落盘次数过多,一次备份有4次落盘,主战点两次,备站点两次,读两次写两次。最后,存在多个存储块同时备份的情况。

架构总体上还是集中式的结构,包含4种不同角色的进程:Scheduler、Backup、Proxy和Agent。Agent进程部署在每台数据库服务器中,用于感知应用;Scheduler进程主要负责备份、还原、主备切换等任务的调度工作以及所有Backup进程的管理;Backup部署在集群的每个节点,只负责备份或者还原两个任务的数据传输;Proxy进程的功能是转发Agent和Scheduler间的消息,因为Agent跑在前端网络而Scheduler跑在灾备网络,两者无法直接连通。

备份还原

为解决数据完整性问题,Agent提供了一组API接口,允许应用程序以插件的方式提供对应的驱动程序,只有提供了驱动的应用程序才允许备份。

应用程序可以拥有自己独有的保障数据完整性的方法,Agent不关心具体机制,只负责将备份准备、开始、完成以及恢复准备、开始、完成的消息通知给驱动。此外,Agent还提供一个数据通道用于保存驱动程序的私有数据,这些数据在备站点还原时将重新传递给驱动,驱动可以根据这些私有数据来保证应用程序还原到给定的状态。

驱动程序保障数据完整性的最简单暴力的方式是,当接收到准备备份的通知时将缓存Flush到硬盘。不过,数据库一般都有自己的方式。以XX数据库为例,它会维护一个叫做FileLSN的版本号,代表该版本前面的数据已经持久化到硬盘。准备备份时只要维护好FileLSN和快照的对应关系即可,恢复时将数据库滚回到FileLSN的版本。

中途落盘问题。
中途落盘会带来两个影响,一是备份效率低,二是每个节点需要预留足够的磁盘空间用于保存临时的差异文件。对此采取分片传输的策略来解决该问题。分片大小默认为128M,分片从主站点读取出来后绕过本地磁盘和远端磁盘直接写入备战点,待所有分片都完成后在备战点新建同名快照,完成一次备份。分片策略在解决中途落盘问题的同时也为我们提供了一种监控备份进度的手段,进度的最小粒度为分片大小。不过,它也带来了新的问题,如果备份过程中Backup节点异常将导致备站点的存储块成为脏块。这会引入一系列数据一致性的问题。为此,需要一个地方维护记录存储块的状态,我们将这些状态记录在一个Rados对象。

扫描差异效率低的问题。
主站点的Backup进程将备份过程划分为生成分片、读取分片、合并分片、发送分片4个阶段,每个阶段都有独立的若干线程来完成,线程间通过队列交换数据,数据以流水线的方式从上个阶段传递给下个阶段。

生成分片阶段将给定的存储块切割成若干固定大小的区间,每个读取分片阶段的线程领取到一个区间后开始计算区间内两个快照间的差异数据。由于计算比较耗时,所以采用了多个线程并发执行的策略以缩短整体时间。正常情况下每个分片的数据作为一个请求发送给备战点。但实际情况中存储块的大部分区间是空的,发送空分片也会消耗很多额外时间。合并分片阶段甄别出连续为空的分片,将这些分片合并成一个请求。极端情况下,如果两个快照间没有差异,那么只要一个请求就能完成备份了。

负载均衡问题。
备份任务应该分配给哪两个Backup节点?主站点的Scheduler根据备份策略和备站点中存储块的状态新建备份任务,然后将备份任务下发给主站点的Backup节点以启动备份。备份任务是一组关于备份信息的数据集合,其中包含两个Backup节点的地址,一个节点位于主站点用于读取并发送差异数据,另一个节点位于备站点用于接收并写入差异数据到集群。这两个Backup节点分别由主站点和备站点的Scheduler节点选出。

Scheduler选择Backup节点主要考虑两个因素:第一个因素是Backup节点正在运行的备份任务个数,第二个因素是Backup节点最近5分钟内的平均负载。选取两个因素加权和最小的节点,两个因素的权重可配置,默认为6:4。

主备切换

主站点切换为备站点,或者备站点切换为主站点,对服务进程来说意味着元数据和程序行为的改变。

主站点切换为备站点主要划分为三个阶段:停服务、切换元数据、改变程序行为。主站点Scheduler节点接收到切换为备站点的请求后会生成3个对应于停服务、切换元数据和改变程序行为的站点级任务,将这些任务写入Task对象,并启动第一个任务。

保护备站点的储存块不受污染。
备份过程中,备站点的Backup进程一直在写储存块,如果存在其它的应用程序同时写储存块的话将引起两个致命问题:
(1)备站点的快照数据和主站点不一致;
(2)其它应用程序会发现数据不稳定,经常被Backup进程改写。
我们为每个储存块提供了两种访问方式,一种通过iSCSI协议,另一种通过RBD私有协议。外部应用程序只允许使用iSCSI协议,内部应用程序默认使用私有协议。为解决污染问题,备站点必须限制参与备份的储存块只支持私有访问协议。因此在主站点切换为备站点的停服务阶段,Scheduler首先通知所有Agent让所有外部应用程序停止对储存块的访问,然后通知所有Backup节点取消对储存块的iSCSI协议支持。同样地,在备站点切换为主站点的起服务阶段,Scheduler通知所有Backup节点对参与备份的储存块增加对iSCSI协议的支持。

切换元数据。
我们将服务进程相关的数据划分为两大类,第一类是会影响服务角色判断以及可以动态改变的数据,将这类数据记为元数据;第二类是不影响角色判断的数据,例如线程池大小、端口号、日志级别等,将这类数据记为配置数据。配置数据保存在配置文件,元数据保存在集群元数据池(默认为.backup池)的不同Rados对象内。

总体上元数据又可以划分为3大类:站点级别、储存块级别以及临时任务。站点级元数据包含了主站点的Fsid和备站点的Fsid,Scheduler通过比较自己的Fsid和主站点Fsid来判断自己的角色。切换元数据时只要互换主备站点的Fsid即可。

储存块级元数据包括两部分内容:第一部分记录参与备份的主站点储存块和备站点储存块之间的对应关系,同时保存在主站点和备站点;第二部分描述每个储存块的状态,主站点只描述主站点的储存块,备站点只描述备站点的储存块,因此各自保存。切换元数据时只要切换第一部分内容即可。

临时任务元数据是为了应对Scheduler节点故障而设计,是运行时数据不需要切换。

改变程序行为。
元数据切换完成后,随着站点角色的转变,Scheduler的部分行为已经完成改变。除此之外,Scheduler还有一些子服务需要重新激活或关闭,例如备份策略任务、快照回收任务等。

备站点切换为主站点也划分为三个阶段:切换元数据、改变程序行为、起服务。

故障处理

Scheduler故障。
Scheduler乃系统中枢,负责备份策略、快照回收、心跳检测等核心功能。如果Scheduler故障将导致整个系统无法工作。为此对Scheduler做了高可用,通过Keepalived实现。

Keepalived为主节点配置一个虚IP地址,并检测每个节点,当发现主节点故障后将Vip切换到新节点。虚IP漂移涉及到两个问题,一是新旧节点间如何共享数据,二是旧节点故障前正在处理的请求可能会丢失。对第一个问题,我们将所有元数据保存在集群元数据池,因此新旧节点都可以共享数据。第二个问题常用的解决思路是持久化每个消息到元数据池,不过我们没有采用这种方法而是采用了更简单的重试方法。由于Scheduler已经做了高可用,因此我们假设Scheduler节点是永不宕机的。Scheduler客户端在超时或者对端关闭引起错误时将重发请求。

Backup故障。
Backup节点故障最直接的影响是排队中和正在运行的备份任务要么还没开始执行要么只备份了一部分内容就结束了。针对这种情况,我们实现了断点续备功能。Scheduler将备份任务分配给Backup节点后将定期检查备份任务的执行情况,在发现Backup节点连接中断或者任务凭空消失时将重新为给定的储存块准备一个新的备份任务以继续完成剩余内容的备份。断点续备的关键是如何找到上次备份的结束位置,其实只要以备站点接收到的数据为准即可。

Backup节点离线的一个结果是不能再继续接受备份任务了,因此Scheduler要及时检测到离线的节点,避免将备份任务分配给无效的节点。

Backup故障对状态监控的影响。
监控服务通过读取储存块的状态元数据来在界面中显示储存块的状态,其中的备份状态包括空闲、备份中、备份中断、本地恢复、远端恢复、恢复中断等。如果备份任务失败,那么应该在界面中显示备份中断或恢复中断。

如果备站点Backup退出或者灾备网络断开,那么对应的主站点Backup能够检测到这类故障并做好元数据的修改,并不会导致主站点的监控数据出错。

如果主站点Backup异常退出或重启将来不及修改状态元数据,从而导致监控数据出错。同样地我们借助Scheduler来检查任务是否异常,发现异常后修正状态元数据。对每个备份任务,Scheduler一方面将其保存在元数据池(即上文提及的临时任务),另一方面将任务发送给Backup节点执行。在元数据池中保留备份任务的存根带来两个好处:一是应对Scheduler故障,二是应对Backup故障。

主备切换过程故障。
主备切换涉及到Agent、Scheduler、Backup每个服务进程,切换过程中有部分故障可以自动修复但也有一些故障需要人工干预,例如存在离线的Backup节点、数据库故障等。对需要人工干预的故障问题,提供了“重新激活”任务的运维工具。

举个例子,假设主备切换总共需要完成10个任务,在执行到第5个任务时某个Agent出现故障。这时需要运维人员介入先处理Agent故障,然后使用运维工具重新激活切换任务,此时将重新运行第5个任务。

状态监控

(待续)

附加功能

一致性组。
备站点查看备份数据。

(待续)

存在的问题

储存层异地灾备的价值思考。


基于Ceph快照的异地灾备设计