暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

【PG高可用】-Postgresql wal文件和参数解析

原创 闫伟 2021-09-10
2839

Table of Contents

1 Postgresql WAL简介和基本术语

​ 事务日志是数据库的重要组成部分,存储了数据库系统中所有更改和操作的历史,以确保数据库不会因为故障(例如掉电或其他导致服务器崩溃的故障)而丢失数据。在PostgreSQL(以下简称PG)中,事务日志文件称为Write Ahead Log(以下简称WAL)。

​ WAL(预写式日志)的概念就是在修改数据之前,必须要把这些修改操作记录到磁盘中,这样后面更新实际数据时,就不需要实时地把数据持久化到文件中了。即使机器突然宕机或数据库异常退出,导致一部分内存中的脏数据没有及时地刷新到文件中,在数据库重启后,通过读取WAL日志,并把最后一部分的WAL日志重新执行一遍,就可以恢复到宕机时的状态。

1.1 作用

​ WAL作用是什么?总结如有以下几点:

  • 实现事务的持久性
  • 物理复制(流复制)
  • 逻辑复制
  • 系统崩溃后恢复
  • 基于时间点恢复(PITR)

1.2 术语

术语 说明
Redo Log 1.Redo Log保存数据库的所有修改历史
2.Redo Log Files用于恢复/增量备份和PITR(Point In Time Recovery)/复制
3.在写入数据文件前,每个数据库的变更都会先行写入到Redo Log File中
WAL segment file 持久化存储设备上的Redo Log文件,在PG中每个WAL segment file大小为16MB(默认).
–with-wal-segsize=SEGSIZE可以编译大小
在initdb(PG11版本)初始化通过–wal-segsize也可以指定大小;同时-X可以指定目录
XLOG Record(WAL data) 在PG中用于存储历史修改,可以认为是Redo log.
WAL buffer WAL Record缓冲区,Redo Log先写入到缓冲区,在"合适的时候"通过WAL writer写入到WAL segment file中.
LSN (Log Sequence Number) WAL record中的LSN表示该记录写入到事务日志中位置,大小为uint64.
在XLOG Record中,LSN是唯一的.
checkpointer 检查点后台进程,执行checkpoint.
Redo point PostgreSQL在执行Crash Recover时的起始点.
checkpointer后台进程启动时,Redo point存储在内存中.
在PG运行过程中,Redo point调整为指向启动最新检查点时在WAL segment file中WALRecord的写入位置.
checkpoint record 在执行checkpoint时,首先会在WAL buffer中写入checkpoint相关的XLOG Record,里面包含有Redo point,这种类型的的XLOG Record成为checkpoint record.
pg_control pg_control是物理文件,保存检查点的基本信息,在数据库恢复中使用.
可通过命令pg_controldata查看该文件的相关信息.

1.3 WAL段切换

当发生下面任一种情况时,WAL段会发生切换:
(1)WAL段已经被填满
(2)调用函数 pg_switch_wal()
(3)启用了 archive_mode 且已经超过archive_timeout 配置的时间

2 Postgresql WAL相关参数和系统函数

img

2.1 WAL 参数

  • wal_level

    可选的参数值是minimal、replica、logical。需要这个参数需要重启PG,默认值是replica;如果需要逻辑复制需要调整为logical,如果需要流复制至少是replica,在minimal级别中,minimal会去掉除从崩溃或者立即关机中进行恢复所需的信息之外的所有记录对于创建或重写永久关系的事务的其余部分,不会记录任何信息,建议保持默认设置。

  • fsync

​ 表示数据库将调用fsync()把文件系统中的脏页刷新到物理磁盘,确保数据库在操作系统或者硬件崩溃的情况下可恢复到一个一致的状 态。默认值on。fsync = off时,可以关闭full_page_writes,因为即使full_page_writes = on 也无法保证数据安全性。

  • synchronous_commit (enum)

    表示提交事务提交后是否需要等待 WAL 刷到磁盘后才返回成功信息。默认值on。
    可选的参数值是 off、local、remote_write、remote_apply、on。
    不同于 fsync,将这个参数设置为 off 不会产生数据库不一致性的风险:只会导致用户最近的几个已提交成功的事务丢失,即在数据库崩溃或突然关机后,重启数据库时,用户会发现故障时间点附近的这几个事务实际上并没有提交成功,而是回滚掉了,而数据库状态是一致的。

    如果synchronous_standby_names为空,则唯一有意义的设置为on 和 off ; remote_apply,remote_write 和 local都提供与on相同的本地同步级别。

    如果synchronous_standby_names为非空:

    • 当设置为 remote_apply 时,提交将等待,直到来自当前同步备用服务器的答复显示他们已收到事务的提交记录并应用了它,以便它变得对备用服务器上的查询可见,并写入备用服务器上的持久存储。 这将导致比以前的设置更大的提交延迟,因为它等待 WAL 重放(replay)。 简单的说remote_apply 表示本地wal已落盘,备库wal已落盘并且已经完成重做,这个设置保证了拥有两份持久化的wal,同时备库也已经完成了重做。这个选项带来的事务响应时间最高。

    • 当这个参数被设置为remote_write时,表示流复制主库提交事务时,需等待备库接收主库发送的wal日志流并写入备节点操作系统缓存中,之后向客户端返回成功,这种情况下备库出现异常关闭时不会有已传送的wal日志丢失风险,但备库操作系统异常宕机就有已传送的wal丢失风险了,此时wal可能还没有完全写入备节点wal文件中,简单的说 remote_write 表示本地wal已落盘,备库的wal还在备库操作系统缓存中,也就是说只有一份持久化的wal。这个选项带来的事务响应时间较低。

    • 当设置为on时,提交将等待,直到来自于当前同步的后备服务器的回复显示它们已经收到了事务的提交记录并将其刷入了磁盘。 这保证事务将不会被丢失,除非主服务器和所有同步后备都遭受到了数据库存储损坏的问题。简单的说on表示本地wal已落盘,备库的wal也已落盘,有两份持久化的wal,但备库此时还没有完成重做。这个选项带来的事务响应时间较高。

    • 设置local会导致提交等待本地刷写到磁盘,而不是复制。在使用同步复制时这通常是不可取的,但是为了完整性提供了这个选项。

    synchronous_commit setting local durable commit standby durable commit after PG crash standby durable commit after OS crash standby query consistency
    remote_apply
    on
    remote_write
    local
    off
  • wal_sync_method (enum)

    表示向磁盘强制更新 WAL 数据的方法,默认值fsync。可选的参数值是open_datasync、fdatasync、fsync、fsync_writethrough、open_sync。此选项一般保持默认值。如果fsync = off,该参数设置就没有意义。

  • full_page_writes

    服务器会在检查点checkpoint之后对页面第一次修改时将整个页面写到 WAL 日志里。默认值on。
    这么做是因为在操作系统崩溃期间可能只有部分页面写入磁盘,导致在一个页面中混合有新旧数据。将完整的页面保存在WAL日志中,就可以直接使用WAL日志中的页覆盖新旧数据混合页以完成恢复工作。

  • wal_log_hints (boolean)

​ 默认值是off。需要重启,当这个参数设置为ON时,PostgreSQL数据库服务器在一个检查点之后第一次页面更改过程(即使是对提 示位进行非关键性的修改)中,将每个磁盘页的全部内容写入到WAL中。

​ 如果开启了数据校验和,用数据页校验和初始化集群,那么提示位更新总是被记录在WAL中,所以就不需要将这个参数设置为ON了,因为此时无论如何都会记录提示位。

  • wal_compression (boolean)

    当这个参数为on时,如果full_page_writes 为打开或者处于基础备份期间,PostgreSQL服务器 会压缩写入到 WAL 中的完整页面镜像。压缩页面镜像将在 WAL 重放时 被解压。默认值为off。只有超级用户可以更改这个设置。 打开这个参数可以减小 WAL 所占的空间且无需承受不可恢复的数据损坏风险, 但是代价是需要额外的 CPU 开销以便在 WAL 记录期间进行压缩以及在 WAL 重放时解压。

  • wal_init_zero (boolean)

    如果设置为on(默认值),此选项会导致新的 WAL 文件被零填充。 在某些文件系统上,这可确保在我们需要写入 WAL 记录之前分配空间。 但是,Copy-On-Write(COW)文件系统可能不会从此技术中受益,因此可以选择跳过不必要的工作。 如果设置为off,则在创建文件时仅写入最终字节,以便其具有预期大小。

  • wal_recycle (boolean)

    如果设置为 on (默认值),此选项通过重命名来回收 WAL 文件,从而避免创建新文件。 在 COW 文件系统上,创建新文件系统可能更快,因此提供了禁用此行为的选项。

  • wal_buffers (integer)

    用于还未写入磁盘的 WAL 数据的共享内存量。默认值 -1 选择等于shared_buffers的 1/32 的尺寸(大约3%),但是不小于64kB也不大于 WAL 段的尺寸(通常为)。如果自动的选择太大或太小可以手工设置该值,但是任何小于32kB的正值都将被当作32kB。 如果指定值时没有单位,则以WAL块作为单位,即为 XLOG_BLCKSZ 字节,通常为8kB。这个参数只能在服务器启动时设置。

  • wal_writer_delay (integer)

    指定 WAL 写入器刷写 WAL 的频繁程度,以时间为单位。 在刷写WAL之后,写入器将根据wal_writer_delay所给出的时间长度进行睡眠,除非被一个异步提交的事务提前唤醒。 如果最近的刷写发生在 wal_writer_delay 之前,并且小于 wal_writer_flush_after WAL的值产生之后,那么WAL只会被写入操作系统,而不会被刷写到磁盘。 如果指定值时没有单位,则以毫秒作为单位。 默认值是 200 毫秒(200ms)。注意在很多系统上,有效的睡眠延迟粒度是 10 毫秒,把wal_writer_delay设置为一个不是 10 的倍数的值,其效果和把它设置为大于该值的下一个 10 的倍数产生的效果相同。这个参数只能在postgresql.conf文件中或者服务器命令行上设置。

  • wal_writer_flush_after (integer)

    指定 WAL 写入器刷写 WAL 的频繁程度,以卷为单位。 如果最近的刷写发生在 wal_writer_delay 之前,并且小于 wal_writer_flush_after WAL的值产生之后,那么WAL只会被写入操作系统,而不会被刷写到磁盘。 如果wal_writer_flush_after被设置为0,则WAL数据总是会被立即刷写。 如果指定值时没有单位,则以WAL块作为单位,即为XLOG_BLCKSZ字节,通常为8kB。 默认是1MB。这个参数只能在postgresql.conf文件中或者服务器命令行上设置。

  • wal_skip_threshold (integer)

    wal_levelminimal,并且在创建或重写永久关系之后提交事务时,此设置将确定如何保留新数据。 如果数据小于此设置,将其写入 WAL 日志;否则,使用受影响文件的 fsync。 根据存储的属性,如果此类提交减慢了并发事务,提高或降低此值可能会有所帮助。 如果指定此值时没有单位,则视为千字节。默认为两兆字节(2MB)。

  • commit_delay (integer)

    在一次 WAL 刷写被发起之前,commit_delay增加一个时间延迟。 如果系统负载足够高,使得在一个给定间隔内有额外的事务准备好提交,那么通过允许更多事务通过一个单次 WAL 刷写来提交能够提高组提交的吞吐量。 但是,它也把每次 WAL 刷写的潜伏期增加到了最多commit_delay。 因为如果没有其他事务准备好提交,就会浪费一次延迟,只有在当一次刷写将要被发起时有至少commit_siblings个其他活动事务时,才会执行一次延迟。 另外,如果fsync被禁用,则将不会执行任何延迟。 如果指定值时没有单位,则以微秒作为单位。 默认的commit_delay是零(无延迟)。只有超级用户才能修改这个设置。 在PostgreSQL的 9.3 发布之前,commit_delay的行为不同并且效果更差:它只影响提交,而不是所有 WAL 刷写,并且即使在 WAL 刷写马上就要完成时也会等待一整个配置的延迟。从PostgreSQL 9.3 中开始,第一个准备好刷写的进程会等待配置的间隔,而后续的进程只等到领先者完成刷写操作。

  • commit_siblings (integer)

    在执行commit_delay延迟时,要求的并发活动事务的最小数目。大一些的值会导致在延迟间隔期间更可能有至少另外一个事务准备好提交。默认值是五个事务。

  • wal_keep_segments

    指定在后备服务器需要为流复制获取日志段文件的情况下,pg_wal目录下所能保留的过去日志文件段的最小数目。13版本之后调整了

    wal_keep_sizes

  • wal_block_size

    wal块大小。默认8K

  • wal_segment_size

    wal的页大小*block size=默认16M

2.2 检查点参数

  • checkpoint_timeout (integer)

    自动 WAL 检查点之间的最长时间。如果指定值时没有单位,则以秒为单位。 合理的范围在 30 秒到 1 天之间。默认是 5 分钟(5min)。增加这个参数的值会增加崩溃恢复所需的时间。这个参数只能在postgresql.conf文件中或在服务器命令行上设置。

  • checkpoint_completion_target (floating point)

    指定检查点完成的目标,作为检查点之间总时间的一部分。默认是 0.5。 这个参数只能在postgresql.conf文件中或在服务器命令行上设置。

  • checkpoint_flush_after (integer)

    当执行检查点时写入的数据量超过此数量时,就尝试强制 OS 把这些写发送到底层存储。 这样做将会限制内核页面高速缓存中的脏数据数量,降低在检查点末尾发出fsync或者 OS 在后台大批量写回数据时被卡住的可能性。 那常常会导致大幅度压缩的事务延迟,但是也有一些情况(特别是负载超过shared_buffers但小于 OS 页面高速缓存)的性能会降低。 这种设置可能会在某些平台上没有效果。 如果指定值时没有单位,则以块为单位,即为BLCKSZ 字节,通常为8kB。 合法的范围在0(禁用强制写回)和2MB之间。Linux 上的默认值是256kB,其他平台上是0(如果BLCKSZ不是8kB,则默认值和最大值会按比例缩放到它)。这个参数只能在postgresql.conf文件中或者服务器命令行上设置。

  • checkpoint_warning (integer)

    如果由于填充WAL段文件导致的检查点之间的间隔低于这个参数表示的时间量,那么就向服务器日志写一个消息(它建议增加max_wal_size的值)。 如果指定值时没有单位,则以秒为单位。默认值是 30 秒(30s)。零则关闭警告。如果checkpoint_timeout低于checkpoint_warning,则不会有警告产生。这个参数只能在postgresql.conf文件中或在服务器命令行上设置。

  • max_wal_size (integer)

    在自动 WAL 检查点之间允许 WAL 增长到的最大尺寸。这是一个软限制,在特殊的情况下 WAL 尺寸可能会超过max_wal_size, 例如在重度负荷下、archive_command失败或者高的 wal_keep_size设置。 如果指定值时没有单位,则以兆字节为单位。默认为 1 GB。增加这个参数可能导致崩溃恢复所需的时间。 这个参数只能在postgresql.conf 或者服务器命令行中设置。

  • min_wal_size (integer)

    只要 WAL 磁盘用量保持在这个设置之下,在检查点时旧的 WAL 文件总是 被回收以便未来使用,而不是直接被删除。这可以被用来确保有足够的 WAL 空间被保留来应付 WAL 使用的高峰,例如运行大型的批处理任务。 如果指定值时没有单位,则以兆字节为单位。默认是 80 MB。这个参数只能在postgresql.conf 或者服务器命令行中设置。

2.3 归档参数

  • archive_mode (enum)

    当启用archive_mode时,可以通过设置 archive_command命令将完成的 WAL 段发送到 归档存储。除用于禁用的off之外,还有两种模式: onalways。在普通操作期间,这两种模式之间 没有区别,但是当设置为always时,WAL 归档器在归档恢复 或者后备模式下也会被启用。这个参数只能在服务器启动时设置。当wal_level被设置为minimal时,archive_mode不能被启用。

  • archive_command (string)

    本地 shell 命令被执行来归档一个完成的 WAL 文件段。字符串中的任何%p被替换成要被归档的文件的路径名, 而%f只被文件名替换(路径名是相对于服务器的工作目录, 即集簇的数据目录)。

    例如:‘test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f’
    字符串中的%p 要被归档的文件的路径名,%f 归档的文件名

  • archive_timeout (integer)

    archive_command仅在已完成的 WAL 段上调用。因此,如果你的服务器只产生很少的 WAL 流量(或产生流量的周期很长),那么在事务完成和它被安全地记录到归档存储之间将有一个很长的延迟。为了限制未归档数据存在的时间,你可以设置archive_timeout来强制服务器来周期性地切换到一个新的 WAL 段文件。为了限制未归档数据存在的时间,可以设置archive_timeout来强制服务器来周期性地切换到一个新的 WAL 段文件。

2.4 恢复参数

  • restore_command (string)

    用于获取 WAL 文件系列的一个已归档段的本地 shell 命令。这个参数是归档恢复所必需的,但是对于流复制是可选的。例如:

    restore_command = ‘cp /mnt/server/archivedir/%f “%p”’

  • archive_cleanup_command (string)

    这个可选参数指定了一个 shell 命令,它将在每一个重启点被执行。

    archive_cleanup_command = ‘pg_archivecleanup /mnt/server/archivedir %r’

  • recovery_end_command (string)

    这个参数指定了一个将只在恢复末尾被执行一次的 shell 命令。

2.5 恢复目标

默认情况下,恢复将会一直恢复到 WAL 日志的末尾。下面的参数可以被用来指定一个更早的停止点。 在recovery_targetrecovery_target_lsnrecovery_target_namerecovery_target_timerecovery_target_xid中, 最多只能使用一个

  • recovery_target`` = 'immediate' --目前唯一的值
  • recovery_target_name (string) --这个参数指定(pg_create_restore_point()所创建)的已命名的恢复点,恢复将进入该恢复点。
  • recovery_target_time (timestamp) --恢复指定时间点
  • recovery_target_time (timestamp) --恢复指定的事务ID
  • recovery_target_lsn (pg_lsn) --恢复指定的WAL日志的LSN
  • recovery_target_timeline (string) --指定恢复到一个特定的时间线中。该值可以是数字时间线 ID 或特殊值

相关部分我会单独在备份和恢复的笔记中进行相关测试和总结

2.5 相关函数使用

  • pg_current_wal_flush_lsn

  • pg_current_wal_insert_lsn

  • pg_current_wal_lsn

  • pg_wal_file_name

  • pg_walfile_name_offset

  • pg_wal_lsn_diff

  • pg_wal_replay_pause

  • pg_wal_replay_resume

  • pg_switch_wal

函数 说明 使用
pg_current_wal_lsn 当前WAL的写入位置
pg_current_wal_flush_lsn 当前的预写式日志刷写位置
pg_current_wal_insert_lsn pg_current_wal_insert_lsn() 写入 wal buffer 的位置
pg_wal_file_name 通过lsn查看对应的WAL文件
pg_walfile_name_offset 转换预写式日志位置字符串为文件名以及文件内的十进制字节偏移
pg_wal_lsn_diff 计算两个预写式日志位置间的差别
pg_wal_replay_pause 立即暂停恢复
pg_wal_replay_resume 如果恢复被暂停,重启
pg_switch_wal 强制切换到一个新的预写式日志文件(默认只限于超级用户,但是可以授予其他用户 EXECUTE 特权来执行该函数)
pg_ls_waldir 查看walfile文件的路径

1、检查同步状态
select pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) as sent_delay,   
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn)) as write_delay,   
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn)) as flush_delay,   
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) as replay_delay,   
*  
from pg_stat_replication;

sent_location     # Master传送WAL的位置
write_location    #Slave接收WAL的位置(写入到磁盘)
flush_location    #Slave同步到磁盘的WAL位置(刷入到磁盘)
replay_location   #同步到数据库的WAL位置(应用到数据库)


2、查看当前的LSN和wal 名称
select pg_current_wal_lsn(),pg_walfile_name(pg_current_wal_lsn());
 pg_current_wal_lsn |     pg_walfile_name
--------------------+--------------------------
 0/19291E8          | 000000010000000000000001
(1 row)

3、查看文件路径
select * from pg_ls_waldir() order by modification desc limit 5;

4、通过系统函数来获得当前WAL的写入位置和插入位置
highgo=# select pg_current_wal_lsn(),pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn 
----------------------------+---------------------------
  0/1B000108              | 0/1B000108
LSN为0/1B000108
logId就是“0/1B000108”中的第一个数字,即“0”
logSeg就是“0/1B000108”中的第二个数字除以16M的大小,即1B000108除以16M,而16M相当于2的24次方,相当于十六进制数“1B000108”右移6位,即“1B000108”中的最高两位“1B”
那么根据WAL文件的格式timelineID+logId+logSeg,则相当于:“00000001”+“00000000”+“0000001B”,即为:“00000001000000000000001B”
写的位置是在文件“00000001000000000000001B”中的偏移量是多少呢?实际上是在“0/1B000108”中第二个数字“1B000108”后六位“000108”,换算成十进制为“264”。

5、查看offset
select pg_walfile_name_offset('0/1B000108');
     pg_walfile_name_offset     
--------------------------------------
 (00000001000000000000001B,264)
 
 select pg_current_wal_lsn(),
                  pg_walfile_name(pg_current_wal_lsn()),
                  pg_walfile_name_offset(pg_current_wal_lsn());

pg_current_wal_lsn |     pg_walfile_name      |       pg_walfile_name_offset        
--------------------+--------------------------+-------------------------------------
 8/20E63FE8         | 000000010000000800000020 | (000000010000000800000020,15089640)

2.6 wal_writer_flush_after 和 wal_writer_delay

两个都需要遵循。

如果事务中的数据在没有达到指定的 wal_writer_flush_after 参数指标,将会参考 wal_writer_delay。
如果事务中的数据没有达到 wal_writer_delay 参数指标,将会参考 wal_writer_flush_after

3 Postgresql WAL内部结构

3.1 LSN和WAL文件名称解析

img

PG使用无符号64bit整型(uint64)作为日志文件的寻址空间,

WAL segment file
WAL segment file文件长度为24,由3部分组成,每个部分是8个16进制数字:
1.第1部分是TimeLineID,0x00000000 -> 0xFFFFFFFF
2.第2部分是逻辑文件ID,0x00000000 -> 0xFFFFFFFF
3.第3部分是物理文件ID,0x00000000 -> 0x000000FF
逻辑文件ID占32bit,物理文件ID占8bit,16M的文件占24bit,合计64bit.PG通过这三部分的组合,达到最大64bit的文件寻址空间.


postgres@[local]:5432=#select pg_current_wal_lsn(),
postgres-#                   pg_walfile_name(pg_current_wal_lsn()),
postgres-#                   pg_walfile_name_offset(pg_current_wal_lsn());
 pg_current_wal_lsn |     pg_walfile_name      |       pg_walfile_name_offset
--------------------+--------------------------+------------------------------------
 0/19291E8          | 000000010000000000000001 | (000000010000000000000001,9605608)
(1 row)

 
LSN为0/19291E8
logId就是“0/19291E8”中的第一个数字,即“0”
logSeg就是“0/1B000108”中的第二个数字除以16M的大小,即19291E8除以16M,而16M相当于2的24次方,相当于十六进制数“19291E8”右移6位,即“19291E8”中的最高两位“01”
那么根据WAL文件的格式timelineID+logId+logSeg,则相当于:“00000001”+“00000000”+“00000001”,即为:“000000010000000000000001”
写的位置是在文件“000000010000000000000001”中的偏移量是多少呢?实际上是在“0/1B000108”中第二个数字“1B000108”后六位“9291E8”,换算成十进制为“9605608”。

postgres@[local]:5432=#select cast(cast(('x'||'009291E8') AS bit(32)) as int);
  int4
---------
 9605608
(1 row)


postgres@[local]:5432=#SELECT file_name, upper(to_hex(file_offset)) file_offset
postgres-# FROM pg_walfile_name_offset('0/19291E8');
        file_name         | file_offset
--------------------------+-------------
 000000010000000000000001 | 9291E8
(1 row)

3.2 WAL文件内部结构

img

WAL segment file
WAL segment file内部划分为N个page(Block),每个page大小为8K,第一个page的header对应的数据结构为XLogLongPageHeaderData,其他page的header对应的数据结构是XLogPageHeaderData.在header后是N个XLOG Record.

XLOG Record
XLOG Record由两部分组成,第一部分固定大小,对应的结构体为XLogRecord;第二部分是XLOG Record data

XLOG Record data
XLOG Record data由以下几部分组成:
1.0..N个XLogRecordBlockHeader,每个XLogRecordBlockHeader对应一个block data;
注意:如设置了BKPBLOCK_HAS_IMAGE标记,则在XLogRecordBlockHeader结构体后跟XLogRecordBlockImageHeader结构体;如设置了BKPIMAGE_HAS_HOLE和 BKPIMAGE_IS_COMPRESSED则在XLogRecordBlockImageHeader后跟XLogRecordBlockCompressHeader结构体;
2.XLogRecordDataHeader[Short|Long]:如数据<256Bytes,则使用Short格式,否则使用Long格式;
3.block data:full-write-block数据,如启用了压缩,则压缩存储,相关元数据存储在XLogRecordBlockHeader中的XLogRecordBlockCompressHeader中.
4.main data:(tuple) data/checkpoint等日志数据.


XLogPageHeaderData
每一个事务日志文件(WAL segment file)的page(大小默认为8K)都有头部数据.
注:每个文件第一个page的头部数据是XLogLongPageHeaderData(详见后续描述),而不是XLogPageHeaderData

/*
 * Each page of XLOG file has a header like this:
 * 每一个事务日志文件的page都有头部信息,结构如下:
 */
//可作为WAL版本信息
#define XLOG_PAGE_MAGIC 0xD098  /* can be used as WAL version indicator */

typedef struct XLogPageHeaderData
{
    //WAL版本信息,PG V11.1 --> 0xD98
    uint16      xlp_magic;      /* magic value for correctness checks */
    //标记位(详见下面说明)
    uint16      xlp_info;       /* flag bits, see below */
    //page中第一个XLOG Record的TimeLineID,类型为uint32
    TimeLineID  xlp_tli;        /* TimeLineID of first record on page */
    //page的XLOG地址(在事务日志中的偏移),类型为uint64
    XLogRecPtr  xlp_pageaddr;   /* XLOG address of this page */

    /*
     * When there is not enough space on current page for whole record, we
     * continue on the next page.  xlp_rem_len is the number of bytes
     * remaining from a previous page.
     * 如果当前页的空间不足以存储整个XLOG Record,在下一个页面中存储余下的数据
     * xlp_rem_len表示上一页XLOG Record剩余部分的大小
     *
     * Note that xl_rem_len includes backup-block data; that is, it tracks
     * xl_tot_len not xl_len in the initial header.  Also note that the
     * continuation data isn't necessarily aligned.
     * 注意xl_rem_len包含backup-block data(full-page-write);
     * 也就是说在初始的头部信息中跟踪的是xl_tot_len而不是xl_len.
     * 另外要注意的是剩余的数据不需要对齐.
     */
    //上一页空间不够存储XLOG Record,该Record在本页继续存储占用的空间大小
    uint32      xlp_rem_len;    /* total len of remaining data for record */
} XLogPageHeaderData;

#define SizeOfXLogShortPHD  MAXALIGN(sizeof(XLogPageHeaderData))

typedef XLogPageHeaderData *XLogPageHeader;

XLogLongPageHeaderData
如设置了XLP_LONG_HEADER标记,在page header中存储额外的字段.
(通常在每个事务日志文件也就是segment file的的第一个page中存在).
这些附加的字段用于准确的识别文件。


/*
 * When the XLP_LONG_HEADER flag is set, we store additional fields in the
 * page header.  (This is ordinarily done just in the first page of an
 * XLOG file.)  The additional fields serve to identify the file accurately.
 * 如设置了XLP_LONG_HEADER标记,在page header中存储额外的字段.
 * (通常在每个事务日志文件也就是segment file的的第一个page中存在).
 * 附加字段用于准确识别文件。
 */
typedef struct XLogLongPageHeaderData
{
    //标准的头部域字段
    XLogPageHeaderData std;     /* standard header fields */
    //pg_control中的系统标识码
    uint64      xlp_sysid;      /* system identifier from pg_control */
    //交叉检查
    uint32      xlp_seg_size;   /* just as a cross-check */
    //交叉检查
    uint32      xlp_xlog_blcksz;    /* just as a cross-check */
} XLogLongPageHeaderData;

#define SizeOfXLogLongPHD   MAXALIGN(sizeof(XLogLongPageHeaderData))
//指针
typedef XLogLongPageHeaderData *XLogLongPageHeader;

/* When record crosses page boundary, set this flag in new page's header */
//如果XLOG Record跨越page边界,在新page header中设置该标志位
#define XLP_FIRST_IS_CONTRECORD     0x0001
//该标志位标明是"long"页头
/* This flag indicates a "long" page header */
#define XLP_LONG_HEADER             0x0002
/* This flag indicates backup blocks starting in this page are optional */
//该标志位标明从该页起始的backup blocks是可选的(不一定存在)
#define XLP_BKP_REMOVABLE           0x0004
//xlp_info中所有定义的标志位(用于page header的有效性检查)
/* All defined flag bits in xlp_info (used for validity checking of header) */
#define XLP_ALL_FLAGS               0x0007

#define XLogPageHeaderSize(hdr)     \
    (((hdr)->xlp_info & XLP_LONG_HEADER) ? SizeOfXLogLongPHD : SizeOfXLogShortPHD)

XLogRecord
事务日志文件由N个的XLog Record组成,逻辑上对应XLOG Record这一概念的数据结构是XLogRecord.
XLOG Record的整体布局如下:
头部数据(固定大小的XLogRecord结构体)
XLogRecordBlockHeader 结构体
XLogRecordBlockHeader 结构体

XLogRecordDataHeader[Short|Long] 结构体
block data
block data

main data
XLOG Record按存储的数据内容来划分,大体可以分为三类:
1.Record for backup block:存储full-write-page的block,这种类型Record的目的是为了解决page部分写的问题;
2.Record for (tuple)data block:在full-write-page后,相应的page中的tuple变更,使用这种类型的Record记录;
3.Record for Checkpoint:在checkpoint发生时,在事务日志文件中记录checkpoint信息(其中包括Redo point).

/*
 * The overall layout of an XLOG record is:
 *      Fixed-size header (XLogRecord struct)
 *      XLogRecordBlockHeader struct
 *      XLogRecordBlockHeader struct
 *      ...
 *      XLogRecordDataHeader[Short|Long] struct
 *      block data
 *      block data
 *      ...
 *      main data
 * XLOG record的整体布局如下:
 *      固定大小的头部(XLogRecord 结构体)
 *      XLogRecordBlockHeader 结构体
 *      XLogRecordBlockHeader 结构体
 *      ...
 *      XLogRecordDataHeader[Short|Long] 结构体
 *      block data
 *      block data
 *      ...
 *      main data
 *
 * There can be zero or more XLogRecordBlockHeaders, and 0 or more bytes of
 * rmgr-specific data not associated with a block.  XLogRecord structs
 * always start on MAXALIGN boundaries in the WAL files, but the rest of
 * the fields are not aligned.
 * 其中,XLogRecordBlockHeaders可能有0或者多个,与block无关的0或多个字节的rmgr-specific数据
 * XLogRecord通常在WAL文件的MAXALIGN边界起写入,但后续的字段并没有对齐
 *
 * The XLogRecordBlockHeader, XLogRecordDataHeaderShort and
 * XLogRecordDataHeaderLong structs all begin with a single 'id' byte. It's
 * used to distinguish between block references, and the main data structs.
 * XLogRecordBlockHeader/XLogRecordDataHeaderShort/XLogRecordDataHeaderLong开头是占用1个字节的"id".
 * 用于区分block引用和main data结构体.
 */
typedef struct XLogRecord
{
    //record的大小
    uint32      xl_tot_len;     /* total len of entire record */
    //xact id
    TransactionId xl_xid;       /* xact id */
    //指向log中的前一条记录
    XLogRecPtr  xl_prev;        /* ptr to previous record in log */
    //标识位,详见下面的说明
    uint8       xl_info;        /* flag bits, see below */
    //该记录的资源管理器
    RmgrId      xl_rmid;        /* resource manager for this record */
    /* 2 bytes of padding here, initialize to zero */
    //2个字节的crc校验位,初始化为0
    pg_crc32c   xl_crc;         /* CRC for this record */

    /* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
    //接下来是XLogRecordBlockHeaders和XLogRecordDataHeader
} XLogRecord;
//宏定义:XLogRecord大小
#define SizeOfXLogRecord    (offsetof(XLogRecord, xl_crc) + sizeof(pg_crc32c))

/*
 * The high 4 bits in xl_info may be used freely by rmgr. The
 * XLR_SPECIAL_REL_UPDATE and XLR_CHECK_CONSISTENCY bits can be passed by
 * XLogInsert caller. The rest are set internally by XLogInsert.
 * xl_info的高4位由rmgr自由使用.
 * XLR_SPECIAL_REL_UPDATE和XLR_CHECK_CONSISTENCY由XLogInsert函数的调用者传入.
 * 其余由XLogInsert内部使用.
 */
#define XLR_INFO_MASK           0x0F
#define XLR_RMGR_INFO_MASK      0xF0

/*
 * If a WAL record modifies any relation files, in ways not covered by the
 * usual block references, this flag is set. This is not used for anything
 * by PostgreSQL itself, but it allows external tools that read WAL and keep
 * track of modified blocks to recognize such special record types.
 * 如果WAL记录使用特殊的方式(不涉及通常块引用)更新了关系的存储文件,设置此标记.
 * PostgreSQL本身并不使用这种方法,但它允许外部工具读取WAL并跟踪修改后的块,
 *   以识别这种特殊的记录类型。
 */
#define XLR_SPECIAL_REL_UPDATE  0x01

/*
 * Enforces consistency checks of replayed WAL at recovery. If enabled,
 * each record will log a full-page write for each block modified by the
 * record and will reuse it afterwards for consistency checks. The caller
 * of XLogInsert can use this value if necessary, but if
 * wal_consistency_checking is enabled for a rmgr this is set unconditionally.
 * 在恢复时强制执行一致性检查.
 * 如启用此功能,每个记录将为记录修改的每个块记录一个完整的页面写操作,并在以后重用它进行一致性检查。
 * 在需要时,XLogInsert的调用者可使用此标记,但如果rmgr启用了wal_consistency_checking,
 *   则会无条件执行一致性检查.
 */
#define XLR_CHECK_CONSISTENCY   0x02


/*
 * Header info for block data appended to an XLOG record.
 * 追加到XLOG record中block data的头部信息
 *
 * 'data_length' is the length of the rmgr-specific payload data associated
 * with this block. It does not include the possible full page image, nor
 * XLogRecordBlockHeader struct itself.
 * 'data_length'是与此块关联的rmgr特定payload data的长度。
 * 它不包括可能的full page image,也不包括XLogRecordBlockHeader结构体本身。
 *
 * Note that we don't attempt to align the XLogRecordBlockHeader struct!
 * So, the struct must be copied to aligned local storage before use.
 * 注意:我们不打算尝试对齐XLogRecordBlockHeader结构体!
 * 因此,在使用前,XLogRecordBlockHeader必须拷贝到一队齐的本地存储中.
 */
typedef struct XLogRecordBlockHeader
{
    //块引用ID
    uint8       id;             /* block reference ID */
    //在关系中使用的fork和flags
    uint8       fork_flags;     /* fork within the relation, and flags */
    //payload字节大小
    uint16      data_length;    /* number of payload bytes (not including page
                                 * image) */

    /* If BKPBLOCK_HAS_IMAGE, an XLogRecordBlockImageHeader struct follows */
    //如BKPBLOCK_HAS_IMAGE,后续为XLogRecordBlockImageHeader结构体
    /* If BKPBLOCK_SAME_REL is not set, a RelFileNode follows */
    //如BKPBLOCK_SAME_REL没有设置,则为RelFileNode
    /* BlockNumber follows */
    //后续为BlockNumber
} XLogRecordBlockHeader;
 
#define SizeOfXLogRecordBlockHeader (offsetof(XLogRecordBlockHeader, data_length) + sizeof(uint16))

/*
 * Additional header information when a full-page image is included
 * (i.e. when BKPBLOCK_HAS_IMAGE is set).
 * 当包含完整页图像时(即当设置BKPBLOCK_HAS_IMAGE时),附加的头部信息。
 *
 * The XLOG code is aware that PG data pages usually contain an unused "hole"
 * in the middle, which contains only zero bytes.  Since we know that the
 * "hole" is all zeros, we remove it from the stored data (and it's not counted
 * in the XLOG record's CRC, either).  Hence, the amount of block data actually
 * present is (BLCKSZ - <length of "hole" bytes>).
 * XLOG代码知道PG数据页通常在中间包含一个未使用的“hole”(空闲空间),
 *   大小为零字节。
 * 因为我们知道“hole”都是零,
 *   以我们从存储的数据中删除它(而且它也没有被计入XLOG记录的CRC中)。
 * 因此,实际呈现的块数据量为(BLCKSZ - <“hole”的大小>)。
 *
 * Additionally, when wal_compression is enabled, we will try to compress full
 * page images using the PGLZ compression algorithm, after removing the "hole".
 * This can reduce the WAL volume, but at some extra cost of CPU spent
 * on the compression during WAL logging. In this case, since the "hole"
 * length cannot be calculated by subtracting the number of page image bytes
 * from BLCKSZ, basically it needs to be stored as an extra information.
 * But when no "hole" exists, we can assume that the "hole" length is zero
 * and no such an extra information needs to be stored. Note that
 * the original version of page image is stored in WAL instead of the
 * compressed one if the number of bytes saved by compression is less than
 * the length of extra information. Hence, when a page image is successfully
 * compressed, the amount of block data actually present is less than
 * BLCKSZ - the length of "hole" bytes - the length of extra information.
 * 另外,在启用wal_compression时,会在去掉“hole”后,尝试使用PGLZ压缩算法压缩full page image。
 * 这可以简化WAL大小,但会增加额外的解压缩CPU时间.
 * 在这种情况下,由于“hole”的长度不能通过从BLCKSZ中减去page image字节数来计算,
 *   所以它基本上需要作为额外的信息来存储。
 * 但如果"hole"不存在,我们可以假设"hole"的大小为0,不需要存储额外的信息.
 * 请注意,如果压缩节省的字节数小于额外信息的长度,
 *   那么page image的原始版本存储在WAL中,而不是压缩后的版本。
 * 因此,当一个page image被成功压缩时,
 *   实际的块数据量小于BLCKSZ - “hole”的大小 - 额外信息的大小。
 */
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.
     * 如标记BKPIMAGE_HAS_HOLE和BKPIMAGE_IS_COMPRESSED设置,则后跟XLogRecordBlockCompressHeader
     */
} XLogRecordBlockImageHeader;

#define SizeOfXLogRecordBlockImageHeader    \
    (offsetof(XLogRecordBlockImageHeader, bimg_info) + sizeof(uint8))

/* Information stored in bimg_info */
//------------ bimg_info标记位
//存在"hole"
#define BKPIMAGE_HAS_HOLE       0x01    /* page image has "hole" */
//压缩存储
#define BKPIMAGE_IS_COMPRESSED      0x02    /* page image is compressed */
//在回放时,page image需要恢复
#define BKPIMAGE_APPLY      0x04    /* page image should be restored during
                                     * replay */

/*
 * Extra header information used when page image has "hole" and
 * is compressed.
 * page image存在"hole"和压缩存储时,额外的头部信息
 */
typedef struct XLogRecordBlockCompressHeader
{
    //"hole"的大小
    uint16      hole_length;    /* number of bytes in "hole" */
} XLogRecordBlockCompressHeader;

#define SizeOfXLogRecordBlockCompressHeader \
    sizeof(XLogRecordBlockCompressHeader)

/*
 * Maximum size of the header for a block reference. This is used to size a
 * temporary buffer for constructing the header.
 * 块引用的header的最大大小。
 * 它用于设置用于构造头部临时缓冲区的大小。
 */
#define MaxSizeOfXLogRecordBlockHeader \
    (SizeOfXLogRecordBlockHeader + \
     SizeOfXLogRecordBlockImageHeader + \
     SizeOfXLogRecordBlockCompressHeader + \
     sizeof(RelFileNode) + \
     sizeof(BlockNumber))

/*
 * The fork number fits in the lower 4 bits in the fork_flags field. The upper
 * bits are used for flags.
 * fork号适合于fork_flags字段的低4位。
 * 高4位用于标记。
 */
#define BKPBLOCK_FORK_MASK  0x0F
#define BKPBLOCK_FLAG_MASK  0xF0
//块数据是XLogRecordBlockImage
#define BKPBLOCK_HAS_IMAGE  0x10    /* block data is an XLogRecordBlockImage */
#define BKPBLOCK_HAS_DATA   0x20
//重做时重新初始化page
#define BKPBLOCK_WILL_INIT  0x40    /* redo will re-init the page */
//重做时重新初始化page,但会省略RelFileNode
#define BKPBLOCK_SAME_REL   0x80    /* RelFileNode omitted, same as previous */

/*
 * XLogRecordDataHeaderShort/Long are used for the "main data" portion of
 * the record. If the length of the data is less than 256 bytes, the short
 * form is used, with a single byte to hold the length. Otherwise the long
 * form is used.
 * XLogRecordDataHeaderShort/Long用于记录的“main data”部分。
 * 如果数据的长度小于256字节,则使用短格式,用一个字节保存长度。
 * 否则使用长形式。
 *
 * (These structs are currently not used in the code, they are here just for
 * documentation purposes).
 * (这些结构体不会再代码中使用,在这里是为了文档记录的目的)
 */
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 */
    //接下来是无符号32位整型的data_length(未对齐)
}           XLogRecordDataHeaderLong;

#define SizeOfXLogRecordDataHeaderLong (sizeof(uint8) + sizeof(uint32))

/*
 * Block IDs used to distinguish different kinds of record fragments. Block
 * references are numbered from 0 to XLR_MAX_BLOCK_ID. A rmgr is free to use
 * any ID number in that range (although you should stick to small numbers,
 * because the WAL machinery is optimized for that case). A couple of ID
 * numbers are reserved to denote the "main" data portion of the record.
 * 块id用于区分不同类型的记录片段。
 * 块引用编号从0到XLR_MAX_BLOCK_ID。
 * rmgr可以自由使用该范围内的任何ID号
 *   (尽管您应该坚持使用较小的数字,因为WAL机制针对这种情况进行了优化)。
 * 保留两个ID号来表示记录的“main”数据部分。
 *
 * The maximum is currently set at 32, quite arbitrarily. Most records only
 * need a handful of block references, but there are a few exceptions that
 * need more.
 * 目前的最大值是32,非常随意。
 * 大多数记录只需要少数块引用,但也有少数例外需要更多。
 */
#define XLR_MAX_BLOCK_ID            32

#define XLR_BLOCK_ID_DATA_SHORT     255
#define XLR_BLOCK_ID_DATA_LONG      254
#define XLR_BLOCK_ID_ORIGIN         253

#endif                          /* XLOGRECORD_H */

3.3 REDO point

REDO point是PostgreSQL启动恢复的起始点,也就是说REDO point是最后一次checkpoint启动时写XLOG Record的位置.
checkpointer进程启动时,从pg_control文件中获取Redo point并存储在内存中,在执行checkpoint时更新Redo point为当前即将写XLOG Record的位置,checkpoint执行成功后把Redo point更新到pg_control文件中.

[postgres@PGserver2 pg_wal]$ pg_controldata
pg_control version number:            1002
Catalog version number:               201707211
Database system identifier:           7001516193260068418
Database cluster state:               in production
pg_control last modified:             Sun 05 Sep 2021 01:02:54 AM CST
Latest checkpoint location:           0/1929140
Prior checkpoint location:            0/190CA08
Latest checkpoint's REDO location:    0/1929108
Latest checkpoint's REDO WAL file:    000000010000000000000001
Latest checkpoint's TimeLineID:       1
Latest checkpoint's PrevTimeLineID:   1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:966
Latest checkpoint's NextOID:          24787
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        548
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  966
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Sun 05 Sep 2021 01:02:51 AM CST
Fake LSN counter for unlogged rels:   0/1
Minimum recovery ending location:     0/0
Min recovery ending loc's timeline:   0
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica
wal_log_hints setting:                off
max_connections setting:              100
max_worker_processes setting:         8
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  8192
Blocks per segment of large relation: 131072
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        1996
Size of a large-object chunk:         2048
Date/time type storage:               64-bit integers
Float4 argument passing:              by value
Float8 argument passing:              by value
Data page checksum version:           0
Mock authentication nonce:            ece979b5fdfe0c74715d4c103840642801fd92322576b6faf15b0507c4062d1d
[postgres@PGserver2 pg_wal]$ pg_controldata |grep 'REDO'
Latest checkpoint's REDO location:    0/1929108
Latest checkpoint's REDO WAL file:    000000010000000000000001
[postgres@PGserver2 pg_wal]$

3.4 full-page-write的机制

考察以下的情况(为方便起见,省略了buffer等相关的信息):

img

在T1,数据库成功执行checkpoint;
在T2,执行DML语句,这时候相关的数据会写入到WAL中(此处忽略了WAL buffer);
在T3,提交该事务;
在T4,bgwriter把dirty pages写入到Data file中,但在写入过程中机器出现故障导致Crash(如掉电等),出现了部分写的情况。
为了应对这种情况,PG在T2写入WAL的时候,会把出现变化的page整页写入到WAL中,而不仅仅是tuple data。在数据库重启执行恢复 的时候,在Redo point开始回放WAL时,如发现XLOG Record是FPI(full-page-image),则整页替换,通过这种机制解决了部分写的问题。

由于整页写,不可避免的出现冗余数据;考虑这么一种情况:如果数据库很繁忙,而且数据的热点分散在不同的table上,同时checkpoint执行间隔较短,那非常多的page就会通过full-page-write写入的WAL中,导致日志空间快速膨胀。在极端情况下,page“满载”(基本没有空闲空间)的情况下更新其中一条记录都会导致整页写入WAL。《如何遏制PostgreSQL WAL的疯狂增长》

4 Postgresql 检查点

4.1 检查点恢复

img

当执行checkpoint后,会确保检查点开始时所有脏的缓冲区都刷到磁盘上,此时checkpoint执行完成。后续我们可以使用checkpoint记录的开始时间作为开始恢复的点。

img

​ checkpoint执行后会带来大量的页面写入操作,为了避免大量的页面写入对I/O造成冲击,在检查点期间写入脏缓冲区的过程会分散为一段时间。该周期由checkpoint_completion_target控制,它是检查点间隔的一部分,默认为0.5,也就是说每个checkpoint需要在checkpoints间隔时间的50%内完成。

​ 通常checkpoint_completion_target值会增加到 0.9 以获得更好的均匀性,PostgreSQL 14版本中会将默认值修改为0.9。

4.2 检查点执行过程

1、首先会对缓冲区中的脏页进行标记

img

2、然后检查指针遍历所有缓存并将标记的脏页刷新到磁盘(页面不会从缓存中逐出,而只会写入磁盘)。

3、新的脏缓冲区不会被标记,并且检查指针不会写入它们。

img

4、创建检查点结束的 WAL 记录,该记录包含检查点开始时间的 LSN。

5、更新控制文件信息($PGDATA/global/pg_control)。

img

4.3 测试过程

1、创建一个表;它的页面将进入缓冲区缓存并变脏:

CREATE EXTENSION pg_buffercache;
CREATE TABLE chkpt AS SELECT * FROM generate_series(1,10000) AS g(n);
SELECT count(*) FROM pg_buffercache WHERE isdirty;

postgres@[local]:5432=#CREATE EXTENSION pg_buffercache;
CREATE EXTENSION
postgres@[local]:5432=#CREATE TABLE chkpt AS SELECT * FROM generate_series(1,10000) AS g(n);
SELECT 10000
postgres@[local]:5432=#SELECT count(*) FROM pg_buffercache WHERE isdirty;
 count
-------
    88
(1 row)

2、记住当前的 WAL 位置

SELECT pg_current_wal_insert_lsn();
postgres@[local]:5432=#SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 0/19F22E0
(1 row)


3、手动执行检查点操作

postgres@[local]:5432=#CHECKPOINT;
CHECKPOINT
postgres@[local]:5432=#SELECT count(*) FROM pg_buffercache WHERE isdirty;
 count
-------
     0
(1 row)

4、看看检查点是如何反映在 WAL 中的

postgres@[local]:5432=#SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 0/19F23C0
(1 row)


5 、可以看到lsn有更新,接下来用pg_waldump查看wal日志记录了哪些内容

查看当前lsn对应的wal日志文件

SELECT file_name, upper(to_hex(file_offset)) file_offset FROM pg_walfile_name_offset('0/19F23C0');
postgres@[local]:5432=#SELECT file_name, upper(to_hex(file_offset)) file_offset FROM pg_walfile_name_offset('0/19F23C0');
        file_name         | file_offset
--------------------------+-------------
 000000010000000000000001 | 9F23C0
(1 row)

6、pg_waldump 查看

pg_waldump -p /data/pg10.18/pgdata/pg_wal -s 0/19F22E0  -e 0/19F23C0  000000010000000000000001

postgres@PGserver2 pg_wal]$ pg_waldump -p /data/pg10.18/pgdata/pg_wal -s 0/19F22E0  -e 0/19F23C0  000000010000000000000001
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/019F22E0, prev 0/019F22A8, desc: RUNNING_XACTS nextXid 969 latestCompletedXid 968 oldestRunningXid 969
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 0/019F2318, prev 0/019F22E0, desc: CHECKPOINT_ONLINE redo 0/19F22E0; tli 1; prev tli 1; fpw true; xid 0:969; oid 24787; multi 1; offset 0; oldest xid 548 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 969; online
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/019F2388, prev 0/019F2318, desc: RUNNING_XACTS nextXid 969 latestCompletedXid 968 oldestRunningXid 969

可以看到日志记录了CHECKPOINT_ONLINE信息,checkpoint start的LSN在单词“redo”之后输出,这个位置对应checkpoint start时间最后一个WAL记录。
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 0/019F2318, prev 0/019F22E0, desc: CHECKPOINT_ONLINE redo 0/19F22E0; tli 1; prev tli 1; fpw true; xid 0:969; oid 24787; multi 1; offset 0; oldest xid 548 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 969; online

7、查看控制文件信息,可知控制文件记录了最近的checkpoint点对应的lsn信息

pg_controldata -D /data/pg10.18/pgdata |egrep 'Latest.*location'

[postgres@PGserver2 pg_wal]$ pg_controldata -D /data/pg10.18/pgdata |egrep 'Latest.*location'
Latest checkpoint location:           0/19F2318
Latest checkpoint's REDO location:    0/19F22E0
[postgres@PGserver2 pg_wal]$

8、模拟故障

1、直接kill掉postgres主进程
[postgres@PGserver2 pg_wal]$ ps -ef|grep postgres
root      4322  4249  0 09:52 pts/1    00:00:00 su - postgres
postgres  4323  4322  0 09:52 pts/1    00:00:00 -bash
postgres  4345  4323  0 09:52 pts/1    00:00:00 psql
postgres  4346 22718  0 09:52 ?        00:00:00 postgres: postgres postgres [local] idle
root      4461  4440  0 09:54 pts/2    00:00:00 su - postgres
postgres  4463  4461  0 09:54 pts/2    00:00:00 -bash
postgres 10250  4463  0 11:35 pts/2    00:00:00 ps -ef
postgres 10251  4463  0 11:35 pts/2    00:00:00 grep --color=auto postgres
postgres 13126     1  0 Sep06 ?        00:00:02 gpg-agent --daemon --use-standard-socket
postgres 22718     1  0 Sep06 ?        00:00:03 /data/pg10.18/bin/postgres -D /data/pg10.18/pgdata
postgres 22719 22718  0 Sep06 ?        00:00:00 postgres: logger process
postgres 22721 22718  0 Sep06 ?        00:00:00 postgres: checkpointer process
postgres 22722 22718  0 Sep06 ?        00:00:01 postgres: writer process
postgres 22723 22718  0 Sep06 ?        00:00:01 postgres: wal writer process
postgres 22724 22718  0 Sep06 ?        00:00:05 postgres: autovacuum launcher process
postgres 22725 22718  0 Sep06 ?        00:00:08 postgres: stats collector process
postgres 22726 22718  0 Sep06 ?        00:00:00 postgres: bgworker: logical replication launcher
postgres 29855     1  0 Sep06 ?        00:00:00 /data/pg12tde/bin/postgres -D /data/pg12tde/pgdata
postgres 29860 29855  0 Sep06 ?        00:00:00 postgres: checkpointer
postgres 29861 29855  0 Sep06 ?        00:00:01 postgres: background writer
postgres 29862 29855  0 Sep06 ?        00:00:01 postgres: walwriter
postgres 29863 29855  0 Sep06 ?        00:00:00 postgres: autovacuum launcher
postgres 29864 29855  0 Sep06 ?        00:00:01 postgres: stats collector
postgres 29865 29855  0 Sep06 ?        00:00:00 postgres: logical replication launcher
root     31643 31615  0 08:25 pts/0    00:00:00 su - postgres
postgres 31644 31643  0 08:25 pts/0    00:00:00 -bash
postgres 31666 31644  0 08:25 pts/0    00:00:00 psql
postgres 31667 22718  0 08:25 ?        00:00:00 postgres: postgres postgres [local] idle
[postgres@PGserver2 pg_wal]$ kill -9 22718

2、查看控制文件Database cluster state状态,没有发生变化

pg_controldata -D /data/pg10.18/pgdata |grep state
--------
Database cluster state:               in production
3、启动数据库,查看日志

[postgres@PGserver2 pg_wal]$ pg_ctl -D $PGDATA start
pg_ctl: another server might be running; trying to start server anyway
waiting for server to start....2021-09-07 11:36:39 CST [10318]: user=,db=,app=,client= LOG:  listening on IPv4 address "0.0.0.0", port 5432
2021-09-07 11:36:39 CST [10318]: user=,db=,app=,client= LOG:  listening on IPv6 address "::", port 5432
2021-09-07 11:36:39 CST [10318]: user=,db=,app=,client= LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2021-09-07 11:36:39 CST [10318]: user=,db=,app=,client= LOG:  redirecting log output to logging collector process
2021-09-07 11:36:39 CST [10318]: user=,db=,app=,client= HINT:  Future log output will appear in directory "log".
 done
server started
[postgres@PGserver2 pg_wal]$


5 wal文件解析工具-pg_waldump

把PG数据库集群的 wal 日志翻译成人为可阅读的信息,该工具要求访问数据库 data 目录权限,主要用于展示和debug的目的

[postgres@PGserver2 pg_wal]$ pg_dump --help
pg_dump dumps a database as a text file or to other formats.

Usage:
  pg_dump [OPTION]... [DBNAME]

General options:
  -f, --file=FILENAME          output file or directory name
  -F, --format=c|d|t|p         output file format (custom, directory, tar,
                               plain text (default))
  -j, --jobs=NUM               use this many parallel jobs to dump
  -v, --verbose                verbose mode
  -V, --version                output version information, then exit
  -Z, --compress=0-9           compression level for compressed formats
  --lock-wait-timeout=TIMEOUT  fail after waiting TIMEOUT for a table lock
  --no-sync                    do not wait for changes to be written safely to disk
  -?, --help                   show this help, then exit

Options controlling the output content:
  -a, --data-only              dump only the data, not the schema
  -b, --blobs                  include large objects in dump
  -B, --no-blobs               exclude large objects in dump
  -c, --clean                  clean (drop) database objects before recreating
  -C, --create                 include commands to create database in dump
  -E, --encoding=ENCODING      dump the data in encoding ENCODING
  -n, --schema=SCHEMA          dump the named schema(s) only
  -N, --exclude-schema=SCHEMA  do NOT dump the named schema(s)
  -o, --oids                   include OIDs in dump
  -O, --no-owner               skip restoration of object ownership in
                               plain-text format
  -s, --schema-only            dump only the schema, no data
  -S, --superuser=NAME         superuser user name to use in plain-text format
  -t, --table=TABLE            dump the named table(s) only
  -T, --exclude-table=TABLE    do NOT dump the named table(s)
  -x, --no-privileges          do not dump privileges (grant/revoke)
  --binary-upgrade             for use by upgrade utilities only
  --column-inserts             dump data as INSERT commands with column names
  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting
  --disable-triggers           disable triggers during data-only restore
  --enable-row-security        enable row security (dump only content user has
                               access to)
  --exclude-table-data=TABLE   do NOT dump data for the named table(s)
  --if-exists                  use IF EXISTS when dropping objects
  --inserts                    dump data as INSERT commands, rather than COPY
  --no-publications            do not dump publications
  --no-security-labels         do not dump security label assignments
  --no-subscriptions           do not dump subscriptions
  --no-synchronized-snapshots  do not use synchronized snapshots in parallel jobs
  --no-tablespaces             do not dump tablespace assignments
  --no-unlogged-table-data     do not dump unlogged table data
  --quote-all-identifiers      quote all identifiers, even if not key words
  --section=SECTION            dump named section (pre-data, data, or post-data)
  --serializable-deferrable    wait until the dump can run without anomalies
  --snapshot=SNAPSHOT          use given snapshot for the dump
  --strict-names               require table and/or schema include patterns to
                               match at least one entity each
  --use-set-session-authorization
                               use SET SESSION AUTHORIZATION commands instead of
                               ALTER OWNER commands to set ownership

Connection options:
  -d, --dbname=DBNAME      database to dump
  -h, --host=HOSTNAME      database server host or socket directory
  -p, --port=PORT          database server port number
  -U, --username=NAME      connect as specified database user
  -w, --no-password        never prompt for password
  -W, --password           force password prompt (should happen automatically)
  --role=ROLENAME          do SET ROLE before dump

If no database name is supplied, then the PGDATABASE environment
variable value is used.

Report bugs to <pgsql-bugs@postgresql.org>.

Options:
  -b, --bkp-details      输出有关备份块的细节。
  -e, --end=RECPTR       在指定的日志位置停止读取,而不是一直读取到日志流的末尾。
  -f, --follow           在到达可用 WAL 的末尾之后,保持每秒轮询一次是否有新的 WAL 出现。
  -n, --limit=N          显示指定数量的记录,然后停止。
  -p, --path=PATH        要在哪个目录中寻找日志段文件。默认是在当前目录的pg_xlog 子目录中搜索。
  -r, --rmgr=RMGR        只显示由指定资源管理器生成的记录。如果把list作为资源管理器名称 传递给这个选项,则打印出可用资源管理器名称的列表然后退出。
  -s, --start=RECPTR     要从哪个日志位置开始读取。默认是从找到的最早的文件的第一个可用日志记录开始。
  -t, --timeline=TLI     要从哪个时间线读取日志记录。默认是使用startseg(如果指定) 中的值,否则默认为 1
  -V, --version          打印pg_xlogdump版本并且退出。
  -x, --xid=XID         只显示用给定事务 ID 标记的记录。
  -z, --stats[=record]   显示概括统计信息(记录的数量和尺寸以及全页镜像)而不是显示 每个记录。可以选择针对每个记录生成统计信息,而不是针对每个 资源管理器生成。
  -?, --help             show this help, then exit
  
  
  • 展示 wal 中所有的资源管理名称
pg_waldump --rmgr=list
[postgres@PGserver2 pg_wal]$ pg_waldump --rmgr=list
XLOG
Transaction
Storage
CLOG
Database
Tablespace
MultiXact
RelMap
Standby
Heap2
Heap
Btree
Hash
Gin
Gist
Sequence
SPGist
BRIN
CommitTs
ReplicationOrigin
Generic
LogicalMessage

具体的官方文档:https://www.postgresql.org/docs/current/pgwaldump.html

6 WAL的归档配置

max_wal_size = 1GB
min_wal_size = 80MB
max_wal_size (integer)
在自动 WAL 检查点之间允许 WAL 增长到的最大尺寸。这是一个软限制, 在特殊的情况下 WAL 尺寸可能会超过max_wal_size, 例如在重度负荷下、archive_command失败或者高的 wal_keep_segments设置。如果指定值时没有单位,则以兆字节为单位。默认为 1GB。增加这个参数 可能导致崩溃恢复所需的时间。这个参数只能在postgresql.conf 或者服务器命令行中设置。
min_wal_size (integer)
只要 WAL 磁盘用量保持在这个设置之下,在检查点时旧的 WAL 文件总是 被回收以便未来使用,而不是直接被删除。这可以被用来确保有足够的 WAL 空间被保留来应付 WAL 使用的高峰,例如运行大型的批处理任务。 如果指定值时没有单位,则以兆字节为单位。默认是 80 MB。这个参数只能在postgresql.conf 或者服务器命令行中设置。



archive_mode = on
上述参数为on,表示打开归档备份,可选的参数为on,off,always 默认值为off,所以要手动打开

archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
该参数的默认值是一个空字符串,他的值可以是一条shell命令或者一个复杂的shell脚本。在shell脚本或命令中可以用 “%p” 表示将要归档的wal文件包含完整路径的信息的文件名,用“%f” 代表不包含路径信息的wal文件的文件名



archive_status下会有已经完成归档的日志信息


7 参考引用

作者:EthanHe

https://www.jianshu.com/p/b9087d9f20e2

瀚高HGDB 禹晓@PGConf.Asia 2020

https://www.modb.pro/course/play/91?lsId=2997

https://www.cnblogs.com/mingfan/p/14829511.html

《PostgreSQL指南_内幕探秘 》

https://zhuanlan.zhihu.com/p/166413747

https://www.cnblogs.com/mingfan/p/14829511.html

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论