丢失RAM中的数据的风险是我们需要在故障后恢复数据的技术的主要原因。现在我们来讨论这些技术。

日志

为了避免RAM中数据丢失,必须将所有必需的东西妥善保存到磁盘(或其他非易失性介质)中。为此,做了以下的操作。在更改数据时,还维护了这些更改的日志。当我们更改buffer cache中页面上的某些内容时,我们会在日志中创建此更改的记录。 该记录包含必要时足以重做更改的最少信息。为此,日志记录必须在更改后的页面写入磁盘之前必须先写入磁盘。 这解释了名称:预写日志(WAL)。

如果发生故障,磁盘上的数据可能会不一致:某些页面写得较早,而另一些页面写得较晚。但是WAL仍然存在,我们可以读取并重做在故障之前执行的操作,但是操作的结果会晚一点到磁盘。

 

为什么不强行将数据页面本身写入磁盘,为什么要重复工作呢?

首先,WAL是按顺序追加顺序流数据。甚至HDD磁盘也可以很好地进行顺序写入。但是,由于页面或多或少地分散在磁盘上,因此数据本身是以随机方式写入的。

其次,WAL记录可能比页面小很多。

第三,在写入磁盘时,我们不必在每个时间点都要维护磁盘上数据的一致性。

第四,正如我们稍后将看到的,WAL(一旦可用)不仅可以用于恢复,还可以用于备份和复制。

 

必须对所有操作进行WAL记录,以防发生故障时导致磁盘上的数据不一致。具体来说,以下操作是WAL记录的:

·buffer cache中对页面的修改(主要是表页面和索引页面)—因为页面更改需要花费一些时间才能到达磁盘。

·事务的提交和中止-因为状态更改是在XACT缓冲区中完成的,所以更改也需要一些时间才能到达磁盘。

·文件操作(创建和删除文件和目录,例如在创建表期间创建文件)-因为这些操作必须与数据更改同步。

 

以下内容未进行WAL记录:

·对unlogged属性的表的操作。

·对临时表的操作-日志记录没有意义,因为此类表的生存期不超过创建它们的会话的生存期。

 

在PostgreSQL 10之前,没有对哈希索引写WAL,但是此问题已得到纠正。

逻辑结构

PostgreSQL的WAL(2)--Write-Ahead Log_postgresql

 

我们可以从逻辑上将WAL想象成一系列不同长度的记录。每个记录都包含有关特定操作的数据,并以标准header作为前缀。在header中,其余部分指定以下内容:

·与记录相关的事务ID。

·资源管理器-负责记录的系统组件。

·校验和(CRC)-用于检测数据是否损坏。

·记录的长度,并链接到上一个记录。

 

至于数据,它们可以具有不同的格式和含义。例如:它们可以由需要以一定偏移量写在页面内容顶部的页面片段表示。资源管理器指定了“理解”如何解释其记录中的数据。有单独的表管理器,每种类型的索引,事务状态等等。可以使用以下命令获取它们的完整列表

pg_waldump -r list
XLOG
Transaction
Storage
CLOG
Database
Tablespace
MultiXact
RelMap
Standby
Heap2
Heap
Btree
Hash
Gin
Gist
Sequence
SPGist
BRIN
CommitTs
ReplicationOrigin
Generic
LogicalMessage
物理结构

WAL作为$PGDATA/pg_wal目录中的文件存储在磁盘上。默认情况下,每个文件为16 MB。可以增加此大小,以避免在一个目录中包含多个文件。在PostgreSQL 11之前,只能在编译源代码时执行此操作,但是现在可以在初始化集群时指定大小(使用--wal-segsize选项)。

WAL记录写入当前使用的文件,一旦结束,将使用下一个文件。

在服务器的共享内存中,为WAL分配了特殊的缓冲区。wal_buffers参数指定WAL缓存的大小(默认值表示自动设置:已分配buffer cache的1/32)。

WAL缓存的结构类似于buffer cache,但是以循环模式下工作:将记录添加到“ head”,但从“ tail”开始写入磁盘。

pg_current_wal_lsn和pg_current_wal_insert_lsn函数分别返回写(«tail»)和插入(«head»)位置:

=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 0/331E4E64         | 0/331E4EA0
(1 row)

为了引用某个记录,使用了pg_lsn数据类型:它是一个64位整数,表示记录的开始相对于WAL的开始的字节偏移。LSN以斜杠分隔的两个32位十六进制数字输出。

我们可以知道在哪个文件中可以找到所需的位置以及与文件开头的偏移量:

# select pg_current_wal_lsn(),pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 23C/5AFCAE38       | 23C/5AFCAE38
(1 row)

s=# SELECT file_name, upper(to_hex(file_offset)) file_offset
postgres-# FROM pg_walfile_name_offset('23C/5AFCAE38');
        file_name         | file_offset 
--------------------------+-------------
 000000010000023C0000005A | FCAE38
(1 row)

=# 

文件名由两部分组成。8位高位十六进制数字显示时间线的编号(用于从备份还原),其余部分对应于LSN的高位(LSN其余的低位显示偏移量)。

在文件系统中,可以在$PGDATA/pg_wal/目录中看到WAL文件,但是从PostgreSQL 10开始,还可以使用专门的功能来查看它们:

=> SELECT * FROM pg_ls_waldir() WHERE name = '000000010000000000000033';
           name           |   size   |      modification      
--------------------------+----------+------------------------
 000000010000000000000033 | 16777216 | 2019-07-08 20:24:13+03
(1 row)
WAL

让我们看看如何进行WAL以及如何确保提前写入。让我们创建一个表:

=> CREATE TABLE wal(id integer);
=> INSERT INTO wal VALUES (1);

我们来研究page的header部分。为此,我们需要一个著名的扩展:

=> CREATE EXTENSION pageinspect;

让我们开始一个事务,并记住插入WAL的位置:

=> BEGIN;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F377C
(1 row)

  

现在我们将执行一些操作,例如,更新一行:

=> UPDATE wal set id = id + 1;

此更改已写WAL记录,并且插入位置已更改:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F37C4
(1 row)

为了确保在WAL写入之前不会将更改后的数据页刷新到磁盘,与该页相关的最后一个WAL记录的LSN存储在页头中:

=> SELECT lsn FROM page_header(get_raw_page('wal',0));
    lsn     
------------
 0/331F37C4
(1 row)

请注意,WAL是整个集群的,新记录始终都在那里。因此,页面上的LSN可以小于pg_current_wal_insert_lsn函数刚返回的值。但是由于在我们的系统中什么也没有发生,因此数字是相同的。

现在,让我们提交事务。

=> COMMIT;

提交也被WAL记录,并且位置再次更改:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/331F37E8
(1 row)

每次提交都会变更XACT结构中事务状态(我们已经讨论过)。状态存储在文件中,但它们也使用自己的缓存,该缓存在共享内存中占据128页。因此,对于XACT页面,也必须跟踪最后一个WAL记录的LSN。但是,此信息存储在RAM中,而不是存储在页面本身中。

创建的WAL记录将被一次写入磁盘。我们将在稍后的某个时间讨论确切的情况,但是在上述情况下,情况已经发生:

=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
--------------------+---------------------------
 0/331F37E8         | 0/331F37E8
(1 row)

此后,数据和XACT页面可以刷新到磁盘。但是,如果我们不得不更早地刷新它们,它将被检测到,并且WAL记录将被迫首先进入磁盘。

具有两个LSN位置,我们可以通过简单地从另一个中减去一个来获得它们之间的WAL记录数量(以字节为单位)。我们只需要将ksn位置转换为pg_lsn类型:

=> SELECT '0/331F37E8'::pg_lsn - '0/331F377C'::pg_lsn;
 ?column? 
----------
      108
(1 row)

在这种情况下,行和提交的更新在WAL中需要108个字节。

我们可以用相同的方式评估服务器在一定负载下每单位时间生成的WAL记录的数量。这是重要的信息,需要进行调优(我们将在下次讨论)。

现在,让我们使用pg_waldump实用工具查看创建的WAL记录。

该工具还可以使用一系列LSN(如本例所示),并为指定的事务选择记录。应该以postgres OS用户身份运行该工具,因为它需要访问磁盘上的WAL文件。

postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/331F377C -e 0/331F37E8 000000010000000000000033
rmgr: Heap        len (rec/tot):     69/    69, tx:     101085, lsn: 0/331F377C, prev 0/331F3014, desc: HOT_UPDATE off 1 xmax 101085 ; new off 2 xmax 0, blkref #0: rel 1663/16386/33081 blk 0
rmgr: Transaction len (rec/tot):     34/    34, tx:     101085, lsn: 0/331F37C4, prev 0/331F377C, desc: COMMIT 2019-07-08 20:24:13.945435 MSK

在这里,我们看到两个记录的header。

第一个是HOT_UPDATE操作,与堆资源管理器有关。文件名和页号在blkref字段中指定,与更新后的表页面相同:

=> SELECT pg_relation_filepath('wal');
 pg_relation_filepath 
----------------------
 base/16386/33081
(1 row)

第二个记录是COMMIT,与事务资源管理器有关。

这种格式几乎不容易阅读,但是可以让我们在需要时使用。

恢复

当我们启动服务器时,首先启动postmaster进程,然后启动startup进程,该启动进程的任务是确保在发生故障时进行恢复。

为了确定是否需要恢复,startup进程会在专用控制文件$PGDATA/global/pg_control中查看集群状态。但是我们也可以通过pg_controldata实用程序自己检查状态:

postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state
Database cluster state:               in production

常规方式关闭的postgresql server将处于“shutdown”状态。如果postgresql server不工作,但状态仍处于“in production”,则表示DBMS已关闭,并且恢复将自动完成。

为了进行恢复,startup进程将顺序读取WAL并将记录应用于页面(如果需要)。可以通过将磁盘上页面的LSN与WAL记录的LSN进行比较来确定需求。如果页面的LSN看起来更大,则不需要应用该记录。实际上,它甚至无法应用,因为记录是按照严格的顺序应用的。

但是也有例外。某些记录被创建为FPI(full page image),该记录将覆盖页面内容,因此无论其状态如何都可以将其应用于页面。可以将事务状态的更改应用于XACT页的任何版本,因此无需在此类页中存储LSN。

在恢复过程中,与常规工作一样,页面将在buffer cache中更改。为此,postmaster进程启动所需的后台进程。

WAL记录以类似的方式应用于文件。

And at the very end of the recovery process, respective initialization forks overwrite all unlogged tables to make them empty。

这是该算法的非常简化的描述。具体来说,到目前为止,我们还没有说起从哪里开始阅读WAL记录。

最后要提的是:恢复过程包括两个阶段。在第一(前滚)阶段,将应用日志记录,并且服务器将重做由于故障而丢失的所有工作。在第二(回滚)阶段,将回滚在故障时刻尚未提交的事务。但是PostgreSQL不需要第二阶段。如前所述,由于多版本并发控制的实现功能,无需物理回滚事务-因此无需在XACT中设置commit位。