CMU 15-445 19-Database Logging

Motivation

从事务的角度来说,事务commit之后,使用者就会认为处理好的数据就被放到硬盘上了(并不管底层是如何实现的)

但实际上,出于对性能的考量(硬盘和磁盘的访问速度有明显差距)

实时修改的数据页是不会立刻写入到磁盘上的

而是优先放到内存中的(至于什么时候数据才能落盘,会根据不同的置换策略对内存上的数据进行写入)



因此,如果修改好的数据只保存在了内存中,而并没有写入到磁盘上

那么当内存遭到破坏时(比如说断电、希特勒打仗等突发情况)

就会造成事务数据的丢失

因此需要DBMS的recovery机制




Crash recovery

故障恢复算法是为了确保数据库一致性事务的原子性和面对故障时数据的持久性的一种技术


故障恢复算法主要分为两部分:

第一部分:在正常的事务处理阶段添加一些操作,使得DBMS可以在故障发生时对数据进行恢复(防患于未然;本章的重点

第二部分:在数据库故障发生时执行一些操作,以此维护数据库的原子性、一致性和持久性(利用上一个部分所添加的操作,维护DBMS的ACID特性)



思考:

DBMS的不同组件是依赖于在不同的存储器

比如说缓存是放到内存上的(易失性存储,但访问速度快),而数据一般是放在硬盘上的(非易失性存储,但能持久化数据)

因此,DBMS需要对不同类型的故障进行分类,从而针对不同故障给出应对处理





Failure classification


Transaction failure

事务级别的故障,是正常运行的过程中不可避免的

是DBMS的开发者必须要考虑的


逻辑错误

  • 事务因为某些原因不能完整的执行下去

  • 例如:用户发出rollback指令、事务执行的过程中违背了完整性约束、又或是说并发控制协议(比如OCC)发生了冲突,从而造成事务回滚


内部状态错误

  • DBMS发现内部状态错误(比如事务间的死锁),必须终止某个活跃的事务


System failure

系统级别的故障,比如当数据还在内存的时候,就发生了硬件故障,导致内存中的数据消失了

也是DBMS的开发者需要考虑的


软件故障

  • OS、DBMS本身的故障(例如:DBMS未捕获的除零异常)

硬件故障

  • 存储数据库的电脑崩溃(例如:断电、CPU损坏)
  • 硬件故障的前提是:非易失性存储(硬盘)上的数据不会因为系统崩溃而损坏
  • PS:硬盘故障属于Storage media failure


Storage media failure

存储媒介的故障,比如说SSD、HHD等存储介质出现了问题

或是说磁盘的磁头崩溃,破坏了全部或部分非易失性存储

这种故障属于无法修复的硬件故障,是不可恢复


但另一方面,这类故障是可以被检测出来的(例如:磁盘控制器可以使用校验和来检测故障)


因此这种故障在设计数据库的时候,不会考虑这类问题(当然可以在数据库运维的时候可以备份多份数据,实现容灾)



Observation

DBMS的主要数据都存储在了非易失性存储器上

但是非易失性存储器(例如磁盘)的读取效率远慢于易失性存储器


因此,为了高效的利用易失性存储,实际的读取数据流程:

  • 首先将目标数据读取到内存中
  • 然后在内存中对数据进行修改
  • 最后将修改过的数据放回磁盘中

但另一方面,DBMS还需要对使用者做出如下保证:

  • 一旦某个事务提交了,那么对这个事务的提交便已经持久化(维护持久性
  • 如果某个事务终止了,那么前面的部分修改是不能持久化的(维护原子性;好像这个事务没有发生一样)

其实从上述二者的论述中,发现了设计DBMS的一个矛盾

一方面,为了性能考虑,需要将数据先放到非易失性存储介质上,后续在写回磁盘

而另一方面,为了ACID,需要将数据实时落盘



Undo vs Redo



Undo

删除不完整的事务,或被终止的事务对数据的影响

也就是,对那些不完整的事务(或被终止的事务)中已经完成了操作的数据进行回滚

目的是为了在事务终止的时候,维护原子性(要么都执行,要么都不执行)



Redo

恢复已commited的事务中,已经执行了的操作,以此维护事务的持久性

比如操作日志已经落盘了,但是数据实际上还没有得到修改

那么就要将读取并执行日志中的相关操作




Buffer pool policies

背景:

假设有两个事务A、B,同时对同一个页中不同的数据进行操作


如果事务A要commit,那么此时是否要将数据页刷盘呢?

  • 如果刷盘的话,如果后续事务B又要使用该数据页,就得重新读数据了,浪费时间
  • 但如果不刷盘的话,那该数据页就会一直放在内存中,浪费内存

同时还会带来另一个问题:如果要将数据页刷盘,是只将事务B的数据刷盘,还是把整个数据页(包含事务A和事务B)刷盘?

  • 如果只刷事务B的数据,刷盘效率过低
  • 如果是刷整个数据页,万一事务A发生了回滚,就有需要重新读取数据再修改


Steal policy

DBMS是否允许未提交的事务的数据,覆盖原有的数据(即在修改数据的时候,到底应不应该覆盖原有磁盘上的数据)

steal:允许覆盖(未提交的事务数据可以覆盖原有的数据;不管脏页的数据是否提交,全部都刷盘)

no steal:不允许覆盖(未提交的事务数据不可以覆盖原有的数据;脏页上提交了的数据,才能刷盘)



Force policy

当用户发出commit的时候,是否应该立刻将数据全部都更新到磁盘上

force:必须(提交的事务数据时,必须立刻刷盘

no force:非必要(提交的事务数据时,不需要立刻刷盘



No-steal + Force


优点:

实现上简单方便

不需要撤销终止(回滚)事务的数据修改,因为此时对事务的修改都没有落盘(不需要undo的操作

不需要恢复(重做)对已提交事务的数据修改,因为此时保证已提交的数据都落盘(不需要redo的操作



缺点:

性能上不好(每个事务的提交都需要频繁的刷盘)

同时还会在内存中复制出多份快照(为了不将未提交的事务数据刷盘)

比如说要修改全表的数据,那么就需要将全表的数据都读到内存中(但显然内存大小是有限的)

因此,这种方法是不能支持写的数据量远大于内存容量的




Shadow paging



Example

以树形结构组织数据页,其中根是单个磁盘页

分别维护两个独立的数据库副本:

master:只包含已提交的事务的数据(DB Root指向该版本的数据)

shadow:未提交事务的一个临时数据库




shadow paging的过程:

先将要操作的数据页进行拷贝,即在内存上拷贝一份新的数据

接着后续的写操作都是在这个备份上进行操作

事务commit之后将数据刷入盘中

然后调整DB root指针的指向,使其指向当前新更新的page

最后清理掉原来的page



Undo/Redo

undo:移除shadow页

redo:不需要(因为所有数据都落盘了)



Disadvantages

相比一般的no-steal + force,shadow paging的优点是可以先将一部分数据刷到磁盘上(因为shadow paging是在磁盘上新建的页)


数据页的复制是昂贵的

比如说我们可能只需要修改一个page中的一条数据,但却要将整个page都复制一份(复制成本高)


复制整个页表是非常昂贵的

  • 可以使用B+树结构对页表进行优化

  • 不要复制整棵树,只需要复制树中通往需要修改的叶子节点的路径


事务的提交开销是非常的昂贵

事务提交的时候需要做的事情有:将新数据刷盘,调整DB root中的指向,同时还要删除旧的page(垃圾回收)

数据存在碎片化

  • 一开始数据是连续存储的,但是后续不断的新生成page,接着又删除原有的page,导致数据的存储实际上并不连续,即碎片化

每次只支持一个事务的写入



SQLITE(PRE-2010)

Sqlite就是使用shadow paging(Sqlite主要用在一些嵌入式的设备上,或是一些安卓、苹果等系统上)


Sqlite的shadow paging的具体过程:

当事务开始的时候,Sqlite会将原来的数据页在本地复制一份(放到Journal File区域中)

然后对数据具体的修改都是在内存上进行

接着,如果事务commit的话,就会将内存中的数据刷盘到磁盘上

最后,在Journal File中将对应的page删除


如果刷盘的过程中发生了故障,那么就需要对事务进行回滚

回滚的方式,就是将Journal File中的文件读到内存中,然后再覆盖回原来的文件



Observation

shadowing page依然存在大量对磁盘的随机IO(性能低下)

因此需要一种方法,能够将DBMS的随机IO修改为顺序IO,也就是WAL




Write-ahead log

在磁盘中单独维护一个日志文件,与数据文件是分隔开的

  • 日志中记录的是事务对于DBMS中数据的修改操作

  • 假定日志都存放在稳定的存储介质中

  • 日志中有足够的信息来执行必要的redo和undo操作,以恢复数据库


DBMS必须将对应的日志文件写入磁盘后,才能将对应的修改了的数据页写入磁盘中


策略:steal + no force



WAL-protocol

DBMS先将事务操作的所有日志保存在内存中(一般会在内存中开辟一个空间,用于专门存储日志)

接着,将与更新页面相关的所有日志都保存到了非易失性存储(磁盘)后,才将数据页更新到磁盘上

  • PS:这里对于数据的更新,都是先更新内存中的数据页上,后续才刷盘

只有将事务对应的所有操作日志都写入了非易失性存储(磁盘)后,事务才会被认为是已提交了

  • 也就是说,当用户发送commit的时候,实际上是将日志写入到了磁盘中
  • 而用户的数据页还在内存中
  • 但只要用户的操作日志刷到了磁盘中,就可以保证事务是已提交了的


Log的过程

当事务开始时,向日志中写入<begin>记录,以标记为起点

当事务结束(提交)时:

  • 在log中写入一条<commit>的记录
  • 在写入<commit>的时候,必须要确保所有日志记录在向应用程序返回确认之前被落盘


Log的内容

每个日志条目都包含了关于单个对象变更的信息:

  • 事务Id
  • 对象Id(所操作的数据对象)
  • 前值(修改之前是什么数据;UNDO,用来做undo操作)
  • 后值(修改之后是什么数据;REDO,用来做redo操作)

MySQL中的innodb引擎,就将日志分为的redo log和undo log两部分日志



WAL-example

事务对数据的操作,首先是写入操作相关的日志,接着在将数据读入到内存中进行修改



事务发出commit指令的时候,将buffer中的log写入磁盘中



如果事务提交后,修改的数据页仍在内存中,但是因为断电导致内存中的数据消失了

那么,在后续重启DBMS的时候,可以重新读取log,对事务的数据进行恢复



WAL-implementation

什么时候要将log写入磁盘?

一般来说是用户发出commit指令的时候就将log写入磁盘

但是这样频繁的写入磁盘会导致DBMS的运行效率低下(可用户commit的时候是必须要将log刷盘的)


因此给出的一个优化方法是:组提交

第一个事务commit的时候,不立刻返回,等累积了多个事务commit的时候,一并返回

借助组提交分摊写入磁盘的开销


有个问题:可能会出现相互等待的情况(比如此时只有T1、T2两个事务,但WAL非得等到事务足够多的时候才将日志写入磁盘)

  • 一般用定时器解决:当等待超过了一定的时间后,就直接将日志写入磁盘,不再继续等待

另一个问题:log的page buffer满了该如何处理

  • 此时可以将一些log的page先写入到磁盘上


什么时候将修改好了的数据写入磁盘中?

每次事务数据发生修改的时候,或者当事务提交的时候



在运行时,no-force + steal的性能是最好的(因为steal可以将全部数据刷盘,no-force可以减少刷盘次数)

在恢复时,no-force + steal的性能是最坏的(因为是no-force,所以很多数据都没有立即刷盘,恢复时需要读取日志刷盘;而steal则可能会写入很多没有提交的数据,那么恢复的时候又要读取出来进行undo操作)


但,需要数据库恢复的情况占少数,因此大部分DBMS都选择no-force + steal




Logging schemes

WAL日志的格式

Physical logging

物理日志

记录每个数据页上,在二进制级别上的变化(例如:偏移多少个字节后,数据从什么变为了什么,即数据的变化)

比如:git diff


缺点:如果有个需求是让某一列的数据全部加一,那么物理日志就会变得非常的多,浪费空间(逻辑日志刚好可以解决这个问题)



logical logging

逻辑日志

记录每个事务执行的SQL语句

比如:update,delete和insert查询


优点:每次写入的日志记录的大小少于物理日志记录(比如某个SQL语句修改了大面积的数据集的时候)


缺点:

如果执行的SQL语句是记录当前的某个时间(now函数),但是redo的时候就会有问题(redo的时候重新执行一遍,那此时到底是记录原有的时间,还是现在的时间,无从下手)


再比如说,limit语句,这个语句是不指定到底要输出哪些数据的

因此在主备数据库的时候,可能主数据库有某个索引,备数据库没有这个索引

那么同样执行limit语句的时候就会导致结果不一致


而另一方面,如果有并发日志,很难用逻辑日志实现恢复

  • 难以确定数据库的哪些部分可能在崩溃前被查询修改过

  • redo的时候需要很长时间来恢复,因为需要执行每一个事务的SQL语句


总的来说,就是逻辑日志在恢复的时候,即执行SQL语句的时候会有逻辑问题



Physiological logging

物理日志和逻辑日志的结合,记录的是数据页中,某个元组的数据前后变化

相比物理日志,Physiological记录了元组数据的具体位置,而不是偏移量

相比逻辑日志,Physiological记录了数据的前后变化,恢复上不再有逻辑问题

因为记录的是数据页上某个元组数据的变化,所以允许DBMS对数据页中数据的位置进行重新组织(将空间重新分配)


可这依然没有解决物理日志的问题(给某一列加一,日志如何记录)

而Physiological logging提供了一种mix的记录方式,即可以混合记录日志(实际使用在binlog中)


Physiological logging是目前最流行的日志记录方法

PS:MySQL说自己是物理日志,但是实际上说的是Physiological logging日志


当然,Physiological的恢复成本是高于Physical的,因为它需要DBMS去找到对应位置的数据,而Physical不需要,它直接告诉了DBMS偏移量是多少


为什么MySQL要将redo log和undo log给分开,可以从MVCC的角度理解:

undo log既可以实现数据事务的回滚,页可以用于实现MVCC中,对数据的上一个版本的回推(查找过往数据)

MySQL的undo log和数据是放在一起的,而redo log是单独分开的




Checkpoints

背景:

1、WAL如果不做一些其他的操作,日志就会无限增长

2、日志的不断增长,会导致DBMS一旦崩溃,在恢复的时候就需要读取大量的日志,极其耗费时间



因此,DBMS会定期写下一个检查点(checkpoint

在这个检查点之前的日志和内存中的数据页,全部都会被写入磁盘

那么当DBMS崩溃后,数据的恢复从checkpoint的位置开始恢复即可



Challenges

当写入检查点时,我们必须暂停所有事务以确保快照的一致性(所有的操作都会停止;而暂停会导致吞吐下降)


在崩溃恢复的时候,对于未提交的事务,要执行undo操作

需要扫描过往日志,可能需要花费很长的时间


DBMS执行检查点的频率应该控制在什么范围内?(或者说数据库多长时间存档一次)

  • 太频繁会损耗性能
  • 太稀少又会导致恢复用的时间变长,同时日志太多也会浪费空间


Frequency

检查点会太频繁会导致运行时性能下降(系统花费太多时间刷新缓冲区,给用户的体验就是非常的卡顿)

而checkpoint时间频率低也不行(会使得恢复时间变长,同时日志浪费空间)


发现存档并没有一个非常确定的方案,只有和具体的工程实践相结合才有最优解




Conclusion

WAL几乎是处理易失性存储丢失的最佳办法(并且WAL是连续写,性能上优于随机写)

使用带有checkpoint的增量更新(steal + no force)

在恢复时:使用undo logo对未提交事务进行回滚,使用redo log对提交事务进行恢复


 上一篇
CMU 15-445 20-Database Recovery CMU 15-445 20-Database Recovery
Crash Recovery故障恢复算法是为了确保数据库一致性,事务的原子性和面对故障时数据的持久性的一种技术 故障恢复算法主要分为两部分: 第一部分:在正常的事务处理阶段添加一些操作,使得DBMS可以在故障发生时对数据进行恢复(防患于
2022-12-15
下一篇 
CMU 15-445 18-Multi-Version Concurrency Control CMU 15-445 18-Multi-Version Concurrency Control
Multi-version concurrency control多版本并发控制协议(常常和2PL或TOO一起实现并发控制) 对于DBMS中的每一个数据,都会去记录数据的所有版本(包括历史版本和当前版本) DBMS会维护当前所有数据对象的,
2022-12-01
  目录