[译] 9. 预写日志(Write Ahead Logging-WAL)
原文地址:https://www.interdb.jp/pg/pgsql09.html
原文作者:Hironobu SUZUKI
事务日志是数据库的重要组成部分,因为所有的数据库管理系统都要求即使发生系统故障也不丢失任何数据。它是数据库系统中所有更改和操作的历史日志,以确保没有数据因故障而丢失,例如电源故障或其他导致服务器崩溃的服务器故障。由于日志包含有关已执行的每个事务的足够信息,因此数据库服务器应该能够通过重播事务日志中的更改和操作来恢复数据库集群,以防服务器崩溃。
在计算机科学领域,WAL 是 Write Ahead Logging 的首字母缩写,它是将更改和操作写入事务日志的协议或规则,而在 PostgreSQL 中,WAL 是 Write Ahead Log 的首字母缩写。该术语用作事务日志的同义词,也用于指代与将操作写入事务日志(WAL)相关的实现机制。虽然这有点令人困惑,但在本文档中采用 PostgreSQL 定义。
WAL 机制首先在 7.1 版本中实现,以减轻服务器崩溃的影响。它还使得基于时间点恢复 (PITR) 和流复制 (SR) 的实现成为可能,它们分别在第 10 章和第 11 章 中进行介绍。
虽然理解 WAL 机制对于使用 PostgreSQL 进行系统集成和管理是必不可少的,但这种机制的复杂性使得无法简要总结其特点,所以PostgreSQL中WAL的完整说明如下。第一部分提供了 WAL 的全貌,介绍了一些重要的概念和关键词。在后续部分中,将描述以下主题:
- WAL(事务日志) 的逻辑和物理结构
- WAL 数据的内部布局
- WAL 数据的写入
- WAL 写进程
- (checkpoint) 检查点进程
- 数据库恢复过程
- 管理 WAL 段文件(Managing WAL segment file)
- 连续归档(Continuous archiving)
9.1 概述
让我们看以下WAL机制的概述。为了清晰了解WAL一直在解决的问题,第一小节描述了如果PostgreSQL不实现WAL情况下,发生崩溃时会发生什么事情。第二小节介绍了一些关键概念和显示本章主要主题的概述,如WAL数据写入和数据库恢复。最后一小节完整介绍WAL和增加一个关键理论。
在本节中,为了简化描述,使用了仅包含一页的表 TABLE_A。
9.1.1 不使用WAL时插入操作
如第 8 章所述,为了提供对关系页面的有效访问,每个 DBMS 都实现了共享缓冲池。
假设我们在没有实现 WAL 特性的 PostgreSQL 上的 TABLE_A 中插入一些数据元组;这种情况如图 9.1 所示。
Fig. 9.1. Insertion operations without WAL.
(1) 发出第一个 INSERT 语句,PostgreSQL 将 TABLE_A 的页面从数据库集簇加载到内存的共享缓冲池中,并在页面中插入一个元组。该页面不会立即写入数据库集群。 (如第 8 章所述,修改后的页面通常称为脏页。)
(2) 发出第二个 INSERT 语句,PostgreSQL 将一个新的元组插入到缓冲池上的页面中。此页面尚未写入存储。
(3) 如果操作系统或PostgreSQL服务器由于电源故障等任何原因出现故障,所有插入的数据都将丢失。
因此,没有 WAL 的数据库容易受到系统故障的影响。
历史信息(Historical Info)
在引入 WAL 之前(7.0 或更早版本),PostgreSQL 通过在内存中更改页面时发出同步系统调用来对磁盘进行同步写入,以确保持久性。因此,INSERT 和 UPDATE 等修改命令的性能非常差。
9.1.2 插入操作和数据库恢复
为了在不影响性能的情况下处理上述系统故障,PostgreSQL 支持 WAL。本小节介绍了一些关键词和关键概念,然后介绍了 WAL 数据的写入和数据库的恢复。
PostgreSQL 将所有修改作为历史数据写入持久存储中,为失败做准备。在 PostgreSQL 中,历史数据称为 XLOG 记录或 WAL 数据。
XLOG 记录通过插入、删除或提交等更改操作写入内存中的 WAL 缓冲区。当事务提交/中止时,它们会立即写入存储上的 WAL 段文件。 (准确地说,XLOG记录的写入也可能发生在其他情况下。详细内容将在9.5节中介绍。) XLOG记录的LSN(Log Sequence Number)表示其记录在事务日志上的写入位置。记录的 LSN 用作 XLOG 记录的唯一标识 ID。
顺便说一句,当我们考虑数据库系统如何恢复时,可能有一个问题; PostgreSQL 从什么时候开始恢复?答案是重做(REDO point)点;也就是最近一次checkpoint启动时写入XLOG记录的位置(PostgreSQL中的checkpoint在9.7节中介绍)。事实上,数据库恢复过程与检查点过程密切相关,这两个过程是密不可分的。
WAL 和 checkpoint 过程在 7.1 版本中同时实现。
主要关键字和概念刚刚已介绍完毕,接下来将介绍使用WAL插入元组。请参见图 9.2 和以下说明。
Fig. 9.2. Insertion operations with WAL.
注释(Notation)
'TABLE_A LSN' 显示了 TABLE_A 的页头的pd_lsn值。page LSN也是同理
(1) checkpointer,一个后台进程,周期性地执行checkpointing。每当启动检查点时,它都会将被称为检查点记录(checkpoint record)的 XLOG 记录写入当前 WAL 段文件中。该记录包含最新的 REDO 点的位置。
(2) 发出第一条INSERT语句,PostgreSQL将TABLE_A的页面加载到共享缓冲池中,在页面中插入一个元组,创建该语句的XLOG记录并将其写入WAL缓冲区的LSN_1位置,并将TABLE_A的LSN从 LSN_0 更新为 LSN_1。
在此例子中,该 XLOG 记录是一对页头数据和整个元组。
(3) 当该事务提交时,PostgreSQL 创建并写入该提交动作的 XLOG 记录到 WAL 缓冲区,然后,从 LSN_1位置开始将 WAL 缓冲区上的所有 XLOG 记录写入并刷新到 WAL 段文件。
(4) 发布第二个INSERT语句,PostgreSQL将一个新的元组插入页面,在WAL缓冲区LSN_2的位置中创建和写入该元组的Xlog记录作用,并将Table_A的LSN从LSN_1更新为LSN_2。
(5) 当此语句的事务提交时,PostgreSQL 的操作方式与步骤(3) 相同。
(6) 想象一下什么时候应该发生操作系统故障。即使共享缓冲池上的所有数据都丢失了,页面的所有修改都已作为历史数据写入 WAL 段文件。
以下说明显示了如何将我们的数据库集簇恢复到崩溃前的状态。这里不需要做任何特别的事情,因为 PostgreSQL 重新启动时会自动进入恢复模式。参见图 9.3。 PostgreSQL 将会从 (REDO) 重做点按顺序读取和重放相应 WAL 段文件中的 XLOG 记录。
Fig. 9.3. Database recovery using WAL.
(1) PostgreSQL 从相应的 WAL 段文件中读取第一个 INSERT 语句的 XLOG 记录,将 TABLE_A 的页面从数据库集簇加载到共享缓冲池中。
(2) 在尝试重放 XLOG 记录之前,PostgreSQL 应该将 XLOG 记录的 LSN 与相应页面的 LSN 进行比较,这样做的原因将在第 9.8 节中描述。重放 XLOG 记录的规则如下所示。
如果XLOG记录的LSN号大于页面的LSN,则将XLOG记录中的数据部分插入到页面,并将页面的LSN更新为XLOG的LSN。另一方面,XLOG记录的LSN号小于页面的LSN,则除了读取下一个WAL数据外别无他法。
在此例中,XLOG 记录被重放,因为 XLOG 记录的 LSN (LSN_1) 大于 TABLE_A 的 LSN (LSN_0);然后,TABLE_A 的 LSN 从 LSN_0 更新为 LSN_1。
(3) PostgreSQL 以同样的方式重放剩余的 XLOG 记录
PostgreSQL 可以通过按时间顺序重放 WAL 段文件中写入的 XLOG 记录来恢复自身。因此,PostgreSQL 的 XLOG 记录显然是 REDO 日志。
PostgreSQL 不支持 UNDO 日志。
虽然写 XLOG 记录肯定要消耗一些代价,但和写整个修改过的页面相比,这算不了什么。我们确定我们可以获得更大的好处,即系统故障容忍度。
9.1.3 全页写(Full-Page Writes)
假设存储中 TABLE_A 的页面数据已损坏,因为在bgwriter进程一直在写入脏页时操作系统出现故障。由于无法在损坏的页面上重放 XLOG 记录,我们需要一个附加功能。
PostgreSQL 支持一种称为全页写入(full-page writes)的特性来处理此类故障。如果启用,PostgreSQL 在每个检查点后的每个页面的首次修改时将 header-data 和整个页面组成的一对值作为XLOG记录写入;默认启用。在PostgreSQL 中,这种包含整个页面的XLOG记录称为备份块(backup block) (或整页镜像(full-page image))。
让我们再次描述元组的插入,但启用了全页写入。请参见图 9.4 和以下说明。
Fig. 9.4. Full page writes.
(1) checkpointer 启动一个检查点进程。
(2) 在插入第一条 INSERT 语句时,虽然 PostgreSQL 的操作方式与上一小节几乎相同,但这条 XLOG 记录是该页面的备份块(即它包含整个页面),因为这是在最新的检查点之后第一次写这个页面。
(3) 当这个事务提交时,PostgreSQL 以与上一小节相同的方式运行。
(4) 在第二个 INSERT 语句的插入中,PostgreSQL 的操作方式与上一小节相同,因为该 XLOG 记录不是备份块。
(5) 当该语句的事务提交时,PostgreSQL 的操作方式与上一小节相同。
(6) 为了证明全页写入的有效性,这里我们考虑在bgwriter将其写入HDD硬盘时,由于操作系统故障而导致存储上的 TABLE_A 页面损坏的情况。
重新启动 PostgreSQL 服务器以修复损坏的集簇。请参见图 9.5 和以下说明。
Fig. 9.5. Database recovery with backup block.
(1) PostgreSQL 读取第一条 INSERT 语句的 XLOG 记录,并将损坏的 TABLE_A 的页面从数据库集群加载到共享缓冲池中。在这个例子中,XLOG 记录是一个备份块,因为根据全页写入的写入规则,每页的第一个 XLOG 记录始终是它的备份块。
(2) 当XLOG记录是它的备份块时,应用另一个重放规则:记录的数据部分(即页面本身)将被覆盖到页面上,而不管它们两个的XLOG LSN的值,并且将页面的LSN更新到 XLOG 记录的 LSN。
在此例中,PostgreSQL 将XLOG 记录的数据部分覆盖到损坏的页面,并将TABLE_A的LSN更新为LSN_1。通过这种方式,损坏的页面由其备份块恢复。
(3) 由于第二条 XLOG 记录是非备份块,PostgreSQL 的操作方式与上一小节中的方式相同。
这样,即使因进程或操作系统宕机而发生一些数据写入错误,PostgreSQL 也可以恢复数据库。
WAL、备份和复制
如上所述,WAL 可以防止由于进程或操作系统宕机而导致的数据丢失。但是,如果发生文件系统或介质故障,数据将会丢失。为了应对此类故障,PostgreSQL 提供了在线备份和复制功能。
如果定期进行在线备份,即使发生介质故障,也可以从最近的备份中恢复数据库。但是,请注意,在进行最后一次备份后所做的更改无法恢复。要将所有更改实时存储到另一个存储或主机,请使用同步复制功能。有关详细信息,请分别参见第 10 章和第 11 章。
9.2 事务日志和 WAL 段文件
从逻辑上讲,PostgreSQL 将 XLOG 记录写入事务日志,这是一个 8 字节长(16 ExaByte)的虚拟文件。
由于事务日志的容量实际上是无限的,因此可以说 8 字节的地址空间已经足够大,我们不可能处理 8 字节长度的文件。因此,PostgreSQL 中的事务日志默认分为 16 Mbyte的文件,每个文件称为 WAL segment。参见图 9.6。
WAL segment 文件大小
在版本 11 或更高版本中,通过 initdb 命令创建 PostgreSQL 集簇时,可以使用 --wal-segsize 选项配置 WAL segment 文件的大小。
Fig. 9.6. Transaction log and WAL segment files
WAL(段)文件名是16进制的24位数字,命名规则如下:
'WAL segment file name' = timelineId + $ (uint32)\frac{LSN-1}{16M \times 256} + (uint32)\frac{LSN-1}{16M} $%256
timelineId
PostgreSQL的WAL包含timelineId(4字节无符号整数)的概念,用于第10章描述的Point-in-Time Recovery(PITR)。但是在本章中timelineId固定为0x00000001,因为在以下描述中不需要此概念。
第一个 WAL 段文件是 000000010000000000000001。如果第一个已被写入 XLOG 记录填满,则将提供第二个 000000010000000000000002。后继文件按升序顺序使用,0000000100000000000000FF 填满后,将提供下一个 000000010000000100000000。这样,每当最后 2 位结转时,中间 8 位数字就会增加 1。
类似地,在 0000000100000001000000FF 被填满后,将提供 000000010000000200000000,以此类推。
pg_xlogfile_name / pg_walfile_name
使用内置函数 pg_xlogfile_name(9.6 或更早版本)或 pg_walfile_name(10 或更高版本),我们可以找到包含指定 LSN 的 WAL 段文件名。一个例子如下所示:
testdb=# SELECT pg_xlogfile_name('1/00002D3E'); -- # In version 10 or later, "SELECT pg_walfile_name('1/00002D3E');" pg_xlogfile_name -------------------------- 000000010000000100000000 (1 row)
9.3 WAL段内部布局
默认情况下,WAL段是一个16MB的文件,它的内部分成一个个8192字节(8KB)大小的页面。第一个页面包含了由 XLogLongPageHeaderData 结构定义的页头数据,而其它所有页面的顶部包含由XLogPageHeaderData 结构定义的页面信息。跟在页头之后,XLOG 记录按照降序从头开始写入每一个页面。如图9.7
Fig. 9.7. Internal layout of a WAL segment file.
XLogLongPageHeaderData 和 XLogPageHeaderData 结构都在 src/include/access/xlog_internal.h 中定义。由于在以下描述中不需要这两种结构,因此就省略了介绍。
9.4 XLOG 记录的内部布局
XLOG 记录包括通用头部部分和每个相关的数据部分内容。第一个小节描述了头部结构;其余两个小节分别介绍了 9.4 或更早版本以及 9.5 版本中数据部分的结构。 (数据格式在 9.5 版中已更改。)
9.4.1 XLOG记录的头部部分
所有 XLOG 记录都有一个由 XLogRecord 结构体定义的通用头部部分。此处,9.4 及更早版本的结构如下所示,但在 9.5 版本中有所更改。
typedef struct XLogRecord
{
uint32 xl_tot_len; /* total len of entire record */
TransactionId xl_xid; /* xact id */
uint32 xl_len; /* total len of rmgr data */
uint8 xl_info; /* flag bits, see below */
RmgrId xl_rmid; /* resource manager for this record */
/* 2 bytes of padding here, initialize to zero */
XLogRecPtr xl_prev; /* ptr to previous record in log */
pg_crc32 xl_crc; /* CRC for this record */
} XLogRecord;
除了两个变量之外,大部分变量都非常明显,无需描述。
xl_rmid 和 xl_info 都是与资源管理器相关的变量,它们是与 WAL 特性相关操作的集合,例如 XLOG 记录的写入和重放。资源管理器的数量随着每个 PostgreSQL 版本的增加而增加,版本 10 包含以下它们:
Operation(操作) | Resource manager(资源管理器) |
---|---|
Heap tuple operations | RM_HEAP, RM_HEAP2 |
Index operations | RM_BTREE, RM_HASH, RM_GIN, RM_GIST, RM_SPGIST, RM_BRIN |
Sequence operations | RM_SEQ |
Transaction operations | RM_XACT, RM_MULTIXACT, RM_CLOG, RM_XLOG, RM_COMMIT_TS |
Tablespace operations | RM_SMGR, RM_DBASE, RM_TBLSPC, RM_RELMAP |
replication and hot standby operations | RM_STANDBY, RM_REPLORIGIN, RM_GENERIC_ID, RM_LOGICALMSG_ID |
以下是资源管理器如何工作的一些代表性示例:
-
如果发出 INSERT 语句,其XLOG记录的头部变量
xl_rmid
和xl_info
分别设置为'RM_HEAP' 和 'XLOG_HEAP_INSERT'。恢复数据库时,根据xl_info
变量选择的'RM_HEAP'的函数heap_xlog_insert()
重放这条XLOG记录 -
虽说UPDATE 语句也类似,但XLOG记录的头部变量
xl_info
设置为'XLOG_HEAP_UPDATE',在数据库恢复时,通过 'RM_HEAP'的函数heap_xlog_update()
重放其XLOG记录 -
当事务提交时,其XLOG记录的头部变量
xl_rmid
和xl_info
分别设置为'RM_XACT' 和 'XLOG_XACT_COMMIT'。恢复数据库集簇时,调用函数xact_redo_commit()
重放这条XLOG记录
在9.5 或更高版本中,从XLogRecord结构中删除了一个变量 (xl_len),以改进XLOG的记录格式,将其大小减少了几个字节。
XLogRecord 结构在9.4或更早版本中由 src/include/access/xlog.h 文件定义,而在9.5或更高版本由 src/include/access/xlogrecord.h 定义。
heap_xlog_insert 和 heap_xlog_update 函数都在 src/backend/access/heap/heapam.c 文件中定义;而xact_redo_commit 函数在 src/backend/access/transam/xact.c文件定义
9.4.2 XLOG 记录中数据部分(9.4或更早版本)
XLOG记录的数据部分分成备份块(整个页面)和非备份块(操作不同的数据)。
Fig. 9.8. Examples of XLOG records (version 9.4 or earlier).
下面使用一些具体示例描述XLOG记录的内部布局。
9.4.2.1 备份块(backup block)
备份块如图 9.8(a) 所示。它由两个数据结构和一个数据对象组成,如下图所示:
-
XLogRecord 结构(记录头部部分)
-
BkpBlock 结构
typedef struct BkpBlock @ include/access/xlog_internal.h { RelFileNode node; /* relation containing block */ ForkNumber fork; /* fork within the relation */ BlockNumber block; /* block number */ uint16 hole_offset; /* number of bytes before "hole" */ uint16 hole_length; /* number of bytes in "hole" */ /* ACTUAL BLOCK DATA FOLLOWS AT END OF STRUCT */ } BkpBlock;
BkpBlock 包含用于在数据库集簇中标识该页面的变量(即包含该页面的关系的 relfilenode 和fork编号,以及该页面的块号),以及该页面可用空间的起始位置和长度。
-
整个页面,除了其可用空间
9.4.2.2 非备份块(Non-Backup Block)
在非备份块中,数据部分的布局因每个操作而异。这里以 INSERT 语句的 XLOG 记录为例进行说明。请看图9.8(b)。在此例中,INSERT 语句的 XLOG 记录由两个数据结构和一个数据对象组成,如下所示:
-
XLogRecord 结构(记录头部部分)
-
xl_heap_insert 结构
typedef struct BlockIdData { uint16 bi_hi; uint16 bi_lo; } BlockIdData; typedef uint16 OffsetNumber; typedef struct ItemPointerData { BlockIdData ip_blkid; OffsetNumber ip_posid; } typedef struct RelFileNode { Oid spcNode; /* tablespace */ Oid dbNode; /* database */ Oid relNode; /* relation */ } RelFileNode; typedef struct xl_heaptid { RelFileNode node; ItemPointerData tid; /* changed tuple id */ } xl_heaptid; typedef struct xl_heap_insert { xl_heaptid target; /* inserted tuple id */ bool all_visible_cleared; /* PD_ALL_VISIBLE was cleared */ } xl_heap_insert;
xl_heap_insert 结构包含用于标识数据库集簇中插入元组的变量(即包含该元组的表的 relfilenode 和该元组的 tid),以及该元组的可见性标志。
-
插入的元组--准确地说,从元组中删除几个字节
xl_heap_header 结构的源码注释中描述了从插入元组中删除几个字节的原因:
我们不会在 WAL 中存储插入或更新元组的整个固定部分(HeapTupleHeaderData);我们可以通过重构WAL记录中其它地方可用的字段来节省一些字节,也许不需要重构。
这里再举一个例子。参见图9.8(c) 。Checkpoint 记录中的XLOG记录相当简单;它由两个数据结构体组成,如下所示:
- XLogRecord 结构体(头部部分)
- 包含检查点信息的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。
Fig. 9.9. Common XLOG record format.
头部部分包含0或多个 XLogRecordBlockHeaders 结构体和0或1个 XLogRecordDataHeaderShort) (或XLogRecordDataHeaderLong)结构体;它必须至少包含其中之一。当该记录要存储整页镜像(即备份块)时,它包含了 XLogRecordBlockHeader ,XLogRecordBlockImageHeader 结构体;如果启用了块压缩特性,则还包括 XLogRecordBlockCompressHeader 结构体。
数据部分由零个或多个块数据和零个或一个主数据组成,分别对应XLogRecordBlockHeader(s)和XLogRecordDataHeader 结构体。
WAL 压缩
在9.5或更高版本,可以通过设置参数 wal_compression = enable 使用 LZ 压缩算法压缩 XLOG 记录中的整页镜像。在这种情况下,将添加 XLogRecordBlockCompressHeader 结构体。
此功能有两个优点和一个缺点。优点是减少写入记录的 I/O 代价和抑制 WAL 段文件的消耗。缺点是要消耗大量的 CPU 资源进行压缩。
Fig. 9.10. Examples of XLOG records (version 9.5 or later).
下面显示了一些具体示例,如上一小节所示。
9.4.3.1 备份块(Backup Block)
INSERT 语句创建的备份块如图 9.10(a) 所示。它由四个数据结构和一个数据对象组成,如下所示:
- XLogRecord 结构体(记录头部部分)
- 包含一个LogRecordBlockImageHeader结构的XLogRecordBlockHeader 结构体
- XLogRecordDataHeaderShort 结构体
- 一个备份块(块数据)
- xl_heap_insert 结构体(主数据)
XLogRecordBlockHeader 包含用于标识数据库集簇中的块的变量(relfilenode,fork编号,块编号);XLogRecordImageHeader 包含了此块的长度和偏移量。(这两个头部结构一起共同存储了沿用至9.4版本中BkpBlock 结构的相同数据)
XLogRecordDataHeaderShort 存储的是该记录中主数据xl_heap_insert 结构的长度。(参见以下内容)
包含整页镜像的XLOG记录的主数据已不使用了,除非在某些特定情况下(如:逻辑解码或投机插入场景)
9.4.3.2 非备份块
下面对INSERT语句创建的非备份块记录进行说明(参见图9.10(b))。它由四个数据结构和一个数据对象组成,如下所示:
-
XLogRecord 结构体(记录头部部分)
-
XLogRecordBlockHeader 结构体
XLogRecordBlockHeader 结构包含了三个值(relfilenode、fork 编号和块编号)用于指定插入元组的块,以及插入元组的数据部分的长度。
-
XLogRecordDataHeaderShort 结构体
XLogRecordDataHeaderShort 包含新的 xl_heap_insert 结构的长度,它是这条记录的主要数据。
-
一个插入元组(确切地说,是一个 xl_heap_header 结构和一个插入的数据整体)
-
xl_heap_insert 结构体(主数据)
typedef struct xl_heap_insert { OffsetNumber offnum; /* inserted tuple's offset */ uint8 flags; /* xl_heap_header & TUPLE DATA in backup block 0 */ } xl_heap_insert;
新的 xl_heap_insert 仅包含两个值:块内此元组的偏移量和可见性标志;它变得非常简单,因为 XLogRecordBlockHeader 存储了旧数据中包含的大部分数据。
作为最后一个例子,检查点记录如图 9.10(c) 所示。它由三个数据结构组成,如下所示:
- XLogRecord 结构体(记录头部部分)
- 包含主要数据长度的 XLogRecordDataHeaderShort 结构体
- CheckPoint 结构(主要数据)
虽然新格式对我们来说有点复杂,但对于资源管理器的解析器来说是个非常好的设计,而且许多类型的 XLOG 记录的大小通常比以前的要小。主要结构的大小如图 9.8 和 9.10 所示,因此您可以计算这些记录的大小并相互比较。 (新检查点的大小大于之前的,但包含更多变量。)
9.5 XLOG 记录的写入
完成了热身练习,现在我们准备认识 XLOG 记录的写入。因此,我将在本节中尽可能准确地讲解它。
首先,发出以下语句来探索 PostgreSQL 内部过程:
testdb=# INSERT INTO tbl VALUES ('A');
通过发出上述语句时,就会调用内部函数 exec_simple_query(); 该函数的伪代码如下所示:
exec_simple_query() @postgres.c
(1) ExtendCLOG() @clog.c /* Write the state of this transaction
* "IN_PROGRESS" to the CLOG.
*/
(2) heap_insert()@heapam.c /* Insert a tuple, creates a XLOG record,
* and invoke the function XLogInsert.
*/
(3) XLogInsert() @xlog.c (9.5 or later, xloginsert.c)
/* Write the XLOG record of the inserted tuple
* to the WAL buffer, and update page's pd_lsn.
*/
(4) finish_xact_command() @postgres.c /* Invoke commit action.*/
XLogInsert() @xlog.c (9.5 or later, xloginsert.c)
/* Write a XLOG record of this commit action
* to the WAL buffer.
*/
(5) XLogWrite() @xlog.c /* Write and flush all XLOG records on
* the WAL buffer to WAL segment.
*/
(6) TransactionIdCommitTree() @transam.c /* Change the state of this transaction
* from "IN_PROGRESS" to "COMMITTED" on the CLOG.
*/
在下面的段落中,将解释每一行伪代码,以便了解XLOG记录的写入。参见图9.11和9.12。
(1) ExtendCLOG() 函数将此事务 'IN_PROGRESS' 的状态写入(内存中)CLOG
(2) heap_insert() 函数将堆元组插入到共享缓冲池的目标页中,创建该页的XLOG记录,并调用XLogInsert()函数
(3) XLogInsert() 函数将 heap_insert() 函数创建的XLOG记录写入到WAL缓冲区的LSN_1中,然后将修改页面的pd_lsn从LSN_0更新为LSN_1
(4) 调用 finish_xact_command() 函数用于提交该事务,创建提交操作的XLOG记录,然后XLogInsert() 函数将此记录写入到 WAL 缓冲区的 LSN_2 处。
Fig. 9.11. Write-sequence of XLOG records.
这些 XLOG 记录是 9.4 版本的格式
(5) XLogWrite() 函数将WAL缓冲区上所有的XLOG记录写入并刷新到WAL段文件
如果 wal_sync_method 参数设置为 'open_sync' 或 'open_datasync',则记录是同步写入的,因为该函数(XLogWrite())使用带有指定标志 O_SYNC 或 O_DSYNC 的系统调用open()函数写入所有记录。如果参数设置为'fsync', 'fsync_writethrough' 或 'fdatasync' ,则执行相应的系统调用函数--fsync(),带F_FULLFSYNC选项的fcntl() 或 fdatasync()。在任何情况下,都会确保所有 XLOG 记录写入到存储。
(6) TransactionIdCommitTree() 在 CLOG 上将此事务的状态从IN_PROGRESS' 修改为 'COMMITTED'
Fig. 9.12. Write-sequence of XLOG records (continued from Fig. 9.11).
在上面的例子中,提交操作会导致XLOG记录写入到WAL段文件,但是,当发生以下任一一种情况时,也会导致这种写入:
- 一个正在运行的事务已提交或已中止
- WAL缓冲区已被元组填满(WAL缓冲区大小由wal_buffers参数控制)
- WAL写进程定期写入(参见下一节)
如果发生上述情况之一,则 WAL 缓冲区上的所有 WAL 记录都将写入 WAL 段文件,无论它们的事务是否已提交。
DML(数据操作语言)操作写入 XLOG 记录是理所当然的,但非 DML 操作也是如此(写XLOG 记录)。就如上所述,提交操作会写入包含已提交事务 ID 的 XLOG 记录。另一个示例可能是检查点操作,将包含该检查点一般信息写入到XLOG 记录。此外,SELECT语句在特殊情况下也会创建XLOG记录,尽管它通常不会创建它们。例如,如果在SELECT语句执行过程中,HOT删除页中不需要的元组和对页中必要的元组进行碎片整理,那么,修改页中的XLOG记录将会写入到WAL缓冲区中。
9.6 WAL写进程
WAL writer 是一个后台进程,用于定期检查 WAL 缓冲区并将所有未写入的 XLOG 记录写入 WAL 段。此进程的目的是避免 XLOG 记录的突发写入。如果没有开启这个进程,那么在一次性地提交大量数据时,XLOG 记录的写入可能会遇到瓶颈。
WAL writer 默认开启且无法禁用。wal_writer_delay 配置参数用于控制检查间隔周期,默认值为200毫秒。
9.7 PostgreSQL 中的检查点进程
在 PostgreSQL 中,检查点(后台)进程执行检查点;当发生以下情况之一时,触发检查点:
-
从上一个检查点开始已经过了checkpoint_timeout设置的间隔时间(默认间隔时间300秒(5分钟))
-
在9.4或更早版本,自上一个检查点以来已消耗了checkpoint_segments参数设置的WAL文件数量(默认值是3)
-
在 9.5 或更高版本中,pg_xlog(在 10 或更高版本中为 pg_wal)中的 WAL 段文件的总大小已超过参数 max_wal_size 的值(默认值为 1GB(64 个文件))
-
以smart 或 fast 模式关闭PostgreSQL服务
当超级用户手动发出 CHECKPOINT 命令时,也会执行此操作。
在 9.1 或更早的版本中,如 8.6 节所述,bgwriter进程同时进行检查点和脏页写入。
在以下小节中,将描述检查点的概要和保存当前检查点元数据的 pg_control 文件。
9.7.1 检查点进程概要
检查点处理有两方面:数据库恢复的准备以及清理共享缓冲区上的脏页。在本小节中,将重点介绍其内部处理过程。见图9.13和下面的描述。
Fig. 9.13. Internal processing of PostgreSQL's checkpoint.
(1) 检查点进程启动之后,重做(REDO)点被存储到内存中。重做(REDO)点是最近一次检查点启动时写入XLOG记录的位置,也是数据库恢复的起点。
(2) 此检查点的XLOG 记录(即检查点记录)写入到WAL缓冲区。这个记录的数据部分由 CheckPoint 结构定义,它包含了几个变量,如步骤 (1) 中存储的重做(REDO)点。
另外,从字面上说,写检查点记录的位置就是检查点。
(3) 共享内存中的所有数据(如:clog 的内容等)都被刷新到存储(磁盘)
(4) 共享缓冲池上的所有脏页都被逐渐写入并刷新到存储中
(5) pg_control 文件已被更新。该文件包含基本信息,例如:检查点记录的写入位置(也称为检查点位置)。这个文件的详细内容稍后介绍。
从数据库恢复的角度总结上面的描述,检查点创建包含重做(REDO)点的检查点记录,并将检查点位置等存储到 pg_control 文件中。
因此,PostgreSQL 可以通过由 pg_control 文件提供的重做(REDO)点(从检查点记录中获得)开始重放 WAL 数据来恢复自身。
9.7.2 pg_control 文件
由于 pg_control 文件包含了检查点的基本信息,对于数据库恢复来说,它是必不可缺的。如果 pg_control 文件损坏或不可读时,恢复进程无法启动,从而无法获得起点。
尽管 pg_control 文件存储了 40 多个条目,而下一节中需要的三个条目如下所示:
-
State:最近一次检查点开始时数据库服务器的状态。共有七种状态:'start up' 是系统正在启动的状态; shut down' 是系统被shutdown命令正常关闭的状态;'in production' 是系统正在运行的状态;等等。
-
Latest checkpoint location:最新的检查点记录的LSN位置
-
Prior checkpoint location:前一个检查点记录的LSN位置。请注意,它在版本 11 中已弃用;详情如下所述。
在PG11中弃用先前检查点
PostgreSQL 11或更高版本将只存储包含最新检查点或更新的WAL段文件。在 pg_xlog(pg_wal) 子目录下保存 WAL 段文件中包含先前检查点的旧WAL段文件将不被存储,以减少磁盘空间。详见文档描述。
pg_control 文件存储在基目录(base-directory)下的全局子目录中。可以使用 pg_controldata 实用程序查看其内容。
postgres> pg_controldata /usr/local/pgsql/data
pg_control version number: 937
Catalog version number: 201405111
Database system identifier: 6035535450242021944
Database cluster state: in production
pg_control last modified: Sat Mar 26 15:16:38 2022
Latest checkpoint location: 0/C000F48
Prior checkpoint location: 0/C000E70
... snip ...
9.8 PostgreSQL 中的数据库恢复
PostgreSQL 实现了基于重做日志的恢复特性。如果数据库服务器崩溃,PostgreSQL 通过从重做(REDO)点开始按顺序重放 WAL 段文件中的 XLOG 记录来恢复数据库集簇。
到本节为止,我们已经多次讨论过数据库恢复,所以我将描述关于恢复的两件还没介绍过的事。
首先是PostgreSQL如何开启恢复。当PostgreSQL启动时,先读取pg_control文件。然后是从哪个点开始进行恢复的细节。见图9.14和下面的描述。
Fig. 9.14. Details of the recovery process.
(1) PostgreSQL 启动时读取 pg_control 文件的所有项。如果(state)状态列的值为 'in production',则PostgreSQL 将进入恢复模式,因为它意味着数据库没有正常停止;如果是'shut down'状态,则进入正常启动模式
(2) PostgreSQL 从相应的WAL段文件中读取已写入pg_control文件的最新检查点位置的XLOG记录,并从该记录中获取重做(REDO)点。如果最新的检查点记录无效,PostgreSQL 会读取它的前一个记录。如果两条记录都不可读,则放弃自行恢复。(请注意,PostgreSQL 11 中已不存储先前检查点)
(3) 适当的资源管理器从重做(REDO)点按顺序读取和重放 XLOG 记录,直到它们到达最新 WAL 段的最后一个点。在重放XLOG记录的过程中,如果它是一个备份块,就用它覆盖对应的表页而不管LSN号。否则,只有当该记录的 LSN 大于相应页面的 pd_lsn 时,才会重放(非备份块的)XLOG 记录。
第二点是关于LSN的比较:为什么要比较非备份块的 LSN 和对应页面的 pd_lsn。与前面的示例不同,这里将使用一个特定示例来强调需要在两个 LSN 之间进行比较的原因。见图 9.15 和 9.16。(请注意,为了简化描述,省略了 WAL 缓冲区。)
Fig. 9.15. Insertion operations during the background writer working.
(1) 在TABLE_A中插入一个元组,并在LSN_1位置写入一条XLOG记录
(2) bgwriter进程将 TABLE_A 的页面写入存储。此时,该页面的 pd_lsn 为 LSN_1
(3) 在TABLE_A中插入一个新的元组,并在LSN_2处写入一条XLOG记录,修改的页面尚未写入存储
与概览中的示例不同,在此场景中,TABLE_A 的页面已写入存储。
使用 immediate-mode 关闭,然后启动。
Fig. 9.16. Database recovery.
(1) PostgreSQL 加载第一条 XLOG 记录和 TABLE_A 的页面,但不需要重放,因为这条记录的 LSN 不大于 TABLE_A 的 LSN(两个值都是 LSN_1)。其实一目了然,无需重放。
(2) 接着,PostgreSQL 重放第二条 XLOG 记录,因为这条记录的 LSN (LSN_2) 大于当前 TABLE_A 的 LSN (LSN_1)。
从这个例子可以看出,如果非备份块的重放顺序不正确或者非备份块被重放一次以上,数据库集簇将会出现不一致。简而言之,非备份块的重做(重放)操作不是幂等的。因此,为了保持正确的重放顺序,当且仅当它的 LSN 大于相应页面的 pd_lsn 时,才应重放非备份块记录。
另一方面,由于备份块的重做操作是幂等的,备份块可以被重放任意次数,而不管其LSN。
9.9 WAL 段文件管理
PostgreSQL 将XLOG记录写入到pg_xlog子目录(在版本 10 或更高版本中,pg_wal 子目录)下的一个WAL段文件,如果旧文件已填满时,就切换到新文件。WAL文件的数量将根据几个配置参数以及服务的活动而变化。此外,它们的管理策略在9.5版本中得到了改进。
在以下小节中,将描述 WAL 段文件的切换和管理。
9.9.1 切换 WAL 段文件
在发生以下任意一种场景时切换WAL段文件:
- WAL段已被填满。
- pg_switch_xlog 函数已被发起
- archive_mode 已开启且已超过参数 archive_timeout 设置的时间
切换文件通常会被回收(重命名和重用),以便将来使用。但如果不再需要,就可能会在以后删除。
9.9.2 在9.5或更高版本中的WAL段管理
每当检查点开始时,PostgreSQL 都会估算并准备下一个检查点周期所需的 WAL 段文件的数量。此类估算是针对先前检查点周期中消耗的文件数进行的。它们从包含先前重做(REDO)点的段开始计算,值介于 min_wal_size(默认情况下,80 MB,即 5 个文件)和 max_wal_size(1 GB,即 64 个文件)之间。如果检查点启动,必要的文件将被保留或回收,而不必要的文件将被删除。
一个具体的例子如图 9.17 所示。假设检查点开启前有6个文件,WAL_3 包含先前重做(REDO)点(在10或更早版本;11或更高版本为重做(REDO)点),PostgreSQL 预估需要五个文件。在这种情况下,WAL_1将被重命名为WAL_7以便回收重用,而WAL_2将被删除。
可以删除比包含先前重做(REDO)点更旧的文件,因为从第9.8节中介绍的恢复机制中清晰地知道它们永远不会被使用。
Fig. 9.17. Recycling and removing WAL segment files at a checkpoint.
如果由于 WAL 活动激增而需要更多文件,在 WAL 文件的总大小小于 max_wal_size 时将创建新文件。如图 9.18 中,如果 WAL_7 已被填满,则新创建 WAL_8。
Fig. 9.18. Creating WAL segment file.
WAL 文件的数量根据服务器活动而自适应地改变。如果 WAL 数据写入量不断增加,WAL 段文件的预估数量以及 WAL 文件的总大小也会逐渐增加。在相反的情况下(即 WAL 数据写入量减少),它们随之减少。
如果 WAL 文件的总大小超过 max_wal_size,将启动一个检查点。图 9.19 说明了这种情况。通过执行检查点,将创建一个新的重做(REDO)点,最后一个重做点就成为前一个;然后回收不必要的旧文件。这样,PostgreSQL 将始终只保留数据库恢复所需的 WAL 段文件。
Fig. 9.19. Checkpointing and recycling WAL segment files.
wal_keep_segments 配置参数和复制槽特性也会影响 WAL 段文件的数量。
9.9.3 在9.4或更早版本中的WAL段管理
WAL 段文件的数量主要由以下三个参数控制:checkpoint_segments、checkpoint_completion_target 和 wal_keep_segments。它的数量通常超过$((2 + checkpoint_completion_target) \times checkpoint_segments + 1)$ 或 $(checkpoint_segments + wal_keep_segments + 1)$ 个文件。这个数字可能会临时达到 $(3 \times checkpoint_segments + 1)$ 个文件,具体取决于服务器的活动情况。复制槽也会影响它们的数量。
如9.7节所述,当checkpoint_segments 指定的文件数量已被消耗时触发检查点进程。因此,在WAL文件中总要保证有两个或更多个重做(REDO)点,因为文件的数量总是大于$2 \times checkpoint_segments$。如果通过超时触发检查点进程,也是如此。因此,PostgreSQL 将始终保存恢复所需的足够多的 WAL 段文件(有时超过会所需)。
在 9.4 或更早的版本中,checkpoint_segments 参数总是令人头疼。如果设置得小,会频繁触发检查点进程,导致性能下降,而如果设置大,就需要巨大的磁盘空间存储WAL文件,其中一些文件并不总是必需的。
在 9.5 版本中,WAL 文件的管理策略得到了改进,并已废弃checkpoint_segments 参数。因此,上述权衡问题也得到了解决。
9.10 连续归档和归档日志
连续归档是在 WAL 段切换时将 WAL 段文件复制到归档区的功能,由archiver (后台)进程执行。复制的文件称为存档日志(archive log)。此功能通常用于热物理备份和第 10 章中描述的 PITR(Point-in-Time Recovery)
归档区的路径由配置参数archive_command控制。例如,使用以下参数,WAL 段文件在每次每个段切换时复制到目录'/home/postgres/archives/':
archive_command = 'cp %p /home/postgres/archives/%f'
其中,占位符 %p 是复制的 WAL 段, %f 是归档日志。
Fig. 9.20. Continuous archiving.
当切换 WAL 段文件 WAL_7 时,该文件作为 Archive log 7 复制到归档区。
参数archive_command 可以设置任何Unix 命令和工具,因此您可以通过设置scp 命令或任何文件备份工具而不是普通的copy 命令将归档日志传输到其他主机。
PostgreSQL 不会清理创建的归档日志,因此在使用此功能时应该正确管理归档日志。如果您什么都不做,归档日志的数量会持续增加。
pg_archivecleanup 实用程序是一个有用的管理归档日志的工具。
此外,unix 命令 find 可用于删除归档日志。以下命令删除三天前创建的归档日志。
$ find /home/postgres/archives -mtime +3d -exec rm -f {} \;
附录
相关源码片段
XLogRecordBlockHeader
typedef struct XLogRecordBlockHeader
{
uint8 id; /* block reference ID */
uint8 fork_flags; /* fork within the relation, and flags */
uint16 data_length; /* number of payload bytes (not including page
* image) */
/* If BKPBLOCK_HAS_IMAGE, an XLogRecordBlockImageHeader struct follows */
/* If BKPBLOCK_SAME_REL is not set, a RelFileNode follows */
/* BlockNumber follows */
} XLogRecordBlockHeader;
/*
* The fork number fits in the lower 4 bits in the fork_flags field. The upper
* bits are used for flags.
*/
#define BKPBLOCK_FORK_MASK 0x0F
#define BKPBLOCK_FLAG_MASK 0xF0
#define BKPBLOCK_HAS_IMAGE 0x10 /* block data is an XLogRecordBlockImage */
#define BKPBLOCK_HAS_DATA 0x20
#define BKPBLOCK_WILL_INIT 0x40 /* redo will re-init the page */
#define BKPBLOCK_SAME_REL 0x80 /* RelFileNode omitted, same as previous */
XLogRecordDataHeaderShort
typedef struct XLogRecordDataHeaderShort
{
uint8 id; /* XLR_BLOCK_ID_DATA_SHORT */
uint8 data_length; /* number of payload bytes */
} XLogRecordDataHeaderShort;
#define SizeOfXLogRecordDataHeaderShort (sizeof(uint8) * 2)
typedef struct XLogRecordDataHeaderLong
{
uint8 id; /* XLR_BLOCK_ID_DATA_LONG */
/* followed by uint32 data_length, unaligned */
} XLogRecordDataHeaderLong;
#define SizeOfXLogRecordDataHeaderLong (sizeof(uint8) + sizeof(uint32))
XLogRecordBlockImageHeader
typedef struct XLogRecordBlockImageHeader
{
uint16 length; /* number of page image bytes */
uint16 hole_offset; /* number of bytes before "hole" */
uint8 bimg_info; /* flag bits, see below */
/*
* If BKPIMAGE_HAS_HOLE and BKPIMAGE_IS_COMPRESSED, an
* XLogRecordBlockCompressHeader struct follows.
*/
} XLogRecordBlockImageHeader;
/* Information stored in bimg_info */
#define BKPIMAGE_HAS_HOLE 0x01 /* page image has "hole" */
#define BKPIMAGE_IS_COMPRESSED 0x02 /* page image is compressed */
XLogRecordBlockCompressHeader
typedef struct XLogRecordBlockCompressHeader
{
uint16 hole_length; /* number of bytes in "hole" */
} XLogRecordBlockCompressHeader;
CheckPoint
typedef struct CheckPoint
{
XLogRecPtr redo; /* next RecPtr available when we began to
* create CheckPoint (i.e. REDO start point) */
TimeLineID ThisTimeLineID; /* current TLI */
TimeLineID PrevTimeLineID; /* previous TLI, if this record begins a new
* timeline (equals ThisTimeLineID otherwise) */
bool fullPageWrites; /* current full_page_writes */
uint32 nextXidEpoch; /* higher-order bits of nextXid */
TransactionId nextXid; /* next free XID */
Oid nextOid; /* next free OID */
MultiXactId nextMulti; /* next free MultiXactId */
MultiXactOffset nextMultiOffset;/* next free MultiXact offset */
TransactionId oldestXid; /* cluster-wide minimum datfrozenxid */
Oid oldestXidDB; /* database with minimum datfrozenxid */
MultiXactId oldestMulti; /* cluster-wide minimum datminmxid */
Oid oldestMultiDB; /* database with minimum datminmxid */
pg_time_t time; /* time stamp of checkpoint */
/*
* Oldest XID still running. This is only needed to initialize hot standby
* mode from an online checkpoint, so we only bother calculating this for
* online checkpoints and only when wal_level is hot_standby. Otherwise
* it's set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
} CheckPoint;