理解数据库日志进行故障修复的原理

一、前言

无论是在数据库,还是其他的业务系统,日志是非常重要的。日志通常在系统中有如下的作用:

1.     业务问题定位。系统开发中,谁没写几个BUG? 有了日志,就能方便快速定位问题,修复系统。这也是我们用日志最多的地方。

2.     系统运行流程监控。 雁过必留痕,通过日志可以进行系统校验,确保系统是按预定的流程运行,而不是你以为的方式。毕竟计算机运行的方式是它以为,而不是你以为。

3.     安全审计。 在无纸化办公的时代,谁做了什么事不是删记录就可以像鸵鸟一样,把头埋在沙里,以为万事无虞。

4.     故障修复。 这个基本是数据库的标配。无论是MySQL还是Redis, 无论是Mongodb还是ES, 都有日志协助修复系统。

本文梳理的重点就是数据库如何利用日志进行故障修复。

 

 

二、系统故障

在《事务处理概念与技术》一书中,记录了故障方方面面的来源:环境、操作、维护、硬件、软件、过程等。

作为用户,我们希望故障修复后,系统能够正常工作。或者说,我们希望系统能够容忍一些故障(容错),以保障系统工作时长更久,996是不够的,最好是7*24。毕竟在自动化处理业务流程的时代,系统处理业务流程的效率太高(例如:支付宝助力天猫双11 OceanBase 每秒处理峰值达到6100 万次)。 停机的代价太大。对于电商和金融,每笔处理的流水都是白花花的银子,系统一罢工,损失不是一个人请假或者离职那么微不足道。

故障虽然出现的概率小,但是致命,不得不防。

 

三、应对措施

 

在《事务处理概念与技术》中,事务的理解很简单:数据库状态的变更。比如,我发工资了,那么我银行卡里余额就变多了,这个余额就是银行卡的一个状态。在发工资这个业务流程中,涉及到的另一端是公司银行卡的钱变少了。

数据库需要保障业务流程中,涉及到的主体状态变更是一致的,不然公司的帐就乱了。

这就引出了事务的特性:ACID。 ACID的规约其实就类似于合同法,当流程正常时是压箱底用不到的,当流程出现争端,才需要用合同的条约解决问题。其实理解ACID,用结婚这个现实中的例子更容易理解。严谨一些,以中国的《婚姻法》为例。

原子性-A:最开始两个人的状态都是【未婚】,领到结婚证的一刻,两个人的状态都变成了【已婚】。 两个人的状态是绑定在一起的。

一致性-C:两个人的状态都从【未婚】变成了【已婚】。这里隐藏了一致性的约束:男方只能有一个现任妻子,女方只能有一个现任丈夫。 如同数据库唯一索引的约束一样。

隔离型-I:两个结婚跟别人无关,不影响其他扯证的情侣。

持久性-D:除非离婚,不然婚姻状态是持续的。


事务的隔离性由锁机制实现,原子性、一致性和持久性由事务的日志保障。

 

理解了基础概念,我们来做思想实验。假如我们的业务流程因为代码Bug(比如除0)中断了,如何保障数据库一致性约束呢?即我们的异常处理终极目的是保障数据库中状态的一致性,确保系统符合业务规范要求。

 

undo日志

如果状态异常了,恢复现场就OK了。比如驾校练车,那就是一遍一遍地roll back。我们通过undo日志记录事务变更前的状态,如果当前事务处理异常,回滚到原来的状态,就像这个事情没有发生一样。用T代表事务,X代表事务变更的元素,v代表X原来的值。那么<T,X,v>就是日志记录的三个必要字段。 例如:<发工资,张三, 0> 代表发工资前,月光族张三的银行卡上没有钱了。

 

系统处理事务中会有如下关键日志信息:

日志<START T> 表示一个事务的开始。

日志<COMMIT T>表示一个事务的提交,日志<ABORT T>表示一个事务中止。对于一个事务,<COMMIT T>和<ABORT T>只会记录一条。

 

一个事务的处理流程如下:

S1:记录<START T>表示开启一个事务。

S2:记录undo日志:<T,X,v>,表示事务元素变更前的状态。(要求持久化)

S3:改变X元素,使其状态变更。(要求持久化)

S4:记录<COMMIT T>,表示事务提交。

 

当系统出现故障,暂停提供服务并启动故障修复程序。

S1: 反向遍历日志,对于已经有<COMMIT T>的事务,无需处理。

S2:对于没有<COMMIT T>的事务,用undo日志将事务恢复到变更前的状态。

注意,故障修复的每个操作,又构成了新的事务。

 

Undo日志修复流程就引发了两个概念:检查点(checkpoint)和幂等(idompotent)。

检查点:由于系统故障概率一般较低,导致积累的日志规模庞大。如果遍历整个历史日志,故障修复的效率就太低了。所以用检查点机制来记录系统无故障的里程牌。如果出现故障,修复程序回滚到里程碑处就可以停止了。

幂等:如果故障修复时候,也出故障了。怎么办?确保日志修复的操作是幂等的。有了这个限制,重试就解决问题了。 所谓幂等,执行一次和执行多次结果是一样的。《事务处理概念与技术》中,有个非常形象的例子:

“把核反应堆的反应棒向下移动2cm”就是非幂等的,

“把核反应堆得反应棒移动到xx位置”就是幂等的。

 

Redo日志

 

使用undo日志,会引发性能问题。即事务改变的数据写到磁盘前,不能提交该事务。这就意味着,没笔事务操作都至少有3次磁盘操作。所谓按下葫芦浮起瓢。必须有一种新的机制,这就是Redo日志的来源。

 

如果说Undo日志代表一种消极的人生态度,Redo日志就是一种积极的人生态度。颇有不破楼兰终不还的气势。即如果系统出现故障,由于我们已经记录了我们要到达的终点,那么失败重试就可以了,退什么退!

类似于undo日志,redo日志记录事务<T,X,v>代表事务T改变元素X的值为v,v就是新值。

使用redo日志的事务流程如下:

S1:记录<START T>表示开启一个事务。

S2:记录redo日志:<T,X,v>,表示事务元素变更前的状态。(要求持久化)

S3:记录<COMMIT T>,表示事务提交。(要求持久化)

S4:改变X元素,使其状态变更。

 

相比undo日志,COMMIT T提前了。这样的话,事务是否完成依然是检查日志中有没有COMMIT信息,但是这个流程已经不能保障有COMMIT的事务数据固化到硬盘了。这就要求检查点生成时,必须保证数据已经落盘。

故障修复流程也是相当简单:

S1: 找出所有已经COMMIT的事务

S2:使用redo日志重新执行原事务的操作

Undo/Redo日志

Redo日志要求在事务提交和日志记录刷新前,将所有修改过的块保留在缓冲区中,这样可能增加事务需要的平均缓冲区数量。而且,如果数据库元素不是完整的块,undo日志和redo日志在检查点过程中,对于如何处理缓冲区都存在矛盾。所以新的方案由出炉了,就是undo/redo综合体,颇有要你命三千的意味。即记录日志的时候,记录<T,X,v,w>4个值,代表事务T改变了元素X,x的旧值是v,新值是w。

使用undo/redo日志的事务流程如下:

S1:记录<START T>表示开启一个事务。

S2:记录undo/redo日志:<T,X,v,w>,事务T改变了元素X,x的旧值是v,新值是w。(要求持久化)

S3:修改数据库元素

S4:记录<COMMIT T>日志。

这里S3和S4并没有明确顺序要求。

这里就可以根据当前缓冲区的状态灵活控制了,<COMMIT T>的修改数据库元素的操作谁先谁后已经不重要了。

 

故障恢复:

S1: 遍历日志

S2:对于已经COMMIT的事务,使用redo重做

S3:对于没有COMMIT的事务,使用undo回滚

后续

本文简单总结了事务、ACID的理解方式。从思想原理上梳理了undo、redo、undo/redo机制的演变过程,也梳理了检查点和幂等两个概念的来源。后续将分析MySQL, ES等数据库的日志实现,结合实际了解其在工业产品中的落地姿势。

 

 

参考

《事务处理概念与技术》

《数据库系统实现》



上一篇:React.js -- Getting Started


下一篇:Android Studio 使用局域网远程调试手机APP