段恢复与写前日志Segment-Based Recovery Write-ahead logging revisited

摘要

本论文重新讨论了写前日志,然后去掉了两个核心假设:

  • 页面是恢复单元
  • 时间戳(lns)应该存储在每个页面上。

恢复单个应用程序级对象(而不是页面)简化了对对象大小与页面大小不同的系统的处理。我们将展示如何在页面上消除对lsn的需要,从而为大型对象启用DMA或零复制I/O,增加并发性,并减少应用程序、缓冲区管理器和日志管理器之间的通信。

我们的实验表明,松散耦合显著降低了组件之间的延迟影响。这使得该方法特别适用于大型分布式系统,并支持分布式系统和事务存储的思想的“交叉传播”。然而,这些优势是有代价的:段与physiological redo的不相容阻止了一些重要的优化。我们展示了分配如何使(或防止)ARIES页(和physiological redo)与段混合。我们提出了一个分配策略,以避免使其他ARIES和LSN-free页面组合复杂化的不希望的交互,然后证明这两种方法和我们的组合是正确的。

引言

作者在重新审视写前日志之后,移除了两个基本假设:

  • 页面是数据恢复的基本单位
  • 每个页面都有一个日志序列号(log-sequence number, LSN)

页面对于空间管理和作为与磁盘的传输单元仍然很有用。页面和段可以很好地一起工作(就像在体系结构中一样),在我们的例子中,可以保持与传统的面向页面的数据结构(如b -树)的兼容性。我们的第二个贡献是展示如何使用基于段的恢复来消除页面上对lsn的需求。无lsn页面方便了多页面对象,并且通过隐式设置页面时间戳,允许我们对相同页面的更新重新排序,并利用更高级别的并发性。

然而,基于段的重做仅限于盲写(blind write):不检查它们修改的页面的操作。通常,盲写要么将一个范围置零,要么以偏移量写一个字节数组。相反,ARIES重做检查磁盘上页面的内容并支持生理重做(physiological redo)。生理重做假设每个页面在内部是一致的,并在每个页面上存储标题(header)。这允许系统重新组织页面,然后回写更新,而不生成日志条目。这对于经常在页面中合并空间的b树尤其重要。此外,通过仔细安排页面回写,生理操作可以在不记录更新的情况下重新平衡b -树节点。

写前日志

steal/no-force恢复

  • no-force
    事务不需要在提交时就写回。因为redo日志可以在恢复期间对丢失的页面进行重建
  • steal
    缓冲区管理器可以写出脏页面,因为undo日志可以对被覆盖的数据进行重建

基于页面的恢复

基于页面的恢复方式的核心是每个页面都是自一致性的(self-consistent)并且每个页面都使用LSN(log-sequence number)进行标识。LSN可以保证在恢复阶段redo日志中的每个条目只执行一次。

我们提出了一种类似于ARIES的方法,但是它在应用程序数据的粒度上工作。我们将这种恢复单元称为段:一组可以跨越页面边界的字节。我们还提出了基于段的恢复和允许两者共存的ARIES的泛化。将段边界与高级原语对齐可简化并发性并支持新的优化,例如针对大型对象的零复制I/O。

多页面对象

image_63

基于页面的恢复很难应对大对象——当一个对象的大小超过一个页面时。因为即使页面是连续的,LSN也会打破这种连续性。为了能够在读取和写入时保证对象的一致性,需要非常复杂的操作。

而基于段恢复的机制可以避免每个页面的LSN,允许将对象存储为一个连续的段。这将使得DMA和零拷贝I/O成为可能。

应用与缓存交互

image_64

上图(a)显示了更新一个页面上的一条记录的顺序。在传统的数据库中,一个页面上只有一条记录,因此在原子执行上述顺序时,可以使页面版本与日志保持同步。

在上图(b)画出了基于页面的恢复机制的问题。当一个页面中同时存在两个对象A和B,当要对A进行更新时就会产生一个日志记录,但是还没有更新页面,此时产生了一个对B的更新,同时写入了日志条目并更新了页面,那么此时LSN就是B条目的LSN。此时就意味着,比B1小的LSN的记录的更新操作已经更新了页面了但是实际上没有。这就出现了冲突。发生这种问题的原因在对于同一个页面的不同对象而言,任何一个对象的更新产生的LSN都会作用到所有对象上。这本质上是直写缓存(write-through)。

我们希望的策略是回写缓存(write-back):更新只影响缓存副本。

日志重排序

在每个页面上都有一个LSN也使得重新排序日志条目变得困难,即使在独立事务之间也是如此。这会干扰对重要请求进行优先级排序的机制,并且与缓冲区管理器一样,会将日志与应用程序紧密耦合,从而增加同步和通信开销。理论上,只要对象和事务(例如提交记录)中的顺序保持不变,所有独立的日志条目都可以重新排序。但是,一般来说,即使是两个独立事务中的更新也无法重新排序,因为它们可能共享页面。一旦将LSN分配给共享页面上的日志条目,独立更新的顺序就固定了。

使用面向段的恢复,我们甚至不需要在页面更新时知道LSN,并且可以在以后选择时分配LSN。在某些情况下,我们在将日志写入磁盘时分配LSN,这允许我们将高优先级条目放在日志缓冲区的前面。

分布式恢复

面向页面的恢复导致应用程序、缓冲区管理器和日志管理器之间的紧密耦合。紧密耦合在传统的单核机器上可能很好,但在将组件分发到不同的机器时,以及在较小程度上分发到不同的核时,它会导致性能问题。面向段的恢复使组件之间的耦合更简单、更松散。

基于段的恢复机制

预写日志系统的四个组成部分:

  • 日志文件
    每个记录由以下部分组成:

    • LSN 日志中的偏移量
    • 生成该条目的事务ID
    • 条目更改的段
    • 该段是否含有LSN的Boolean
    • redo information (重做需要的所有其他信息)
  • 应用缓存

  • 缓冲区管理器

  • 页面文件

恢复

基于分段的恢复过程分为三步:

  • 分析检查日志内容并重建缓冲器管理器宕机时的内容。这允许后续的步骤忽略日志中的某些内容。
  • 重做历史,使系统恢复到宕机前的状态
  • 撤消回滚不完整的事务和日志补偿记录

基于段恢复的方法也支持steal/no-force。日志条目执行的操作被限制在物理重做(physical redo)和逻辑撤销(logical undo)。物理重做使得即使系统处于不一致状态也能够进行,逻辑撤销则是用于保持事务的一致性。逻辑撤销允许事务在底层数据更改后安全回滚,例如当另一个事务的b -树插入重新平衡了一个节点时。

段页式混合机制

混合策略下的log结构:

LSN 事务ID 修改的段 LSN标志位 preimage postimage
  • LSN标志位
    用于表示该记录是属于页式的还是段式的。LSN为1时表示为页式的。

算法

image_66

上图主要描述了段s的三个lsn的更新过程。

  • s->lsn_stable
    修改了内存中的页面的第一个LSN
  • s->lsn_volatile
    the latest such value
  • log_stable
    写入磁盘的最近的log的LSN

image_67

上图展示了在混合模式下如何进行redo操作。

image_68

上图展示了如何并发更新N个段。

恢复不变性

段和对象

本文使用对象这个术语来指代不考虑数据库其余部分内容而写入的数据。每个对象在物理上都由一组段支持:原子记录、任意长度的磁盘区域。段使用机器原语存储;我们假设硬件能够独立地更新段,也许可以使用额外的机制。与ARIES类似,基于段的存储基于多级恢复[33],这将对象嵌入了一个嵌套结构;可以展开嵌套以找到所有的段。

用s表示一个地址或者一个地址集合或者一个段,用l表示一个log条目的LSN。那么\(s_l\)就是将log的前缀应用于s的初始值之后的段的值:

\[s_l=log_l(log_{l-1}(...(log_1(s_0)))) \]

在时刻t,如果段在缓冲区管理中,那么就用\(s_t^{mem}\)表示,否则就使用\(\perp\)表示;如果段在硬盘上则使用\(s_t^{stable}\)表示。如果\(s_t^{mem}=s_t^{stable} || s_t^{mem}==\perp\),那么我们就说s是干净的段(clean),否则就是脏的(dirty)。
\(s_t^{current}\)是保存在s中的值,那么有:

\[s_t^{current} = \left\{ \begin{aligned} s_t^{mem} & & {if s_t^{mem}\neq\perp}\\ s_t^{stable} & & {otherwise} \end{aligned} \right. \]

具有相干缓冲区管理器的系统保持\(s_t^{current}=s_{l(t)}\)的不变性,其中\(l(t)\)是时间t最近的日志条目的LSN。非相干系统允许\(s_t^{current}\)过时,并保持较弱的不变性:\(\exist l' \le l(t): s_t^{current} = s_{l'}\)

尽管一个页面拥有多个对象,但是如果它们被原子更新,那么恢复它们将会视之为一个段/对象。否则,我们将把它们视为若干个段的数组,每个字节就是一个段。

Coherency vs. Consistency

我们定义一个集合:

\[LSN(O) = {l: O_l = O} \]

来表示所有\(O_l\)等于某个版本\(O\)的对象的LSN。

基于页面的存储系统的每个页面s都有一个LSN,s.lsn,并且有\(s.lsn \in LSN(s)\),因为页面的LSN总是被设置为最近的log条目的LSN。如果s不是一个页面,或者并不显示地包含LSN,那么就有\(s.lsn = \perp\)

一个对象O是损坏的,如果他是在之前的操作中从未出现的段或者这个段包含一个损坏的对象:

\[\exist segment \quad s \in O: \forall LSN l, s \neq s_l \]

当对象O处于forward操作期间(可能是事务中间)产生的状态时,它是一致的:

\[\exist LSN \quad l: \forall object \quad o \in O, l \ in LSN(o) \]

引理1:\(O\)是一致性的当且仅当它不是撕裂的(is not torn)。

如果一个对象在没有对该对象进行任何正在进行的修改时生成的LSN上是一致的,则该对象是一致的。与对象一样,修改也是嵌套的;如果某些子操作尚未完成,则修改正在进行中。作为特例;事务是对数据库的操作;当没有正在进行的事务时,ACID数据库是一致的。

日志和页文件

日志条目由LSN,e.lsn唯一确定,并指向一个特定对象的操作。日志条目与事务e.tid相关联,这是一组应该以原子的、持久的方式应用到数据库的操作。日志的状态还包括三个特殊的LSN:

  • \(log_t^{trunc}\)
    存储在硬盘上的序列的开始
  • \(log_t^{stable}\)
    存储在硬盘上的最后一个条目
  • \(log_t^{volatile}\)
    内存中最近的条目

Write-ahead与检查点

Write-ahead确保更新在到达页面文件之前到达日志文件。
日志截断和检查点确保所有当前信息可以从磁盘重建。

三阶段恢复

  • 分析检查日志内容并重建缓冲器管理器宕机时的内容。这允许后续的步骤忽略日志中的某些内容。
  • 重做历史,使系统恢复到宕机前的状态
  • 撤消回滚不完整的事务和日志补偿记录

事务回滚

为了支持回滚,我们为每个更高级别的对象更新记录一个逻辑撤消(logical undo),为每个段更新记录一个物理撤消(physical undo)。每一次高级撤销的注册都会使低级逻辑和物理撤销失效,事务提交也是如此。无效的撤销操作将被视为不再存在。

分配

分配主要有两种方法。第一种方法通过适当放置数据和避免重用最近释放的资源来避免不安全的冲突。第二种方法确定何时将数据写入日志,确保系统中某处存在由正在进行的事务释放的数据副本。
如下图所示是四种分配策略。
image_65

前两种策略记录预映像,导致额外的日志记录成本;第三个选项(标记为“XOR”)指的是将新值作为旧值的函数存储的任何差异日志记录策略;第四个等待重用空间,直到释放空间的事务提交。这使得它不适用于释放空间以便立即重用的索引和事务。

差分日志记录被提出作为增加主内存数据库并发性的一种方法,并且必须精确地应用一次日志条目,但是顺序是任意的。相反,我们的方法避免了一次要求,并且仍然能够并行化重做(尽管在较小的范围内)。日志预映像允许其他事务覆盖旧对象所占用的空间。这可能是由于页面压缩造成的,它将页面上的空闲空间合并到单个区域中。因此,对于支持重组的页面,在回收时记录预映像是最简单的方法。对于边界不变的整个页面或段,不会出现页面压缩之类的问题,因此没有什么理由在回收时进行日志记录;相反,事务可以在重用所释放的空间之前记录预映像,或者完全避免记录预映像。

posted @ 2021-11-21 09:03  LightningStar  阅读(595)  评论(0编辑  收藏  举报