基于Berkeley DB的数据库备份系统

Posted by zhangxiaojian on May 2, 2015

趁着五一,劳动一下,把这篇酝酿已久的博客写了。这是去年做的东西,完成了一段时间,经过测试使用还可以,基本可以保证数据库的Durability,无论是突然断电还是磁盘坏了等等。文章的大部分内容都是之前完成的(为了交高级数据库这门课的课程报告….)。适合需要使用Berkeley DB或者其它存储引擎建立自己的备份系统的人阅读和参考。或者是想看看设计思路的~

摘要

本文所述内容基于实验室的INM DBMS系统,原系统使用嵌入式开源数据库Berkeley DB为存储引擎,构建一种全新的语义数据模型。拥有自己的一套不同于SQL的数据库语言。原系统主要着眼于数据模型的构建,ACID的特性均交给存储引擎去完成。虽然存储引擎提供了数据库备份和恢复功能,但不够自动化。原系统在数据库备份与恢复功能上有明显的缺失,随着数据库用在一些中小型网站中,数据不再仅仅来自实验抽取所用,不能丢失。因此需要实现一套备份与恢复系统。

本文主要讲述系统如何设计,如何选择策略,如何解决问题,如何在效率和易用性上进行折中等。文中第一部分简单介绍INM DBMS,第二部分分九个小部分介绍备份的设计,第三部分介绍如何使用备份数据进行恢复。第四部分总结。

一 INM DBMS简简介

INM (Information Network Modle) 是一种语义数据模型。它提供了丰富的语义,更自然地,更直接对真实世界的物体进行建模,能够清晰地描述各种物体之间的复杂的关系,最终实现信息世界和真实世界的对应。由于本文所实现的备份与恢复子系统与数据库语义没有什么关联。就不详细介绍了。大家可以看看之前这篇关于Berkeley DB存储引擎的简介,因为所做工作也是基于它的。

二 备份的设计

2.1 原有系统的情况

数据库在使用过程成会生成日志文件,日志文件中详细记录了数据库的所有信息,可以根据日志文件恢复到任何一个时间点。因此日志文件会随着系统的运行而越来越大,成为磁盘空间的杀手。INM 的日志文件大小为128M,当一个文件写满之后就会重新生成另一个文件来写。写满的文件如果没有包括活动的事务,而且已经执行了一次检查点,那么就可以认为这个日志文件上的数据已经成功写入磁盘,文件就可以删掉了。原有系统就是采用这种方式,为了节省磁盘,开一个线程每隔2分钟执行一次检查点,一旦发现有可删除的日志文件,就毫不犹豫的删除。这样的好处就是节省空间,但是一旦出现数据库崩溃,数据丢失的问题,只有最近使用的日志文件可用,对于恢复就束手无策了。

2.2 冷备份与热备份

在Berkeley DB简介中提到,数据库文件的组织形式还是OS中的文件。那么完全可以使用手动的方式,将整个环境中的数据库文件存储起来,当作一次备份。这是可行的,属于冷备份。要求数据库没有正在活动是事务,或者是数据库停下来了。这在运行一个线上项目的时候是不可能的,最快也只能每天定点维护,像12306一样,每天能够备份一次。这当然不是我们想要的。还有一种热备份技术,可以在数据库运行的任何时间执行备份,Berkeley DB提供命令完成热备份,需要手动在数据库环境目录下执行。

2.3  失效备缓与容灾备份

失效备缓是用于应对磁盘损坏的故障,相当于是某一时间整个数据库的一个快照。一般和数据库环境目录存放在不同的磁盘。当数据库环境所在磁盘故障,立刻可以切换目录到失效备缓目录,数据可以恢复到执行备份时期,好处是切换速度快,可以立刻重启。容灾备份用来应对更大的故障灾害,比如服务器不能用了,真个磁盘坏掉或者是数据中心所在位置发生重大自然灾害。一般并不保存数据文件,而是保存执行过程中产生的日志文件。放在其它服务器上或者是异地的服务器上。不好的地方是切换速度慢,拿到日志文件,需要做一次恢复才能投入使用,恢复的时间取决于日志文件的大小。

2.4 线程还是进程

根据上面的分析,INM 备份系统首先需要改变原来将日志删除的问题,需要在本地建立失效备缓,需要把日志收集起来,存放在其它地方。失效备缓使用Berkeley DB提供的命令即可完成,但关键是执行的间隔。可以在INM 进程中开一个线程,每隔一段时间执行一次备份与同步。值得一提的是,建立失效备缓的代价相当高,需要将所有数据文件拷贝一份,然后将所有日志文件拷贝一份,接着遍历所有日志文件内容,使拷贝的数据和日志保持一致。如果数据库比较大,这一系列的操作可能是秒级的。显然,开一个如此重的线程和数据库执行线程竞争CPU是不明智的。另一个办法是fork一个守护进程,在守护进程中执行这一系列的操作,由OS去对进程进行调度。这样把响应优先级交给OS处理,使备份操作对原有数据库性能影响降到最低。现有的开源数据库PostgreSQL使用的就是进程备份的方式。因此采用守护进程的方式建立失效备缓,时间间隔交给用户,可在配置文件中修改。

2.5  收集日志文件

有了日志文件真是什么都不怕了,备份进程还要做一件事,就是收集日志文件。在主数据库环境中,磁盘空间还是要节省,如果写满,切换磁盘也是麻烦的事。所以要在删除之前确定哪些文件将要被删除,先备份到另一个地方。将要被删除的文件已不包含活动的事务,因此可以使用简单的cp操作进行备份。

比较困难的是日志文件大小的选择,Berkeley DB提供的日志文件默认大小为10M,INM 当前配置为128M,如果文件太大,就算某些数据实际上已经不在需要,但是仍然需要保存在磁盘上,因为暂时不提供部分删除文件内容的方法。如果文件太小,在运行的时候需要频繁的切换日志文件,系统性能会降低,而且对于运行时间长的项目,日志号可能会溢出。结合备份子进程周期和实际项目的写日志速度,理想的情况下是每出现一个可收集的日志文件,就能执行一次备份进程。

当日志文件大小确定后,考虑下面一种情况:

b1

左图有一个写满的日志文件,且符合被收集的条件,和一个写了一部分的日志文件。当执行一次备份进程,1号日志文件将被备份到另一个地方,然后被删除。此时2号文件继续记录数据库信息。假设数据库突然出现了故障,磁盘不能访问了,我们需要日志文件来恢复数据库。但是故障之前只收集了1号日志文件。记录在2号日志文件中的数据将全部丢失。

我们不能在日志文件还在写的时候用另一个线程来备份,这会导致日志文件的破坏或者错误。那么如何将日志数据同步到尽量新的状态 ?

答案在失效备缓中。失效备缓是当前正在运行数据库的快照,它的环境目录下只会保存包含活动事务的日志文件。那岂不是正好,我们首先收集可被收集的日志文件,然后同步一次失效备缓,再将失效备缓目录中的日志文件全部备份到日志收集目录中。因为失效备缓目录的文件不会有活动的事务,因此可以安全的备份。那么数据就能够同步到和失效备缓一致。

b2

上面的例子如果使用这种策略,就能够恢复到如图所示的状态,其中1号日志文件来自主环境的收集,2号来自失效备缓。由于日志文件编号固定,因此下次有更新的日志文件,就会由于同名覆盖掉老的文件。

2.6 多进程的迷思

根据上面的分析,备份进程需要依次做三件事:收集日志,同步失效备缓,备份失效备缓的日志。

b3

由上图所示,主进程和备份进程竞争时间片,由OS调度执行,而备份进程执行一次,完成右图流程图所示的任务。之后根据配置的执行时间进入休眠。前面提到过,虽然建立失效备缓在Berkeley DB中是一个命令,但是其中包含了一系列的操作,都是非常耗时的工作,那就不可能保证备份进程的一个时间片内能够完成一次完整的备份工作,很有可能在执行到一半的时候被OS调走,过一段时间再次获得时间片继续执行。而在被调走的这段时间,主进程很可能又获得了时间片,执行了若干操作,日志文件内容也有所增加。考虑这种情况:

b4

假设备份进程执行日志收集时,日志文件状态如左图,2号文件即将写满。此时执行,1号日志将被收集。完成后备份进程失去时间片,变为等待状态。主进程获得时间片,执行一系列操作,日志文件变为右图所示状态。2号文件已经写满,可以收集。再调度回到备份进程,接着执行同步失效备缓,这一步将会把不需要的日志文件删除。那么1号和2号文件将被删除。但2号文件尚未备份。

起初想到的策略是尽量降低备份进程一个时间片内所做的工作,保证一个时间片就能够完成一次备份工作。需要做的就是减少日志文件大小,增加备份进程执行的频率。因为耗时的主要工作是同步失效备缓时会根据日志文件和数据文件进行一次全扫描并同步。日志文件小了,遍历日志同步的工作自然会少很多。就好像一个时间片能做100件事,我每次只给它5件事,那一定是能够完成的。但后来运行的时候发现并不是这么回事,对于多核的计算机,两个进程可能并行的在两个CPU上获得时间片执行。那么无论将一次备份工作压缩的多小,都无法保证正确。

2.7  检查点

多进程并发出现的错误,一个很大的原因是前面提到的系统原本有一个线程,每个两分钟执行一次检查点。系统的日志能否被删除,一个前提条件就是在日志写满之后,执行了一个检查点。因此不能将执行检查点的任务交给主进程去做。首先把原来INM执行检查点的线程关掉,在备份进程的每一次循环中先执行一个检查点。这样接下来的几个步骤就算被调度中断,再次执行的时候可归档的日志文件是不会变的。因为没有新的检查点。这样就可以保证多进程怎么调度都不会有错。就3.6的例子来看:

b5

进入备份进程,首先在2号日志中执行检查点,如图小箭头所示。然后执行日志收集,1号日志将被收集。之后执行主进程,也可能主进程一直在执行。当备份进程执行失效备缓同步时,日志文件变为右图情况,需要删除不必要的日志,虽然2号日志已经写满,但是3号日志并未包含检查点,因此可被删除的仍只有1号日志。这样就不会出错了。

计算机世界总是公平的,解决了问题,但是会增加一些开销。首先是日志文件占用磁盘大小的开销,因为检查点由备份进程来执行,那么主进程中就很可能含有已经写满,可被收集,但是未执行检查点导致仍然需要留在磁盘上的日志文件。如上例的2号文件。第二个开销是失效备缓,它需要将主进程环境中的全部日志文件复制到自己的目录下,然后遍历所有日志进行同步更新。多了未被删除的日志,所做的工作自然多了不少。

2.8 数据不能少

上面几节实现了失效备缓的建立和日志文件的收集问题。如3.5节所描述,能够将数据恢复到上一次执行备份进程的时候。

b6

假设备份进程半个小时执行一次,那么对于用户来讲,就需要承受最近十几二十分钟提交的事务无效的损失。这不符合事务的持久性。

对于数据库环境损坏,但是磁盘还好,主环境目录中的日志文件还能访问的情况下,可以恢复到和损坏前一致,只需要使用主环境目录下的日志文件覆盖失效备缓中的日志文件,再根据日志同步数据文件即可。但是对于更加严重的故障,导致磁盘中的日志文件不能访问,那么就需要其它方式来恢复。

其实最好的方式是修改Berkeley DB的内核,让它提交一个事务预写日志的时候写两份,一份用来备份。但这种方式难度有点大,需要比较熟悉Berkeley DB 内核,而且对于以后版本升级,维护带来一些麻烦。

站在使用者的观点来看,一个事务无非是一个或多个IQL语句(IQL 语句是INM DBMS的操纵语言)。所以,将IQL语句在词法语法分析之前保存起来,在整个事务执行完成后,将IQL语句写入文件保存起来。那怎么确定到底需要从哪一条语句开始执行呢?当事务执行时,Berkeley DB会为每一个事务分配一个唯一的事务ID,在写入IQL语句的时候,先获得系统的时间和事务的ID,一起记录到文件中。恢复时从哪一条语句开始执行,是由备份进程确定的。

备份进程可以获得最近一次分配的事务ID号,这个ID对应的事务可能早已执行结束,也可能刚刚分配,还在执行中。如果执行备份进程的时候,事务已执行结束,那么失效备缓中此事务的信息必定被同步到日志文件中。因此恢复的时候从该事务ID的下一个事务ID开始恢复。如果事务正在执行,也就是还没有提交,那么失效备缓中就不会同步到该事务。因此恢复的时候就从此事务的ID开始恢复即可。

2.9 效率

INM DBMS为了能够快速响应多用户的请求,采用多线程的方式。因此每个线程都有可能去执行事务。如果记录IQL语句到一个文件中,那么一个线程写的时候,就需要把文件锁住,其它线程要想写,就需要等待。备份进程也要把最近分配的id写进去,那么也要锁,也要有等待。这会降低数据库的响应速度。于是采用每个线程独立写自己的文件。记录线程执行的事务ID和IQL语句。备份进程也是一样,自己写一个文件,记录恢复时需要从哪个事务开始。这样效率是有了,恢复的时候就麻烦了。于是需要写一个独立的程序,用来分析每个文件,生成一个新的文件,包含恢复需要执行的IQL语句。

记录文件使用线程号命名,每个文件控制大小为1G,如果太大,就重新生成一个文件来记录。如图所示:

b7

记录的IQL语句:

b8

txn.id中的记录:

b9

显示恢复时需要从8000003b号事务开始执行。

 三  恢复

3.1 使用失效备缓进行恢复

当主数据库出现问题时,可以使用失效备缓快速恢复,只需要在配置文件将数据库打开的目录从主库切换到失效备缓即可。对应3.9节中就是将目录从./csm 切换到./csmFailover。但是此时数据是上一次执行备份进程时的数据。有两种办法恢复到上一个事务提交后的状态。如果主数据库中日志文件还没有损坏,就使用其中的日志文件覆盖失效备缓目录中的日志文件,再打开使用。如果日志文件已经损坏,在使用失效备缓打开数据库后,执行IQL语句中的分析工具,得到需要执行的IQl语句,执行后即可。

3.2 使用日志文件进行恢复

在2.9节图中所示logArchived目录中含有从数据库运行开始到上一个备份进程执行期间所有的日志文件,使用这些日志文件不仅可以恢复数据到和日志文件一致,也可以恢复到期间的任意一个时间点。要注意的一点是,如果恢复前不备份一份,那么恢复到一个时间点后日志文件也与当前的数据一致,不再能够恢复到最新的一点。在数据库中称作时间线,或是平行宇宙。恢复到与日志文件一致的情况之后,再使用分析工具,确定需要执行的IQL语句即可恢复到崩溃前上一个事务执行的状态。

四  总结

功能已全部实现,测试并已投入使用。先说缺点: 程序是基于Berkeley DB的,实现的时候用了它提供的命令,想在程序中调用,需要配置这些命令对于linux来说全局可见。在做一些判断的时候,用了这些命令行的输出,如果更新Berkeley DB版本的话,也许会有些影响。当初这么做的目的是这样可以保证即使备份子系统出错,数据库原有功能绝对不会受影响。优点的话,就是在第三部分做的一些选择和折中,据了解PostgreSQL也是使用守护进程来收集日志文件,只不过它的日志粒度更小。但对于守护进程上一次执行到数据库崩溃这段时间的数据没有提供保护措施,而是认为收集到的日志文件就应该是安全的。其实无论做多少备份的副本,数据库备份都可能不够用(比如地球爆炸)。所以只能是一定范围的安全和不丢失。作为一个DBMS,应该尽可能提供各种层级的选择给用户,让用户根据自己的需求,资金等等各方面来权衡。