第八章系统故障对策
本章主要讲支持可恢复性这一目标的相应技术,可恢复性是指系统发生某些故障时的数据完整性。支持可恢复性的基础技术是日志,日志以一种安全的方式记录数据的变更历史。
本章主要讨论以下几个问题:
- 数据库日志,通常有三种不同的数据库系统日志(undo,redo,undo/redo)
- 故障恢复,恢复是故障发生后使用日志重建对数据库所作更新的过程。
- 检查点,避免追溯到很久以前的日志。
- 数据备份,它使得数据库不仅能经受暂时的系统故障,并且能够经受数据库丢失的情况。
一、数据库故障模式
我们从可能发生的各种问题及数据库管理系统针对这些问题能做什么,该做什么着手讨论如何应对故障。下面列出最重要的故障模式及DBMS对这些故障所能采取的措施:
-
错误数据输入
例如:电话号码少输入了一位。这种错误通常无法被检测到。
措施:通过编写约束或触发器来防范。 -
介质故障
例如:磁盘一位或少数几位故障,通过磁盘扇区的奇偶校验可检测到。
措施:使用某种RAID模式;维护一个备份;联机保存数据库的冗余拷贝。 -
灾难性故障
例如:火灾,爆炸,恶意破坏。
措施:可采用防范介质故障中的维护一个备份和联机保存数据库的冗余拷贝。 -
系统故障
例如:掉电和软件错误等导致事物状态丢失。
措施:在分离的、非易失的日志中记录所有数据库更新,必要时加以恢复。这正是我们马上要讨论的数据库日志机制建立的原因。
二、数据库事物(Database Transaction)
事物是指用户定义的一个数据库操作序列,它是数据库操作执行的基本单位,要么全部执行成功要么全部执行失败。
1. ACID
数据库引入事物是为了保证数据操作的ACID(Atomicity,Consistency,Isolation,Durability)特性:
a. 原子性(atomicity)
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
b. 一致性(consistency)
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
c. 隔离性(isolation)
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。防止数据丢失。
d. 持久性(Duration)
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
2. 事物管理器和日志管理器
查询处理器 <-- 事物管理器 --> 日志管理器
\ | /
缓冲区管理器 <--> 恢复管理器
|
数据库日志
- 事物管理器:保证并发执行的事务互不干扰,都能够正确执行。
- 事物管理器-->查询处理器: 事物管理器告诉查询处理器使之能够执行查询以及其它构成事物的数据库操作。
- 事物管理器-->日志管理器: 事物管理器将关于事物动作的消息传递给日志管理器,使必需的信息能以“日志记录”的形式存储在日志中。
- 事物管理器-->缓冲区管理器:事物管理器将何时可以或者必须将缓冲区拷贝回磁盘的消息传递给缓冲区管理器。
- 日志管理器:日志管理器维护日志。
- 恢复管理器:日志和数据一样占用磁盘上的空间。当发生数据库系统崩溃时,恢复管理器就被激活,它检查日志并在必要时利用日志恢复数据。
3. 事物的正确执行
数据库的一致性状态必须满足数据库模式的所有约束(实体完整性约束,参照完整性约束,用户定义的完整性约束)。
事物执行的正确性原则:如果事物在没有其它任何事物和系统错误的情况下执行,并且在它开始执行时数据库处于一致的状态,那么事物结束时数据库仍然处于一致的状态。
4. 事物的原语操作
数据在数据库系统中主要有三个地址空间:
- 保持数据库元素的磁盘块空间
- 缓冲区管理器所管理的虚存或主存地址空间
- 事务的局部地址空间
事物要读取数据库元素,首先必须从磁盘被读取到主存的缓冲区中,然后再从缓冲区读取到事物的局部地址空间。事物的写入刚好与此相反。
为了研究日志的方便我们定义一种记法来描述所有使数据在地址空间之间移动的操作:
- INPUT(X): 将包含数据库元素的磁盘块拷贝到主存缓冲区。
- READ(X, t): 将数据库元素X拷贝到事物的局部变量t。
- WRITE(X,t): 将事物局部变量t的值拷贝到主存缓冲区中的数据库元素X。
- OUTPUT(X): 将包含X的缓冲区中的块拷贝回磁盘。
例如:事物T逻辑上由下述两步构成:
A:= A × 2
B:= B × 2
我们可以将T描述为如下几个相关步骤:
READ(A, t); t:=t*2; WRITE(A, t);
READ(B, t); t:=t*2; WRITE(B, t);
详细表述为(假设A=B=8):
动作 | t | 内存中的A | 磁盘中的A | 内存中的B | 磁盘中的B |
---|---|---|---|---|---|
INPUT(A) | 8 | 8 | 8 | ||
READ(A, t) | 8 | 8 | 8 | 8 | |
t:=t*2 | 16 | 8 | 8 | 8 | |
WRITE(A, t) | 16 | 16 | 8 | 8 | |
INPUT(B) | 16 | 16 | 8 | 8 | 8 |
READ(B, t) | 8 | 16 | 8 | 8 | 8 |
t:=t*2 | 16 | 16 | 8 | 8 | 8 |
WRITE(B,t) | 16 | 16 | 8 | 16 | 8 |
OUTPUT(A) | 16 | 16 | 16 | 16 | 8 |
OUTPUT(B) | 16 | 16 | 16 | 16 | 16 |
不难发现,只要所有的这些步骤都执行,数据库的一致性状态就能够保持,但如果系统故障发生在了OUTPUT(A)之后和OUTPUT(B)之前。数据库就会处于不一致的状态了。
我们不能防止这种情况发生,但我们可以在这种情况发生时进行修复——要么A和B都被重置为8,要么都被更新为16。
三、数据库日志
日志是日志记录以追加写的方式构成的文件,每条日志记录了有关某个事物已做的某些操作。日志管理器负责记录数据库的每个重要事件。当数据库系统崩溃时,我们可以用日志将数据库恢复到一个一致性状态。
日志记录的几种形式:
1) <START T>: 事务已经开始。
2) <COMMIT T>: 事物已经完成,并且不会在有修改操作。
3) <ABORT T>: 事物已中止,未能成功完成。
1 undo日志
undo日志,它通过撤销事物在数据库系统崩溃前还没有来得及完成的操作来恢复数据库的一致性状态。
对于undo日志,我们仅需要记录更新记录<T,X,v>,含义是:事物T修改了数据库元素X,而X原来的值是v。更新记录反映的是对主存的修改而不是对磁盘的修改。
如果在使用undo日志的系统中需要进行恢复时,恢复管理器要做的唯一事情是通过恢复旧值消除事务可能在磁盘上造成的影响。简单来说,undo日志不记录数据库元素的新值,而只记录旧值。因此undo日志仅能够还原磁盘改动而不能重做。
1.1 undo日志规则
-
U1: 如果事物改变了数据库元素X,那么形如< T,X,v >的日志记录必须在X的新值刷盘之前写到磁盘。
-
U2: 如果事物提交,则其COMMIT日志记录必须在事物改变的所有数据库元素刷盘之后写到磁盘,但应尽快。
简单来说是这么一个顺序:日志刷盘->被改变的元素刷盘->最后COMMMIT记录刷盘。
举个例子:
步骤 | 动作 | t | Mem-A | Disk-A | Mem-B | Disk-B | 日志 |
---|---|---|---|---|---|---|---|
1) | < START T > | ||||||
2) | READ(A,t) | 8 | 8 | 8 | 8 | ||
3) | t:=t*2 | 16 | 8 | 8 | 8 | ||
4) | WRITE(A,t) | 16 | 16 | 8 | 8 | < T,A,8 > | |
5) | READ(B,t) | 8 | 16 | 8 | 8 | 8 | |
6) | t:=t*2 | 16 | 16 | 8 | 8 | 8 | |
7) | WRITE(B,t) | 16 | 16 | 8 | 16 | 8 | < T,B,8 > |
8) | FLUSH LOG | ||||||
9) | OUTPUT(A) | 16 | 16 | 16 | 16 | 8 | |
10) | OUTPUT(B) | 16 | 16 | 16 | 16 | 16 | |
11) | < COMMIT T > | ||||||
12) | FLUSH LOG |
那么问题来了
- DBMS会同时处理多个事物,日志也是多个事物的日志混记在一起的,因此有可能由于别的事物的FLUSH LOG,过早的将日志落盘。但这对于undo日志没有影响。
- 如果数据库元素A和B在同一个数据块内,有可能由于其中一个元素的刷盘导致另一个元素过早的刷盘而违反规则U1。所以在使用undo日志的数据库我们建议以数据块为最小的封锁单位。
1.2 使用undo日志进行故障恢复
假设此时数据库系统发生了故障,有可能有一个最终尚未提交的事物,有些数据库改动已经落盘有的还没有来得及落盘,这样数据库的状态就不再一致了。
恢复管理器必须使用日志来将数据库从新恢复到某个一致的状态。首先事物管理器从日志尾部开始向上扫描日志(这个顺序是为了恢复到之前的一致状态),扫描过程中记录所有有< COMMIT T >和< ABORT T >记录的事物T,如果看见< T,X,v >则:
- 如果T是被COMMMIT过的,则什么也不做。
- 如果T是一个未完成或者ABORT的事物,则直接将数据库中X的值还原为v。
最后,恢复管理器为每个未终止且未完成的事物T写入一个< ABORT T >记录,标识事务已经结束,然后刷新日志。现在,数据库就恢复正常了,新事物可以继续执行了。
如果在恢复的过程中再次发生崩溃,由于undo日志的设计方式是幂等的,所以恢复操作多次执行与一次执行效果完全相同。
1.3 日志检查点
1.3.1 静止检查点
理论上重放日志需要扫描整个日志文件,可是我们只关心未完成和中止的事物,已经COMMIT的事物日志是没有用的(但是也不能删除)。我们采用周期性的对日志做检查点:
- 停止接收新事物
- 等所有的活跃事物COMMIT或ABORT
- 将日志刷盘并写入< CKPT >
- 开始接收新事物
活跃事物可能很长时间不能完成,导致新事物一直无法执行,这是我们不能接受的。
1.3.2 非静止检查点
在做非静止检查点时,不影响新事物执行,它包括如下步骤:
- 记录< START_CKPT(T1,T2,...,Tk) >并刷新日志,其中Ti表示当前所有活跃事物。
- 等待T1...Tk全部都COMMIT或ABORT,此时允许新事物执行。
- 当T1...Tk都完成时记录< END CKPT >并刷新日志。
日志回放时,从日志尾部向上扫描日志,恢复所有为提交事物的数据元素:
- 如果首先遇到< END_CKPT >,只需要回放日志到下一个< START_CKPT >为止,< START_CKPT >之前的日志可以丢弃;
- 如果首先碰到的是< START_CKPT >, 只需要回放第一步中记录的所有事务(T1,T2,...,Tk)最早开始的地方,再之前的日志记录可以直接删除。
第一种情况举例:
<START T1>
<T1,A,5>
<START T2>
<T2,B,10>
<START CKPT(T1,T2)>
<T2,C,15>
<START T3>
<T1,D,20>
<COMMIT T1>
<T3,E,25>
<COMMIT T2>
<END CKPT>
<T3,F,30>
第二种情况举例:
<START T1>
<T1,A,5>
<START T2>
<T2,B,10>
<START CKPT(T1,T2)>
<T2,C,15>
<START T3>
<T1,D,20>
<COMMIT T1>
<T3,E,25>
2 redo日志
undo日志有一个大问题,就是在我们将事物的数据库元素的改动刷盘前,不能提交该事物。这就使得数据刷盘IO频率很大,使用redo日志就可以避免这种频繁刷盘。
redo log是指在回放日志的时候把已经commit的事务重做一遍,对于没有commit的事务按照abort处理。日志回放并不会处理任何没有commit的事务, 因此,在COMMIT日志持久化之前,不能将数据的修改持久化。因为如果数据在COMMIT之前持久化,那么在系统异常退出的情况下,这种部分修改的事务就会处于一种不一致状态。同时,由于重做事务,因此事务日志中必须记录事务修改以后的值。
2.1 redo日志和undo日志的主要区别
- undo日志记录旧值,redo日志记录新值。
- undo日志,恢复未完成事物,忽略已完成事物。
- redo日志,忽略未完成事物,重做已完成事物。
- undo日志,所有事物改动数据先落盘,后写入COMMIT日志记录。
- redo日志,先写入COMMIT日志记录,所有事物改动数据后落盘。
从数据和日志到达磁盘顺序的角度可以说:数据修改日志都是先于数据落盘;undo日志是数据落盘后写COMMIT日志;redo日志是数据落盘前先写COMMIT日志。
2.2 redo日志规则
在redo日志中< T,X,v >表示:事物T将数据库元素X改为新值v。
- redo log规则:R1:在修改磁盘上的任何数据库元素X以前,要保证与X的这一修改相关的所有的日志记录,包括更新记录< T,X,v >及< COMMIT >记录,都必须出现在磁盘上。
因此当使用redo日志时,与一个事务相关的材料写到磁盘的顺序为:
- 指出被修改元素的日志记录
- 写COMMIT日志
- 修改数据库元素自身
简单描述刷盘顺序就是:改动日志->COMMIT日志->数据库元素的改动
2.3 使用redo日志进行恢复
根据R1规则,如果日志中没有< COMMIT T >,说明事物T的改动都没有落盘,因此我么可以什么都不做。如果日志中有< COMMIT T >,数据改动是否全部落盘无法确定。
redo log恢复步骤:
- 从首部向下开始扫描日志(这个顺序是为了恢复到最后的一致状态),记录所有< T,X,v >。(因为需要重做,所以必须从首部向下扫描)
- 如果T是未提交的,则什么也不做。
- 如果T是已提交的,则将数据库元素X重写为v。
- 最后,为所有未提交的事物写一个< ABORT T >记录并刷新日志。
简而言之,从首部向下,忽略未提交事物,重做已提交事物。
2.4 redo log的checkpoint
- 写日志记录<START CKPT(T1, T2, ..., Tk)>,其中Ti表示当前活跃事务,并刷新日志。
- 当日志记录<START CKPT>写入日志后,把所有已提交事务在缓冲区的新值写到磁盘上去。
- 写日志记录<END CKPT>并刷新日志。
举例:在做检查点的过程中需要将之前已提交事物T1的改动刷新到磁盘。
<START T1>
<T1,A,5>
<START T2>
<COMMIT T1>
<T2,B,10>
<START CKPT(T2)>
<T2,C,15>
<START T3>
<T3,D,20>
<END CKPT>
<COMMIT T2>
<COMMIT T3>
2.5 使用带检查点的redo日志的恢复
下面说一下如何重放带检查点的redo日志:
1)如果最后一条检查点记录是< END CKPT >
a. < START CKPT(t1,...,Tk) >之前提交的事物其修改已落盘,无需恢复。
b. < START CKPT(t1,...,Tk) >中的或之后启动的事物,即使已经提交,其修改都可能未落盘,需要redo。
2)如果最后一条检查点记录是< START CKPT >
不能确定这之前提交事物的修改是否已经全部落盘。需要找到前一个< END CKPT >和与之对应的< START CKPT(S1,...,Sm) >,重做S1,...,Sm中和< START CKPT(S1,...,Sm) >之后启动的事物中所有已经提交的事物(也就是说再匹配1))。
3 undo/redo log
3.1 undo log和redo log的缺陷
undo log 要求数据在事物结束后立即写到磁盘,这会增加磁盘I/O。
redo log 要求我们在事物提交和日志记录刷新以前,将所有修改过的块保留在缓冲区中,这会增加事物需要的平均缓冲区数。
如果数据库元素不是完整的块或块块集,在检测点处理过程中,Undo日志和Redo日志在如何处理缓冲区上存在矛盾。而undo/redo log可以很好的解决这些问题,通过同时记录新旧值,提供了动作顺序上的更大灵活性。
3.2 undo/redo log
日志记录< T,X,v,w >:事物T改变了数据库元素X,由v改为w。
规则:
- UR1:在由于某个事务T所做改变而修改磁盘上的数据库元素X前,更新记录 < T, X, v, w > 必须出现在磁盘上。
也就是说,在修改数据库元素前,必须先写事务更新日志,< COMMIT T >记录可先也可后于数据本身落盘。 - UR2: < COMMIT T >记录一旦出现在日志中,就必须被刷新到磁盘上< FLUSH LOG >。
undo/redo log 也被称作write ahead log。当使用 undo/redo 日志时,与一个事务相关的材料写到磁盘的顺序为:
- 记录update日志
- 更新数据库(不一定持久化,写入缓存中)
- 在合适的时候写commit日志 (在磁盘上任何数据库元素的修改之前或之后,即第3步可能比第2步可能早,或者可能晚)
3.3 重放undo/redo log
重放策略:
- 从前往后遍历日志,redo所有已提交事物。
- 从后往前遍历日志,undo所有未提交事物,并追加< ABORT T >。
3.4 undo/redo log的checkpoint
步骤:
- 写入日志记录< START CKPT(T1,...,Tk) >,其中T1,...,Tk是所有当前活跃事物,并刷新日志。
- 将所有缓冲区中的数据刷盘(包括未提交的事物修改)。
- 写入日志记录< END CKPT >,并刷新日志。
对于2) 需要注意,我们可以容忍将未提交事物写入的数据落盘,所以我们能够容忍小于完整块的数据库元素,并因此共享缓冲区。
唯一要求是:事物在不确定其不会中止之前,不能写入任何值(甚至连写到主存缓冲区也不允许)。
事务恢复中的奇怪行为
读者可能注意到,我们并没有指明在使用 undo/redo 日志恢复时是先撤销还是先重做。事实上,不管是先撤销还是先重做,我们都会面临如下情况:提交并被重做的事务T读取值X,该值是由某个未提交并被撤销的事务U写入的。问题不在于我们是先重做(使X具有U以前的值);还是先撤销(使X具有由T写入的值)。不管哪种方式,这种情况都没有意义,因为数据库的最终状态不对应于任何原子的事务序列的结果。这是由数据库选择的事务隔离级别来决定的。
四、对介质故障的防护
系统故障时磁盘上的数据不会丢失,而主存中的临时数据会丢失。如果以下条件成立,日志系统可以帮助我们恢复系统故障。
a. 日志所在的磁盘不同于存放数据的磁盘。
b. 日志在检查点以后永远不会被丢弃。
c. 日志是redo或undo/redo类型,因为新值被存储在日志中。
备份
如果有可能暂时关闭数据库,我们可以采用一种备份(archive)的方法,再另外的介质上创建一个备份拷贝,并将它们存放在安全的地方。当数据库介质故障时,就可以恢复到该时刻的状态。
如果从备份以来的日志都有得到保存,我们可以使用数据库备份+日志,将数据库恢复到一个更新的状态。
当数据库规模很大时建立完全备份不太现实。因此,数据库备份通常分两个级别:
a. 完全转储,这时需要拷贝整个数据库。也叫做0级备份。
b. 增量转储,这时只需拷贝上一次完全转储或增量转储之后改变的数据库元素。增量转储可以有多次,根据次数被称为i级备份。
我们可以用完全转储 + n次增量转储 + redo或undo/redo日志的方式将数据库恢复到最新的状态。
非静止转储
为防止拷贝的数据库元素只有部分被改变,完全或增量转储都要求数据库暂时停机。非静止检查点试图在磁盘上建立数据库在检查点开始时状态的一个拷贝,我们可以依赖于检查点附近一段时间内的很小一部分日志,弥补和该状态之间的任何偏差。类似地,非静止转储试图建立转储开始时数据库的一个拷贝,转储过程中几分钟甚至几个小时内的数据库元素变化可以通过转储过程中的日志来整理数据,使数据库达到一个新的一致状态。
非静止转储按某种固定的顺序拷贝数据库元素,有可能正好在这些元素被执行中的事务改变时。其结果是,拷贝到备份中的数据库元素可能是也可能不是转储开始时的值。只要在转储持续过程中的日志得到保留,这样的差异可以通过日志来纠正。
网友评论