事务日志是数据库的重要组成部分,存储了数据库系统中所有更改和操作的历史,以确保数据库不会因为故障(例如掉电或其他导致服务器崩溃的故障)而丢失数据。在PostgreSQL(以下简称PG)中,事务日志文件称为Write Ahead Log(以下简称WAL)。
使用WAL不仅可以保证数据库事务操作的持久性,也可以显著地减少写磁盘的次数,因为只需要把日志文件刷新到磁盘就可以保证事务被提交,而不需要把事务改动过的每一个数据文件都刷新到磁盘。日志文件是连续写的,所以同步log的花销远小于刷新数据页的花销。特别是服务器要处理涉及数据存储不同部分的大量小事务时更是这样。
WAL还使得在线备份和时间点恢复成为可能。通过归档WAL数据,我们可以恢复到WAL数据覆盖范围内的任何时间点:只需install一份数据库的物理备份,并恢复WAL日志到所需时间即可。更重要的是,这个物理备份并不必须是一个数据库状态的瞬时快照。
WAL机制实际是在这个写数据的过程中加入了对应的写wal log的过程,步骤一样是先到Buffer,再刷新到Disk。
如下图所示:
图1:change 发生时
图2:commit及checkpoint发生时
为方便理解下文内容,先介绍以下WAL术语:
1、 REDO log
Redo log通常称为重做日志,在写入数据文件前,每个变更都会先行写入到Redo log中。其用途和意义在于存储数据库的所有修改历史,用于数据库故障恢复(Recovery)、增量备份(Incremental Backup)、PITR(Point In Time Recovery)和复制(Replication)。
2、 WAL segment file
为了便于管理,PG把事务日志文件划分为N个segment,每个segment称为WAL segment file,每个WAL segment file大小默认为16MB。
3、 XLOG Record
这是一个逻辑概念,可以理解为PG中的每一个变更都对应一条XLOG Record,这些XLOG Record存储在WAL segment file中。PG读取这些XLOG Record进行故障恢复/PITR等操作。
4、 WAL buffer
WAL缓冲区,不管是WAL segment file的header还是XLOG Record都会先行写入到WAL缓冲区中,在"合适的时候"再通过WAL writer写入到WAL segment file中。
5、 LSN
LSN即日志序列号Log Sequence Number。表示XLOG record记录写入到事务日志中位置。LSN的值为无符号64位整型(uint64)。在事务日志中,LSN单调递增且唯一。
6、 checkpointer
checkpointer是PG中的一个后台进程,该进程周期性地执行checkpoint。当执行checkpoint时,该进程会把包含checkpoint信息的XLOG Record写入到当前的WAL segment file中,该XLOG Record记录包含了最新Redo point的位置。
7、 checkpoint
检查点checkpoint由checkpointer进程执行,主要的处理流程如下:
(1) 获取Redo point,构造包含此Redo point检查点(详细请参考Checkpoint结构体)信息的XLOG Record并写入到WAL segment file中;
(2) 刷新Dirty Page到磁盘上;
(3) 更新Redo point等信息到pg_control文件中。
8、 REDO point
REDO point是PG启动恢复的起始点,是最后一次checkpoint启动时事务日志文件的末尾亦即写入Checkpoint XLOG Record时的位置(这里的位置可以理解为事务日志文件中偏移量)。
9、 pg_control
pg_control是磁盘上的物理文件,保存检查点的基本信息,在数据库恢复中使用,可通过命令pg_controldata查看该文件中的内容。
下文描述了在不同情况下,不使用WAL和使用WAL机制对数据库数据持续性和恢复的影响。为了简化描述,仅使用了只包含一页的表TABLE_A。
如第8 章所述,为了高效访问关系页面,每个 DBMS 都实现了共享缓冲池。
假设我们在不执行 WAL 功能的 PostgresQL 上将一些数据图块插入到TABLE_A中:
如上图所示:
因此,没有 WAL 的数据库容易受到系统故障的影响。
为了在不影响性能的情况下处理上述系统故障,PostgreSQL 支持 WAL。此子节中的一些关键字和关键概念,请参考前文。
PostgreSQL 将所有修改作为历史数据写入持久存储,以便为失败做好准备。在PG中,历史数据称为XLOG记录或WAL数据。
XLOG 记录通过插入、删除或提交操作等操作将更改写入内存WAL 缓冲区。当事务提交/中止时,它们会立即写入存储中的WAL segment文件。
XLOG 记录的LSN(日志序列号)表示其记录写在事务日志上的位置。XLOG 记录的 LSN 用作 XLOG 记录的特有ID。
顺便说一句,当我们考虑数据库系统如何恢复时,可能有一个问题:PG从什么位置开始恢复?答案是redo point:即在启动最新检查点时编写 XLOG 记录的位置。事实上,数据库恢复处理与检查点处理紧密相连,这两种处理都是不可分割的。
在此示例中,此 XLOG 记录是一组头数据和整个tuple。
以下说明显示了如何立即将数据库集群恢复到崩溃前状态。无需做任何特别的事情,因为 PostgreSQL 将通过重新启动自动进入恢复模式。PG将依次从REDO点在适当的WAL段文件中读取和重放XLOG记录。
PostgreSQL 可以通过按时间顺序重放 WAL 段文件中编写的 XLOG 记录来恢复自身。因此,PG的XLOG记录显然是REDO日志。
假设因为操作系统在编写脏页的过程中出现故障,导致TABLE_A存储上的页面数据损坏。由于 XLOG 记录无法在损坏的页面上重放,因此我们需要额外的操作。
PostgreSQL 支持一项称为整页写的功能,以处理此类故障。如果启用,PostgreSQL 将在每个检查点后每个页面的第一次更改中将一对头数据和整个页面作为 XLOG 记录写入。PG默认已启用整页写。在 PostgreSQL 中,包含整个页面的此类 XLOG 记录称为备份块(或整页图像)。
让我们再次描述一下启用了整页写的Tuple 插入,请参阅下图及以下描述:
步骤如下:
重新启动PG服务器以修复损坏的集群。请参阅下图以及以下描述:
事务日志存储了数据库系统中所有更改和操作的历史,随着数据库的运行,事务日志大小不断的增长,那么事务日志有大小限制吗?在PG中,答案是肯定的:大小有限制。
PG使用无符号64bit整型(uint64)作为事务日志文件的寻址空间,理论上,PG的事务日志空间最大为2^64Bytes(即16EB)。这个大小有多大呢?假设某个数据库比较繁忙,每天可以产生16TB的日志文件,那么要达到事务日志文件大小的上限需要的时间是1024*1024/365天≈2800年。也就是说,虽然大小有限制,但从现阶段来看已然足够了。
显然,对于16EB的文件,OS是无法高效管理的,为此,PG把事务日志文件划分为N个大小为16M(默认值)的WAL segment file,其总体结构如下图所示:
WAL segment file文件名称为24个字符,由3部分组成,每个部分是8个字符,每个字符是一个16进制值(即0~F)。其命名规则如下:
每一部分的解析如下(在WAL segment file文件大小为16MB的情况下):
第1部分是TimeLineID,取值范围是0x00000000 -> 0xFFFFFFFF
第2部分是逻辑文件ID,取值范围是0x00000000 -> 0xFFFFFFFF
第3部分是物理文件ID,取值范围是0x00000000 -> 0x000000FF
逻辑文件ID、物理文件ID和文件大小这三部分的组合,实现了64bit的寻找空间:
逻辑文件ID是32bit的uint32(unsigned int 32bit)
物理文件ID是8bit的unit8
16M的文件大小是24bit的unit24
三者共同组成unit64(32+8+24),达到最大64bit的文件寻址空间。
第一个WAL segment 文件名为000000010000000000000001,若该WAL segment写满,那么第二个WAL segment文件为000000010000000000000002。而若0000000100000000000000FF文件写满,那么下一个WAL segment 文件名为000000010000000100000000。
使用内置函数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)
WAL segment file内部划分为N个page(Block),每个page大小为8192 Bytes即8K,每个WAL segment file第1个page的header在PG源码中相应的数据结构是XLogLongPageHeaderData,后续其他page的header对应的数据结构是XLogPageHeaderData。在一个page中,page header之后是N个XLOG Record。如下图所示:
XLOG Record由两部分组成,第一部分是XLOG Record的头部信息,大小固定(24 Bytes),对应的结构体是XLogRecord;第二部分是XLOG Record data。
所有的XLOG记录都有一个由XLogRecord结构体定义的通用的头部。其结构如下:
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 记录。
在此只介绍pg9.5及以后版本的结构。
在版本 9.4 或更早的时候,没有 XLOG 记录的常见格式,因此每个资源管理器必须定义自己的格式。在这种情况下,维护源代码和实施与 WAL 相关的新功能变得越来越困难。为了处理这个问题,9.5版本引入了一种不依赖于资源管理器的常见结构化格式。
XLOG 记录的数据部分可分为两部分:记录头部分和数据部分。如下图所示:
头部分包含零个或多个 XLogRecordBlockHeaders和零或一个 XLogRecordDataHeaderShort(或 XLogRecordDataHeaderLong):它必须至少包含其中之一。当其记录存储整页(即备份块)时,XLogRecordBlockHeader包括XLogRecordBlockImageHeader,如果其块被压缩,还包括XLogRecordBlockCompressHeader。
数据部分由零或多个块数据和零或一个主数据组成,分别对应于XLogRecordBlockHeader和 XLogRecordDataHeader。
插入语句创建的备份块如上图(a) 显示。它由四个数据结构和一个数据对象组成,如下所示:
XLogRecordBlockHeader包含用于标识数据库集群中的块的变量(relfilenode,fork编号和块编号); XLogRecordImageHeader包含此块的长度和偏移号。
XLogRecordDataHeaderShort存储xl_heap_insert结构的长度,该结构是记录的主要数据。
接下来,INSERT语句创建的非备份块记录将描述如下(也参见上图(b))。它由四个数据结构和一个数据对象组成,如下所示:
XLogRecordBlockHeader包含三个值(relfilenode,fork编号和块编号),用于指定插入元组的块,以及插入元组的数据部分的长度。 XLogRecordDataHeaderShort包含新xl_heap_insert结构的长度,该结构是此记录的主数据。
新的xl_heap_insert只包含两个值:块内该元组的偏移量,以及可见性标志;它变得非常简单,因为XLogRecordBlockHeader存储了旧数据中包含的大部分数据。
最后一个例子,检查点记录如上图(c)所示。它由三个数据结构组成,如下所示:
我们完成了热身练习,现在我们已经准备好了解XLOG记录的写入。我将在本节中尽可能准确地解释它。
首先,执行以下语句来探索PostgreSQL内部:
testdb=# INSERT INTO tbl VALUES ('A');
通过发出上述语句,调用内部函数exec_simple_query()。
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记录的写入。
在上面的示例中,commit动作导致将XLOG记录写入WAL段,但是当发生以下任何一种情况时,会发生写入操作:
当然,DML操作会写XLOG记录,但非DML操作也是如此。如上所述,提交操作会写入包含已提交事务的id的XLOG记录。另一个例子是检查点会将该检查点的一般信息写入到XLOG记录。此外,SELECT语句在特殊情况下也会创建XLOG记录,但一般情况下不会创建XLOG。例如,如果在SELECT语句处理期间删除了不必要的元组,而且在页面中发生了由HOT(Heap Only Tuple)引发的碎片整理,则修改页面的XLOG记录将写入WAL缓冲区。
WAL写进程是一个后台进程,用于定期检查WAL缓冲区并将所有未写入的XLOG记录写入WAL段。此过程的目的是避免爆发性的写XLOG记录。如果此进程没有启用,则在提交大量数据时,写入XLOG记录可能会遇到瓶颈。
WAL写进程默认工作的,并且无法禁用。检查间隔设置为配置参数wal_writer_delay,默认值为200毫秒。
在PostgreSQL中,checkpointer(后台)进程执行检查点;当下列之一发生时,其进程开始:
当超级用户手动发出CHECKPOINT命令时,也会执行检查点。
在以下章节中,将描述检查点的概述和保存当前检查点的元数据的pg_control文件。
检查点进程负责两个方面:为数据库恢复做准备和共享缓冲池上的脏页清除。在本小节中,将重点关注前一个方面。参见下图和以下描述。
为了从数据库恢复的角度总结上述描述,检查点创建包含REDO点的检查点记录,并将检查点位置和更多内容存储到pg_control文件中。因此,PostgreSQL可以通过从pg_control文件提供的REDO点(从检查点记录获得)重放WAL数据来恢复自身。
由于pg_control文件包含检查点的基本信息,因此它对于数据库恢复肯定是必不可少的。如果它被破坏或不可读,则恢复过程无法启动以便无法获得起点。
即使pg_control文件存储了40多个项目,但是下一节中仅需要三个项目,如下所示:
pg_control文件存储在global子目录中;可以使用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: Sun Aug 8 15:16:38 2021
Latest checkpoint location: 0/C000F48
Prior checkpoint location: 0/C000E70
PostgreSQL 11中将会删除先前检查点
PostgreSQL 11或更高版本只存储包含最新或更新的检查点的WAL段;将不会存储包含先前检查点的旧的段文件,这样做为了以减少用于在pg_xlog(pg_wal)子目录下保存WAL段文件的磁盘空间。
PostgreSQL实现了基于重做日志的恢复功能。如果数据库服务器崩溃,PostgreSQL通过从REDO点顺序重放WAL段文件中的XLOG记录来恢复数据库集群。
在本节之前我们已经多次讨论了数据库恢复,因此我将描述两方面有关恢复的,但尚未介绍过的内容。
第一点是PostgreSQL如何开始恢复处理。当PostgreSQL启动时,它首先读取pg_control文件。以下是从那个时间点开始恢复处理的细节。参见下图和以下描述。
第二点是关于LSN的比较:为什么应该比较非备份块的LSN和相应页面的pd_lsn。与前面的示例不同,这里将使用强调两个LSN之间进行比较的特定示例来解释。
与概述中的示例不同,在这种情况下,TABLE_A的页面已写入磁盘。
使用immediate模式关闭,然后启动。
图二:数据库恢复
从此示例中可以看出,如果非备份块的重放顺序不正确或者多次重放非备份块,则数据库集群将不一致。简而言之,非备份块的重做(重放)操作不是幂等的。因此,为了保留正确的重放顺序,当且仅当其LSN大于相应页面的pd_lsn时,才重放非备份块记录。
另一方面,由于备份块的重做操作是幂等的,因此备份块可以重放任意次,而不管其LSN如何。
PostgreSQL将XLOG记录写入存储在pg_xlog子目录(版本10或更高版本,pg_wal子目录)中的一个WAL段文件中,如果已填满旧文件,则切换为新文件。 WAL文件的数量根据服务端几个参数的配置而有所不同。此外,他们的管理策略在9.5版本中得到了改进。
在以下小节中,描述了WAL段文件的切换和管理。
发生以下任一情况时,会发生WAL段切换:
切换后的段文件通常被回收(重命名和重用)以供将来使用,如果没有必要,也会删除掉。
每当检查点启动时,PostgreSQL都会预估和准备下一个检查点周期所需的WAL段文件数。这种预估是参考前一个检查点周期中消耗的文件数量。它们从前一个检点点包含REDO点的段做统计,并且该值在min_wal_size(默认情况下,80 MB,即5个文件)和max_wal_size(1 GB,即64个文件)之间。如果检查点启动,将保留或回收必要的文件,同时删除不必要的文件。
具体例子如图9.17所示。假设在检查点启动之前有六个文件,WAL_3包含先前的REDO点(在版本10或更早版本中是先前的REDO点;在版本11或更高版本中,是最新REDO点),并且PostgreSQL预估需要五个文件。在这种情况下,WAL_1将重命名为WAL_7以进行回收,并且将删除WAL_2。
比前一个REDO点更老的的文件会被删除,因为从第上一节中描述的恢复机制可以清楚地看出,它们永远不会被使用。
如果由于WAL活动激增而需要更多文件,则在WAL文件的总大小小于max_wal_size时将创建新文件。例如,在下图中,如果已填充WAL_7,则新创建WAL_8。
WAL文件的数量会根据数据库的繁忙程度自适应地改变。如果WAL数据写入的数量不断增加,则WAL段文件的预估数量以及WAL文件的总大小也逐渐增加。相反的情况下(即WAL数据写入量减少),这些减少。
如果WAL文件的总大小超过max_wal_size,则将启动检查点。下图说明了这种情况。通过检查点,将创建一个新的REDO点,之前最新的REDO点变为了前一个REDO点;然后将回收不必要的旧文件。通过这种方式,PostgreSQL将始终只保存数据库恢复所需的WAL段文件。
配置参数wal_keep_segments和复制槽功能也会影响WAL段文件的数量。
持续归档是postgresql的一个功能,可在WAL段切换时将WAL段文件复制到归档区域,由归档(后台)进程执行。复制的文件称为归档日志。这项功能通常用于第10章中描述的物理热备份和PITR(时间点恢复)。
归档区域的路径设置为配置参数archive_command。例如,使用以下参数,每当段切换时,WAL段文件都会复制到目录’/home/postgres/archives/’:
archive_command = 'cp %p /home/postgres/archives/%f'
其中,%p被复制WAL段目录的占位符,%f是归档日志文件的占位符。
如上图所示:在切换WAL段文件WAL_7时,WAL_7作为archive log 7复制到归档区域。
参数archive_command可以使用任何Unix命令和工具设置,因此您可以通过scp命令将归档日志传输到其他主机,也可以使用文件备份工具替代普通的复制命令传输至其他主机。
PostgreSQL不会自动清理已经创建的归档日志,因此打开归档时应妥善管理日志。如果你什么都不做,归档日志的数量将持续增加。
pg_archivecleanup是归档日志有效的管理工具之一。
翻译整理自:https://www.interdb.jp/pg/pgsql09.html