数据库恢复子系统的常见技术和方案对比(二)

作者:实验室小陈/大数据开放实验室

上一篇文章《数据库恢复子系统的常见技术和方案对比(一)》中,我们基本介绍了数据库管理系统中的Logging & Recovery恢复子系统,详细讨论了基于Physical Logging的主流恢复算法ARIES的概念和技术实现。本文将华师大宫学庆教授关于介绍Logical Undo Logging 的原理以及两种数据库系统SQL Server(Azure)和Silo的恢复技术的介绍分享给大家。

— Logical Undo Logging —

在上篇文章中,我们简单介绍了Early Lock Release的优化思路:通过将索引上的Lock提前释放以提高并发程度,但同时会在事务之间产生依赖关系,导致级联回滚。比如第一个事务已经释放锁,但在刷日志时出现故障需要回滚,此时锁已被下一个事务获得,那下一个事务要和前面的事务一起回滚,极大地影响系统性能。

基于这样的情况,恢复系统中引入了Logical Undo Logging,在一定程度上解决了上述问题。Logical Undo Logging的基本思想是在事务回滚时撤销修改操作,而不是对数据的修改,如插入的撤销是删除,对数据项+1的撤销是对数据项-1。

此外处理Logical Undo时引入了Operation Logging的概念,即记录日志时对事务一些操作采用特殊的日志记录方式,称为Transaction Operation Logging。当Operation开始时,会记录一条特殊的Operation Logging,形式为log<Ti, Oj, operation-begin>,其中Oj为唯一的Operation ID。在Operation执行过程中,系统正常进行Physical Redo和Undo日志记录;而Operation结束后,则会记录特殊的Logging <Ti, Oj,  operation-end, U>,其中U便是对于已经进行的一组操作的Logical Undo操作。

举例来说,如果索引插入新键值对(K5, RID 7)到叶子节点Index I9,其中K5存储在Index I9页面的X位置,替换原值Old1;RID7存储在X+8位置上替换原值Old2。对于该事务,记录日志时会在最前面添加操作开始记录<T1, O1, operation-begin>,中间Physical Logging正常记录数值的更新操作,结束时记录结束日志<T1, O1, operation-end, (delete I9, K5, RID7)>,其中(delete I9, K5, RID7)是对T1操作的Logical Undo,即删除I9节点页面的K5和RID7。

数据库恢复子系统的常见技术和方案对比(二)

在Operation执行结束后,可以提前释放锁,允许其他事务能成功向该页面插入< Key,Record ID>,但导致所有Key值重新排序,使得K5和RID7离开X和X+8的位置。此时如果进行Physical Undo,需要将K5和RID7从X 和X+8撤回,但实际中二者的位置已发生改变,按原日志记录进行Physical Undo并不现实。但从逻辑上看,Undo只需要把K5和RID7从索引节点I9删掉即可,因此在日志中加入操作日志(delete I9, K5, RID7),表示如果进行Undo,只需要按照这个逻辑指令进行即可。

在回滚时,系统对日志进行扫描:如果没有operation-end的日志记录,则进行Physical Undo,这是因为只有在Operation结束时才会释放锁,否则就说明锁还没有被释放,其他事务不可能修改其锁定的数据项。如果发现存在operation-end日志,说明锁已经被释放掉,此时只能进行Logical Undo,把begin operation和end operation之间的所有日志跳过,因为这些日志的Undo已被Logical Undo替代。如果在Undo时发现了<T,O, operation-abort>日志,则说明这个Operation已经Abort成功,可以直接跳过中间日志,回到该事务的<T, O,operation-begin>继续往上Undo;当Undo到<T,start>的日志时,表明该事务的全部日志均已经撤销完成,记录<T,  abort>日志表示Undo结束。

其中需要注意的是,所有Redo操作都是Physical Redo,这是因为Logical Redo的实现非常复杂,例如需要决定Redo顺序等,因此大多数系统里所有的Redo信息都是Physical的。此外,Physical Redo和锁提前释放并不冲突,因为Redo只有在出现故障时才会进行,此时系统挂掉导致所有的锁已经都没有了,重做的时候要重新申请锁。

 

—SQL Server:Constant Time Recovery —

 

对于大多数商业的数据库系统如SQL Server等,大多采用ARIES恢复系统,因此Undo所有未Commit事务的工作量与每个事务所做的工作内容成正比。事务做的操作越多,Undo所花费的时间就越长。因为事务操作可能是一条语句但更新很多记录,Undo就需要对记录逐条撤销,导致撤销时间可能过长。虽然正确性得到保证,但对于云服务或者对可用性要求高的系统将难以接受。为了应对这种情况,出现了CTR优化技术(Constant Time Recovery),将ARIES系统和多版本并发控制相结合,实现固定时间恢复。固定时间恢复是指不管遇到什么情况,都利用数据库系统中的多版本信息,保证恢复操作在确定的时间内完成。其基本思想是利用多版本数据库系统中的不同数据版本,确保数据Undo到一个正确的状态,而不是用原来WAL日志里的信息进行恢复。

  • MS-SQL的并发控制

SQL Server在05年开始引入了多版本并发控制,但早期多版本仅用来实现Snapshot隔离级别而非恢复,支持系统根据Snapshot的时间戳去读数据库中的数据。在更新数据时,多版本并发控制可以在数据Page上对记录进行就地更新,但旧版本没有丢掉,而是被单独放在Version Store中。Version Store是一个特殊的表,只允许不断的添加数据(Append-Only),通过指针链接指出该记录的老版本,老版本又会指向上一个更老的版本,进而构成一个数据版本链。在访问数据时,可以根据事务的时间戳来决定读取哪个版本数据。因此,早期多版本Version Store的更新并不需要记录日志,因为一旦出现故障,重启以后所有时间戳都是最新的,只要保持最后一个版本即可,不会有比这个版本更早的Snapshot访问需求。对于当前Active的事务,可以根据当下时间戳,通过Garbage Collection机制将老版本丢弃。

数据库恢复子系统的常见技术和方案对比(二)

CTR基于原Version Store进行优化,实现了Persistent Version Store,使得旧版本能够持久化存储。在CTR技术下,系统更新Version Store时会记录日志为恢复进行的准备,使得Version Store的体量和开销变大,于是出现两种实现老版本存储的策略In-Row Versioning和Off-RowVersioning。

In-Row Versioning是指对于更新一条记录的操作,如果只是改动很小的数据量,就不需要放进Version Store存储,而是在记录后加一个delta值,说明属性的数值变化。其目的是为了降低Versioning时的开销,因为同一个位置改动的磁盘I/O相对较小。

Off-Row Versioning则有一个特殊的系统表,用来存储所有表的老版本,通过WAL记录Insert操作的Redo记录。当修改量较大导致In-Row Versioning无法完全保存数据更新时,便采用Off-Row Versioning方式。如下图中,A4行的Col 2为444,在更新为555后会写入一个delta,用于记录版本变化。但这种修改受限于数据量的大小,以及记录本身所在的页面上是否有空闲空间。如果有空闲空间就可以写入,若没有就需要把更新的记录放入Off-Row Versioning表中。

 

数据库恢复子系统的常见技术和方案对比(二)

恢复过程中,CTR分为三个阶段实现(Analytics、Redo和Undo)。分析阶段和ARIES类似,用来确定每一个事务的当下状态如Active、Commit和需要Undo的事务。在Redo时,系统会把主表和Version Store的表重放一遍,都恢复到出现crash的状态。Redo完成后,数据库就可以上线对外服务。CTR的第三步是Undo,在分析阶段结束后,已经知道哪些事务未提交,Undo阶段可以直接将这些事务标记为Abort。由于每条Record的不同版本都会记录与该版本相关的事务号,因此后续事务在读到该版本时,首先判断相关事务的状态,如果是Abort就忽略掉该版本而读上一个旧版本。这样的恢复方式使得在读到不可用版本时,需要根据链接去找前一个版本,虽然会带来额外性能开销,但减少了数据库的下线时间。在继续提供服务后,系统可以在剩下时间进行Garbage Collection,将失效老版本慢慢清除,这样的机制叫做Logical Revert。

  • Logical Revert

Logical Revert有两种方式。第一种是用后台进程Background Cleanup把所有数据块扫描一遍,判断哪些是Garbage可以回收。判断条件为:如果主表中最后一个版本来自于已经Abort的事务,那就从Version Store里拿上一个已经Commit的旧版本放到主表。即使此时不做这件事情,后面使用数据时也是会到Version Store中读数据。因此,可以通过后台的Garage Collection进程,慢慢进行版本搬迁。第二种是如果事务在更新数据时,发现主表里的版本是Abort的事务的版本,就会覆盖该版本,而此时这个事务的正确版本应该在Version Store中。

可以看到,CTR的恢复是一个固定时间,只要前两个阶段结束即可,而前两个阶段所需时间实际上只与事务的Checkpoint有关。如果做Checkpoint的间隔是按照固定的日志大小决定,当Redo阶段结束,数据库便可以恢复工作,且恢复时间不会超过一个固定值。

—Soil : Force Recovery—

Silo是哈佛大学和MIT合作研究的一个高性能内存数据库系统原型,解决了并发程度过高导致的吞吐率下降问题。如果一个CPU内核对应一个线程来执行事务,在不存在竞争的情况下,吞吐率随着核数增加而增加,但高到一定程度后会出现下降,可能的情况是因为出现了某些资源竞争所导致的瓶颈。尽管在事务执行过程中每个线程单独执行,但最终所有事务在提交前都需要拿到事务ID,事务ID是全局范围的,事务通过原子性操作atomic_fetch_and_add(&global_tid)获得commit时的ID。而事务ID的分配通过全局的Manager角色实现,在事务申请ID时,Manager会通过计数+1来更新事务计数器,保证事务ID的全局唯一且递增,因此Manager写操作的速度会是系统性能的上限。当并发越来越高且事务都去申请ID时,就会出现竞争关系使得等待时间变长,导致吞吐率下降。

数据库恢复子系统的常见技术和方案对比(二)

  • 乐观的并发控制

对于性能瓶颈问题,Silo的解决思路是多核并发工作+共享内存的数据库里采用乐观并发控制。乐观并发控制在《内存数据库解析与主流产品对比(三)》中介绍过,指事务在执行时认为相互之间没有任何影响,仅在提交时检查是否存在冲突,如果没有冲突再去申请全局的事务ID完成Commit。而Silo通过设计Force Recovery取消了所需的全局事务ID,使用Group Commit的概念,将时间分成多个Epoch,每个Epoch为40毫秒。Epoch包含了当前时间段涉及的多个事务,因此可以通过提交Epoch的方式将这一组事务一起提交,不需要为每个事务逐一申请全局事务ID。但这种设计的缺陷在于如果事务执行时间超过40毫秒,带来的跨级会对提交和恢复带来影响。

在Silo中,每个事务由Sequence Number + Epoch Number来区分,Sequence Number用来决定执行过程中事务的顺序,通过Sequence Number和Epoch Number共同决定恢复策略。每个事务会有事务ID(TID),事务按照Epoch来进行Group Commit,整体的提交按照Epoch Number的先后实现序列化。事务ID为64位,由状态位、Sequence Number和Epoch Number共同组成,其中高位是Epoch Number,中间是Sequence Number,前三位是状态位。每条记录都会保存对应的事务ID,其中状态位用来存放访问记录时的Latch锁等信息,内存数据库和传统基于磁盘的DBMS数据库相比,其中一个重要区别就是锁的管理是和记录放在一起,并不会另外管理Data Buffer和Locking Table。

 

数据库恢复子系统的常见技术和方案对比(二)

  • 事务提交的三个阶段

由于Silo采用标准的乐观并发控制,因此只有在提交时才会检查是否存在冲突。在pre-commit阶段,读数据项时会把数据项中的事务ID存入Local Read-Set,随后再读取数值;而在修改数据记录时,也需要把修改的数据记录放入Local Write-Set里。

Silo事务提交时分三个阶段,第一步为Local Wite-Set里所有要写的记录拿到锁,锁信息保存在事务ID中的状态位,通过原子操作Compare and Set获取;随后从当前的Epoch读出全部事务。系统里有专门线程负责更新Epoch(每40ms+1),所有事务不会去竞争写Epoch Number而只要读该值即可。事务提交的第二步是检查Read-Set,Silo中每个数据项都包含最后一个对其进行更新的事务的ID,如果记录的TID发生变化或记录被其他事务锁住,说明从读到提交的过程中记录已经更改,需要进行Rollback。最后是生成事务TID,在新事务Commit时,TID中的Sequence Number应该是一个大于所有读到的Record的事务ID的最小值,以保证事务递增。提交后只有全部事务落盘后,才会返回结果。

  • Recovery——SiloR

SiloR是Silo的恢复子系统,使用Physical Logging和Checkpoints来保证事务的持久性,并在这两个方面采用并行恢复策略。前面提到,内存数据库中的写日志操作速度最慢,因为日志涉及写盘,磁盘I/O是整个系统架构的性能瓶颈。因此SiloR采用并发方式写日志,并存在以下假设:系统里每个磁盘有一个日志线程,用来服务一组工作线程,日志线程和一组工作线程共享一个CPU Socket。

基于此假设,日志线程需要维护日志缓冲区池(池中有多个日志缓冲区)。一个工作线程如果要执行,首先需要向日志线程索要一个日志缓冲区写日志,写满后交还日志线程落盘,同时再获得新的日志缓冲区;如果没有可用的日志缓冲区,工作线程便会阻塞。日志线程会定时将缓冲区刷到磁盘,缓冲区空出来后,可以继续交给工作线程处理。对于日志文件,每100个Epoch生成1个新日志文件,旧日志文件按照固定规则生成文件名,文件名最后部分用来标识日志中最大的Epoch Number;日志内容则记录了事务的TID和Record更新的集合(Table, Key, Old Value -> New Value)。

 

数据库恢复子系统的常见技术和方案对比(二)

上面介绍了多核CPU中一个核所处理的事情,实际上CPU中每个核都以同样的方式工作,会有一个专用线程来追踪目前哪些日志已经全部刷到磁盘上,并把最新已经落盘的Epoch写到磁盘上的固定位置。所有事务通过比较自己当前的Epoch和已经落盘的Persistent Epoch(以下简称Pepoch),如果小于等于 Pepoch,表明日志已经落盘,可以向客户端返回。

  • 恢复过程

和ARIES恢复系统一样,SlioR也要做Checkpoints。SlioR的第一步是把最后一个检查点的数据读出再进行恢复;由于内存数据库不对索引做日志,因此索引需要在内存中重构。第二步是日志回放,内存数据库只做Redo不做Undo,Redo操作并不是和ARIES一样按正常日志顺序进行,而是从后向前执行直接更新到最新版本。在日志回放时,先检查Pepoch的日志文件,找出最新的Pepoch号,凡是超过该Pepoch号的日志记录,则认为对应的事务没有返回给客户端,因此可以忽略。日志文件的恢复采用Value Logging,对于每条日志,检查数据的Record是否已经存在,如果不存在就根据日志记录生成数据Record;如果存在则比较Record和日志中的TID。如果日志中的TID大于Record中的TID,就证明需要Redo,用日志中的新数据值来替换旧值。可以看到,SiloR的Redo不是像ARIES一样一步步恢复到故障现场,因为ARIES的目的是要将磁盘上的数据恢复到最终正确状态,而SiloR是要把内存当中的数据恢复到最新正确状态。

—In-Memory Checkpoint—

对于内存数据库,恢复系统相比磁盘DBMS系统较为简单些,只要对数据做日志即可,索引则不需要,且只需Redo日志而不需Undo日志。所有数据都是直接覆盖修改,不需要管理Dirty Page,不会存在Buffer Pool和缓冲区落盘问题。但内存数据库仍受限于日志同步时间开销,因为日志依旧要刷到非易失性存储(磁盘)。早期80年代时,内存数据库的研究都是假设内存数据不会丢失,如带电池的内存(断电后还可以用电池支撑一段时间工作);还有非易失性内存NVM(Non-Volatile Memory)等技术,但目前这些技术还远达不到普遍使用,依旧要考虑持久化存储。

持久化系统要求对性能影响最小,不能影响到吞吐量和延迟。为减少对事务执行的影响,内存数据库追求的第一目标是恢复速度,即在故障后可以最快时间内完成恢复。因此串行恢复无法满足速度要求,目前有很多研究工作都集中在对内存数据库的并行日志研究,而并行使得对于锁和Checkpoints的实现都变得复杂。

对于In-Memory的Checkpoints,实现机制通常和并发控制紧密融合,并发控制设计决定了检查点如何实现。理想的In-Memory中Checkpoints第一个要求是不能影响正常的事务执行,并且不能引入额外延迟以及占用太多内存。

Checkpoints种类:Checkpoints分为一致性Checkpoints和Fuzzy Checkpoints两类。一致性Checkpoints指产生的检查点中的数据不包含任何未提交的事务,如果包含则在Checkpoints时去掉未提交的事务;Fuzzy Checkpoints包含已经提交和未提交的事务,在恢复时才把未提交的事务去掉。

Checkpoints机制:Checkpoints机制分为两种,第一种可以通过开发数据库系统自身功能来实现Checkpoint,比如使用多版本存储来做Snapshot;第二种可以通过操作系统级别的Folk功能,拷贝出进程的子进程;随后把内存中所有数据拷贝一遍,但需要额外操作去回滚正在执行且还未提交的修改。

Checkpoints内容:内容上Checkpoint分两种类型,一种是每次Checkpoint都全量拷贝数据;还有一种是增量Checkpoint,每次只做本次与上一次之间产生的增量内容。二者差异在于Checkpoint时数据量以及恢复时所需要的数据量的区别。

Checkpoints频率:有三种Checkpoint频率。一是基于时间的定周期Checkpoint;二是基于固定日志的大小的Checkpoint;最后一种是强制性Checkpoint,如数据库下线后强制进行Checkpoint。

—本文小结—

在本次两篇文章专栏中,我们对数据库系统的恢复子系统进行了介绍,分别介绍了Physical Logging的主流恢复系统ARIES和Logical Undo Logging的相关概念和技术实现。此外,我们介绍了两个数据库系统的恢复策略——SQL Server的CTR固定时间恢复以及内存数据库系统Silo的Force Recovery恢复原理。下一讲将讨论数据库系统的并发控制技术。

 

参考文献:

1. C. Mohan, Don Haderle, Bruce Lindsay, Hamid Pirahesh, and Peter Schwarz. 1992. ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking And Partial Rollbacks Using Write-Ahead Logging. ACM Trans. Database Syst. 17, 1 (March 1992), 94–162. 

2. Antonopoulos, P., Byrne, P., Chen, W., Diaconu, C., Kodandaramaih, R. T., Kodavalla, H., ... & Venkataramanappa, G. M. (2019). Constant time recovery in Azure SQL database. Proceedings of the VLDB Endowment, 12(12), 2143-2154.

3. Zheng, W., Tu, S., Kohler, E., & Liskov, B. (2014). Fast databases with fast durability and recovery through multicore parallelism. In 11th {USENIX} Symposium on Operating Systems Design and Implementation ({OSDI} 14) (pp. 465-477).

4. Ren, K., Diamond, T., Abadi, D. J., & Thomson, A. (2016, June). Low-overhead asynchronous checkpointing in main-memory database systems. In Proceedings of the 2016 International Conference on Management of Data (pp. 1539-1551).

5. Kemper, A., & Neumann, T. (2011, April). HyPer: A hybrid OLTP&OLAP main memory database system based on virtual memory snapshots. In 2011 IEEE 27th International Conference on Data Engineering (pp. 195-206). IEEE.

—————————————————————————————————————————————————————————————————————数据库恢复子系统的常见技术和方案对比(二)

上一篇:【DB笔试面试681】在Oracle中,什么是块清除(Block Cleanout)?


下一篇:glCullFace 、 glFrontFace