9.1. 概述

事务日志(transaction log )是数据库的关键组件,因为当出现系统故障时,任何数据库管理系统都不允许丢失数据。事务日志是数据库系统中所有变更(change)行为(action) 的历史记录,当诸如电源故障,或其他服务器错误导致服务器崩溃时,它被用于确保数据不会丢失。由于日志包含每个已执行事务的相关充分信息,因此当服务器崩溃时,数据库服务器应能通过重放事务日志中的变更与行为来恢复数据库集群。

在计算机科学领域,WAL是Write Ahead Logging 的缩写,它指的是将变更与行为写入事务日志的协议或规则 ;而在PostgreSQL中,WAL是Write Ahead Log 的缩写。在这里它被当成事务日志的同义词,而且也用来指代一种将行为 写入事务日志(WAL)的实现机制。虽然有些令人困惑, 但本文将使用PostgreSQL中的定义。

WAL机制在7.1版本中首次被实现,用以减轻服务器崩溃的影响。它还是时间点恢复(Point-in-Time Recovery PIRT)流复制(Streaming Replication, SR) 实现的基础,这两者将分别在 第十章 基础备份与时间点恢复第十一章 流复制 中介绍。

尽管理解WAL机制对于管理、集成PostgreSQL非常重要,但由于它的复杂性,不可能做到简要介绍。因此本章将会对WAL做一个完整的解释。第一节描绘了WAL的全貌,介绍了一些重要的概念与关键词。接下来的小节中会依次讲述其他主题:

  • 事务日志(WAL)的逻辑结构与物理结构
  • WAL数据的内部布局
  • WAL数据的写入
  • WAL写入者进程
  • 检查点过程
  • 数据库恢复流程
  • 管理WAL段文件
  • 持续归档

9.1 概述

让我们先来概述一下WAL机制。为了阐明WAL要解决的问题,第一部分展示了如果PostgreSQL在没有实现WAL时崩溃会发生什么。第二部分介绍了一些关键概念,并概览了本章中的一些关键主题。最后一部分总结了WAL概述部分,并引出了一个更为重要的概念。

9.1.1 没有WAL的插入操作

正如在 第八章 缓冲区管理器 中讨论的那样,为了能高效访问关系表的页面,几乎所有的DBMS都实现了共享缓冲池。

假设有这样一个没有实现WAL机制的PostgreSQL,现在向表A中插入一些数据元组,如图9.1所示。

图9.1 没有WAL的插入操作图9.1 没有WAL的插入操作

  1. 发起第一条INSERT语句时,PostgreSQL从数据库集簇文件中加载表A的页面到内存中的共享缓冲池。然后向页面中插入一条元组。页面并没有立刻写回到数据库集簇文件中。正如 第八章 缓冲区管理器 中提到的,被修改过的页面通常称为脏页(dirty page)
  2. 发起第二条INSERT语句时,PostgreSQL直接向缓冲池里的页面内添加了一条新元组。这一页仍然没有被写回到持久存储中。
  3. 如果操作系统或PostgreSQL服务器因为各种原因失效(例如电源故障),所有插入的数据都会丢失。

因此没有WAL的数据库在系统崩溃时是很脆弱的。

9.1.2 插入操作与数据库恢复

为了解决上述系统失效问题,同时又不招致性能损失,PostgreSQL支持了WAL。这一部分介绍了一些关键词和概念,以及WAL数据的写入和数据库系统的恢复。

为了应对系统失效,PostgreSQL将所有修改作为历史数据写入持久化存储中。这份历史数据称为XLOG记录(xlog record)WAL数据(wal data)

当插入、删除、提交等变更动作发生时,PostgreSQL会将XLOG记录写入内存中的WAL缓冲区(WAL Buffer) 。当事务提交或中止时,它们会被立即写入持久存储上的WAL段文件(WAL segment file) 中(更精确来讲,其他场景也可能会有XLOG记录写入,细节将在9.5节中描述)。XLOG记录的日志序列号(Log Sequence Number, LSN) 标识了该记录在事务日志中的位置,记录的LSN被用作XLOG记录的唯一标识符。

顺便一提,当我们考虑数据库系统如何恢复时,可能会想到一个问题:PostgreSQL是从哪一点开始恢复的?答案是重做点(REDO Point) ,即最新一个检查点(Checkpoint) 开始时XLOG记录 写入的位置。(PostgreSQL中的检查点将在9.7节中描述)。实际上,数据库恢复过程与检查点过程紧密相连,两者是不可分割的。

WAL与检查点过程在7.1版本中同时实现

介绍完了主要的关键词与概念,现在来说一下带有WAL时的元组插入操作。如图9.2所示:

图9.2 带有WAL的插入操作图9.2 带有WAL的插入操作

表A的LSN展示的是表A页面中页首部里pd_lsn类型的PageXLogRecPtr字段,与页面的LSN是一回事。

  1. 检查点进程是一个后台进程,周期性地执行过程。当检查点进程开始执行检查点时,它会向当前WAL段文件写入一条XLOG记录,称为检查点(Checkpoint Record) 。这条记录包含了最新的重做点 位置。
  2. 发起第一条INSERT语句时,PostgreSQL从数据库集簇文件中加载表A的页面至内存中的共享缓冲池,向页面中插入一条元组,然后在LSN_1位置创建并写入一条相应的XLOG记录,然后将表A的LSN从LSN_0更新为LSN_1。在本例中,XLOG记录是由首部数据与完整元组 组成的一对值。
  3. 当该事务提交时,PostgreSQL向WAL缓冲区创建并写入一条关于该提交行为的XLOG记录,然后将WAL缓冲区中的所有XLOG记录刷写入WAL段文件中。
  4. 发起第二条INSERT语句时,PostgreSQL向页面中插入一条新元组,然后在LSN_2位置创建并写入一条相应的XLOG记录,然后将表A的LSN从LSN_1更新为LSN_2
  5. 当这条语句的事务提交时,PostgreSQL执行同步骤3类似的操作。
  6. 设想当操作系统失效发生时,尽管共享缓冲区中的所有数据都丢失了,但所有页面修改已经作为历史记录被写入WAL段文件中。

接下来的步骤展示了如何将数据库集簇恢复到崩溃时刻前的状态。不需要任何特殊的操作,重启PostgreSQL时会自动进入恢复模式,如图9.3所示。PostgreSQL会从重做点 开始,依序读取正确的WAL段文件并重放XLOG记录。

图9.3 使用WAL进行数据库恢复图9.3 使用WAL进行数据库恢复

  1. PostgreSQL从相关的WAL段文件中读取第一条INSERT语句的XLOG记录,并从硬盘上的数据库集簇目录加载表A的页面到内存中的共享缓冲区中。
  2. 在重放XLOG记录前,PostgreSQL会比较XLOG记录的LSN与相应页面的LSN。这么做的原因在第9.8节中描述。重放XLOG记录的规则如下所示:
    • 如果XLOG记录的LSN要比页面LSN大,XLOG记录中的数据部分就会被插入到页面中,并将页面的LSN更新为XLOG记录的LSN。
    • 如果XLOG记录的LSN要比页面的LSN小,那么不用做任何事情,直接读取后续的WAL数据即可。
  3. PostgreSQL按照同样的方式重放其余的XLOG记录。

PostgreSQL可以通过按时间顺序重放写在WAL段文件中的XLOG记录来自我恢复,因此,PostgreSQL的XLOG记录显然是一种重做日志(REDO log)

PostgreSQL不支持撤销日志(UNDO log)

尽管写XLOG记录肯定有一定的代价,这些代价和全页写入相比微不足道。所付出的代价换来了巨大的收益,比如,系统崩溃时的恢复能力。

9.1.3 整页写入

假设后台写入进程在写入脏页的过程中出现了操作系统故障,导致磁盘上表A的页面数据损坏。XLOG是无法在损坏的页面上重放的,我们需要其他功能来确保这一点。

译注:PostgreSQL默认使用8KB的页面,操作系统通常使用4KB的页面,可能出现只写入一个4KB页面的情况。

PostgreSQL支持诸如整页写入(full-page write) 的功能来处理这种失效。如果启用,PostgreSQL会在每次检查点之后,在每个页面第一次发生变更时,会将整个页面 及相应首部作为一条XLOG记录写入。这个功能默认是开启的。在PostgreSQL中,这种包含完整页面的XLOG记录称为备份区块(backup block) ,或者整页镜像(full-page image)

图9.4 整页写入图9.4 整页写入

  1. 检查点进程开始进行检查点过程。
  2. 在第一条INSERT语句进行插入操作时,PostgreSQL执行的操作几乎同上所述。区别在于这里的XLOG记录是当前页的备份区块 (即,包含了完整的页面),因为这是自最近一次检查点以来,该页面的第一次写入。
  3. 当事务提交时,PostgreSQL的操作同上节所述。
  4. 第二条INSERT语句进行插入操作时,PostgreSQL的操作同上所述,这里的XLOG记录就不是备份块了。
  5. 当这条语句的事务提交时,PostgreSQL的操作同上节所述。
  6. 为了说明整页写入的效果,我们假设后台写入进程在向磁盘写入脏页的过程中出现了操作系统故障,导致磁盘上表A的页面数据损坏。

重启PostgreSQL即可修复损坏的集簇,如图9.5所示

图9.5 使用备份区块进行数据库恢复图9.5 使用备份区块进行数据库恢复

  1. PostgreSQL读取第一条INSERT语句的XLOG记录,并从数据库集簇目录加载表A的页面至共享缓冲池中。在本例中,按照整页写入的规则,这条XLOG记录是一个备份区块。
  2. 当一条XLOG记录是备份区块时,会使用另一条重放规则:XLOG记录的数据部分会直接覆盖当前页面,无视页面或XLOG记录中的LSN,然后将页面的LSN更新为XLOG记录的LSN。 在本例中,PostgreSQL使用记录的数据部分覆写了损坏的页面,并将表A的LSN更新为LSN_1,通过这种方式,损坏的页面通过它自己的备份区块恢复回来了。
  3. 因为第二条XLOG记录不是备份区块, 因此PostgreSQL的操作同上所述。

即使发生一些数据写入错误,PostgreSQL也能从中恢复。(当然如果发生文件系统或物理介质失效,就不行了)

下一节:PostgreSQL在逻辑上将XLOG记录写入事务日志,即,一个长度用8字节表示的虚拟文件(16 EB)。