《PostgreSQL技术内幕——原理探索》第九章 预写式日志——WAL

0    188    1

Tags:

👉 本文共约19435个字,系统预计阅读时间或需74分钟。

事务日志(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)实现的基础,这两者将分别在第10章第11章中介绍。

尽管理解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的页面到内存中的共享缓冲池。然后向页面中插入一条元组。页面并没有立刻写回到数据库集簇文件中。正如第8章中提到的,被修改过的页面通常称为脏页(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也能从中恢复。(当然如果发生文件系统或物理介质失效,就不行了)

9.2 事务日志与WAL段文件

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

虽说事务日志的容量实际上应该是无限的,但8字节长度的地址空间已经足够宽广了。目前是不可能处理这个量级的单个文件的。因此PostgreSQL中的事务日志实际上默认被划分为16M大小的一系列文件,这些文件被称作WAL段(WAL Segment)。如图9.6所示。

WAL段文件尺寸

从版本11开始,在使用initdb创建数据库时,可以通过--wal-segsize选项来配置WAL段文件的大小。

图9.6 事务日志与WAL段文件

图9.6 事务日志与WAL段文件

WAL段文件的文件名是由24个十六进制数字组成的,其命名规则如下: $$ \begin{align} \verb|WAL段文件名| = \verb|timelineId| + (\verb|uint32|) \frac{\verb|LSN|-1}{16\verb|M|*256}
+ (\verb|uint32|)\left(\frac{\verb|LSN|-1}{16\verb|M|}\right) % 256 \end{align} $$

时间线标识

PostgreSQL的WAL有时间线标识(TimelineID,四字节无符号整数)的概念,用于第十章中所述的时间点恢复(PITR)。不过在本章中时间线标识将固定为0x00000001,因为接下来的几节里还不需要这个概念。

第一个WAL段文件名是$00000001\color{blue}{00000000}000000\color{blue}{01}$,如果第一个段被XLOG记录写满了,就会创建第二个段$00000001\color{blue}{00000000}000000\color{blue}{02}$,后续的文件名将使用升序。在$00000001\color{blue}{00000000}000000\color{blue}{FF}$被填满之后,就会使用下一个文件$00000001\color{blue}{00000001}000000\color{blue}{00}$。通过这种方式,每当最后两位数字要进位时,中间8位数字就会加一。与之类似,在$00000001\color{blue}{00000001}000000\color{blue}{FF}$被填满后,就会开始使用$00000001\color{blue}{00000002}000000\color{blue}{00}$,依此类推。

WAL文件名

使用内建的函数pg_xlogfile_name(9.6及以前的版本),或pg_walfile_name(10及以后的版本),我们可以找出包含特定LSN的WAL段文件。例如:

9.3 WAL段文件的内部布局

一个WAL段文件大小默认为16MB,并在内部划分为大小为8192字节(8KB)的页面。第一个页包含了由XLogLongPageHeaderData定义的首部数据,其他的页包含了由XLogPageHeaderData定义的首部数据。每页在首部数据之后,紧接着就是以降序写入的XLOG记录,如图9.7所示。

图9.7 WAL段文件内部布局

图9.7 WAL段文件内部布局

XLogLongPageHeaderDataXLogPageHeaderData结构定义在 src/include/access/xlog_internal.h中。这两个结构的具体说明就不在此展开了,因为对于后续小节并非必需。

9.4 WAL记录的内部布局

一条XLOG记录由通用的首部部分与特定的数据部分构成。本章第一节描述了首部的结构,剩下两个节分别解释了9.5版本前后数据部分的结构。(9.5版本改变了数据格式)

9.4.1 WAL记录首部部分

所有的XLOG记录都有一个通用的首部,由结构XLogRecord定义。9.5更改了首部的定义,9.4及更早版本的结构定义如下所示:

除了两个变量,大多数变量的意思非常明显,无需多言。xl_rmidxl_info都是与资源管理器(resource manager)相关的变量,它是一些与WAL功能(写入,重放XLOG记录)相关的操作集合。资源管理器的数目随着PostgreSQL不断增加,第10版包括这些:

资源管理器
堆元组操作RM_HEAP, RM_HEAP2
索引操作RM_BTREE, RM_HASH, RM_GIN, RM_GIST, RM_SPGIST, RM_BRIN
序列号操作RM_SEQ
事务操作RM_XACT, RM_MULTIXACT, RM_CLOG, RM_XLOG, RM_COMMIT_TS
表空间操作RM_SMGR, RM_DBASE, RM_TBLSPC, RM_RELMAP
复制与热备操作RM_STANDBY, RM_REPLORIGIN, RM_GENERIC_ID, RM_LOGICALMSG_ID

下面是一些有代表性的例子,展示了资源管理器工作方式。

  • 如果发起的是INSERT语句,则其相应XLOG记录首部中的变量xl_rmidxl_info会相应地被设置为RM_HEAPXLOG_HEAP_INSERT。当恢复数据库集簇时,就会按照xl_info选用资源管理器RM_HEAP的函数heap_xlog_insert()来重放当前XLOG记录。
  • UPDATE语句与之类似,首部变量中的xl_info会被设置为XLOG_HEAP_UPDATE,而在数据库恢复时就会选用资源管理器RM_HEAP的函数heap_xlog_update()进行重放。
  • 当事务提交时,相应XLOG记录首部的变量xl_rmidxl_info会被相应地设置为RM_XACTXLOG_XACT_COMMIT。当数据库恢复时,RM_XACTxact_redo_commit()就会执行本记录的重放。

在9.5及之后的版本,首部结构XLogRecord移除了一个字段xl_len,精简了XLOG记录的格式,省了几个字节。

9.4版本中的XLogRecord结构定义在src/include/access/xlog.h中,9.5及以后的定义在src/include/access/xlogrecord.hheap_xlog_insertheap_xlog_update定义在src/backend/access/heap/heapam.c ;而函数xact_redo_commit定义在src/backend/access/transam/xact.c

9.4.2 XLOG记录数据部分(9.4及以前)

XLOG记录的数据部分可以分为两类:备份区块(完整的页面),或非备份区块(不同的操作相应的数据不同)。

图9.8 XLOG记录的样例(9.4版本或更早)

img

让我们通过几个具体示例来了解XLOG记录的内部布局。

9.4.2.1 备份区块

备份区块如图9.8(a)所示,它由两个数据结构和一个数据对象组成,如下所述:

  1. 首部部分,XLogRecord结构体
  2. BkpBlock结构体
  3. 除去空闲空间的完整页面。

BkpBlock包括了用于在数据库集簇目录中定位该页面的变量(比如,包含该页面的关系表的RelFileNodeForkNumber,以及文件内的区块号BlockNumber),以及当前页面空闲空间的开始位置与长度。

9.4.2.2 非备份区块

在非备份区块中,数据部分的布局依不同操作而异。这里举一个具有代表性的例子:一条INSERT语句的XLOG记录。如图9.8(b)所示,INSERT语句的XLOG记录是由两个数据结构与一个数据对象组成的:

  1. 首部部分,XLogRecord结构体
  2. xl_heap_insert结构体
  3. 被插入的元组 —— 更精确地说,是移除了一些字节的元组。

结构体xl_heap_insert包含的变量用于在数据库集簇中定位被插入的元组。(即,包含该元组的表的RelFileNode,以及该元组的tid),以及该元组的可见性标记位。

在结构体xl_heap_header的代码注释中解释了移除插入元组中若干字节的原因:

我们并没有在WAL中存储被插入或被更新元组的固定部分(即HeapTupleHeaderData,堆元组首部),我们可以在需要时从WAL中的其它部分重建这几个字段,以此节省一些字节。或者根本就无需重建。

这里还有一个例子值得一提,如图9.8(c)所示,检查点的XLOG记录相当简单,它由如下所示的两个数据结构组成:

  1. XLogRecord结构(首部部分)
  2. 包含检查点信息的CheckPoint结构体(参见9.7节

xl_heap_header结构定义在src/include/access/htup.h中,而CheckPoint结构体定义在src/include/catalog/pg_control.h中。

9.4.3 XLOG记录数据部分(9.5及后续版本)

在9.4及之前的版本,XLOG记录并没有通用的格式,因此每一种资源管理器都需要定义各自的格式。在这种情况下,维护源代码,以及实现与WAL相关的新功能变得越来越困难。为了解决这个问题,9.5版引入了一种通用的结构化格式,不依赖于特定的资源管理器。

XLOG记录的数据部分可以被划分为两个部分:首部与数据,如图9.9所示:

图9.9 通用XLOG记录格式

img

首部部分包含零个或多个XLogRecordBlockHeaders,以及零个或一个XLogRecordDataHeaderShort(或XLogRecordDataHeaderLong);它必须至少包含其中一个。当记录存储着整页镜像时(即备份区块),XLogRecordBlockHeader会包含XLogRecordBlockImageHeader,如果启用压缩还会包含XLogRecordBlockCompressHeader

数据部分则由零或多个区块数据与零或一个主数据组成,区块数据与XLogRecordBlockHeader(s)对应,而主数据(main data)则与XLogRecordDataHeader对应。

WAL压缩

在9.5及其后的版本,可以通过设置wal_compression = enable启用WAL压缩:使用LZ压缩方法对带有整页镜像的XLOG记录进行压缩。在这种情况下,会添加XLogRecordBlockCompressHeader结构。

该功能有两个优点与一个缺点,优点是降低写入记录的I/O开销,并减小WAL段文件的消耗量;缺点是会消耗更多的CPU资源来执行压缩。

图9.10 XLOG记录样例(9.5及其后的版本)

img

和前一小节一样,这里通过一些特例来描述。

本人提供Oracle(OCP、OCM)、MySQL(OCP)、PostgreSQL(PGCA、PGCE、PGCM)等数据库的培训和考证业务,私聊QQ646634621或微信dbaup66,谢谢!
AiDBA后续精彩内容已被站长无情隐藏,请输入验证码解锁本文!
验证码:
获取验证码: 请先关注本站微信公众号,然后回复“验证码”,获取验证码。在微信里搜索“AiDBA”或者“dbaup6”或者微信扫描右侧二维码都可以关注本站微信公众号。

标签:

Avatar photo

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复