9.4 WAL记录的内部布局

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

9.4.1 WAL记录首部部分

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

typedef struct XLogRecord
{
   uint32          xl_tot_len;   /* 整条记录的全长 */
   TransactionId   xl_xid;       /* 事务ID */
   uint32          xl_len;       /* 资源管理器的数据长度 */
   uint8           xl_info;      /* 标记位,如下所示 */
   RmgrId          xl_rmid;      /* 本记录的资源管理器 */
   /* 这里有2字节的填充,初始化为0 */
   XLogRecPtr      xl_prev;      /* 在日志中指向先前记录的指针 */
   pg_crc32        xl_crc;       /* 本记录的CRC */
} XLogRecord;

除了两个变量,大多数变量的意思非常明显,无需多言。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记录的格式,省了几个字节。

typedef struct XLogRecord
{
    uint32        xl_tot_len;        /* 整条记录的总长度 */
    TransactionId xl_xid;        /* 事物标识 xid */
    XLogRecPtr    xl_prev;        /* 指向日志中前一条记录的指针 */
    uint8        xl_info;        /* 标记位,详情见下*/
    RmgrId        xl_rmid;        /* 本条记录对应的资源管理器 */
    /* 这里有2字节的填充,初始化为0 */
    pg_crc32c    xl_crc;            /* 本记录的CRC */
    /* 紧随其后的是XLogRecordBlockHeaders 与 XLogRecordDataHeader ,不带填充 */
} XLogRecord;

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版本或更早)图9.8 XLOG记录的样例(9.4版本或更早)

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

9.4.2.1 备份区块

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

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

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

# @include/access/xlog_internal.h
typedef struct BkpBlock 
{
  RelFileNode node;        /* 包含该块的关系 */
  ForkNumber  fork;        /* 关系的分支(main,vm,fsm,...) */
  BlockNumber block;       /* 区块号 */
  uint16      hole_offset; /* "空洞"前的字节数 */
  uint16      hole_length; /* "空洞"的长度 */
  /* 实际的区块数据紧随该结构体后 */
} BkpBlock;

9.4.2.2 非备份区块

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

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

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

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;             /* 表空间 */
   Oid             dbNode;              /* 数据库 */
   Oid             relNode;             /* 关系 */
} RelFileNode;
typedef struct xl_heaptid
{
   RelFileNode     node;                /* 关系定位符 */
   ItemPointerData tid;                 /* 元组在关系中的位置 */
} xl_heaptid;
typedef struct xl_heap_insert
{
   xl_heaptid      target;              /* 被插入的元组ID */
   bool            all_visible_cleared; /* PD_ALL_VISIBLE 是否被清除 */
} xl_heap_insert;

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

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

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

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

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记录格式图9.9 通用XLOG记录格式

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

/* 追加写入XLOG记录的区块数据首部。
 * 'data_length'是与本区块关联的,特定于资源管理器的数据荷载长度。它不包括可能会出现
 * 的整页镜像的长度,也不会包括XLogRecordBlockHeader结构本身。注意我们并不会对
 * XLogRecordBlockHeader结构做边界对齐!因此在使用前该结构体必须拷贝到对齐的本地存储中。
 */
typedef struct XLogRecordBlockHeader
{
    uint8        id;                /* 块引用 ID */
    uint8        fork_flags;        /* 关系中的分支,以及标志位 */
    uint16        data_length;    /* 荷载字节数(不包括页面镜像) */
    /* 如果设置 BKPBLOCK_HAS_IMAGE, 紧接一个XLogRecordBlockImageHeader结构 */
    /* 如果未设置 BKPBLOCK_SAME_REL, 紧接着一个RelFileNode结构 */
    /* 紧接着区块号码 */
} XLogRecordBlockHeader;
/* 分支标号放在fork_flags的低4位中,高位用于标记位 */
#define BKPBLOCK_FORK_MASK    0x0F
#define BKPBLOCK_FLAG_MASK    0xF0
#define BKPBLOCK_HAS_IMAGE    0x10    /* 区块数据是一个XLogRecordBlockImage */
#define BKPBLOCK_HAS_DATA    0x20
#define BKPBLOCK_WILL_INIT    0x40    /* 重做会重新初始化当前页 */
#define BKPBLOCK_SAME_REL    0x80    /* 忽略RelFileNode,与前一个相同 */
/* XLogRecordDataHeaderShort/Long 被用于本记录的“主数据”部分。如果数据的长度小于256字节
 * 则会使用Short版本的格式,即使用单个字节来保存长度,否则会使用长版本的格式。 */
typedef struct XLogRecordDataHeaderShort
{
    uint8        id;                /* XLR_BLOCK_ID_DATA_SHORT */
    uint8        data_length;    /* 载荷字节数目 */
}            XLogRecordDataHeaderShort;
#define SizeOfXLogRecordDataHeaderShort (sizeof(uint8) * 2)
typedef struct XLogRecordDataHeaderLong
{
    uint8        id;                /* XLR_BLOCK_ID_DATA_LONG */
    /* 紧随其后的是uint32类型的data_length, 未对齐 */
}            XLogRecordDataHeaderLong;
/* 当包含整页镜像时额外的首部信息(即当BKPBLOCK_HAS_IMAGE标记位被设置时)。
 * 
 * XLOG相关的代码会意识到数据压缩上一个显而易见的情况,即PG里的数据页面通常会在中间包含一个
 * 未使用的“空洞”,空洞里面通常只有置零的字节。如果空洞的长度大于0,我们就会从存储的数据中
 * 移除该“空洞”(且XLOG记录的CRC也不会计算该空洞)。因此区块数据的总量实际上是块大小BLCKSZ
 * 减去空洞包含的字节大小。
 * 
 * 当启用 wal_compression 时,一个包含空洞的整页镜像除了移除空洞,还会额外是引用PGLZ压缩
 * 算法进行压缩。这能减小WAL日志的体积,但会增加在记录WAL日志过程中的CPU开销。在这种情况下,
 * 空洞的大小就无法通过块大小-页面镜像大小来计算了。基本上,这需要存储额外的信息。但当没有
 * 空洞存在时,我们可以假设空洞大小为0,因此就不需要存储额外信息了。注意,当压缩节约的字节数
 * 小于额外信息的长度时,WAL里就会存储原始的页面镜像,而不是压缩过的版本。因此当成功进行压缩
 * 时,区块数据的总量总是要比(块大小BLCKSZ - 空洞字节数 - 额外信息长度)要更小。
 */
typedef struct XLogRecordBlockImageHeader
{
    uint16        length;            /* 页面镜像的字节数 */
    uint16        hole_offset;    /* 空洞前面的字节数 */
    uint8        bimg_info;        /* 标记位,详情见下 */
    /* 如果 BKPIMAGE_HAS_HOLE 且 BKPIMAGE_IS_COMPRESSED, 后面会跟着
     * XLogRecordBlockCompressHeader 结构体 */
} XLogRecordBlockImageHeader;
/* 当页面镜像含有“空洞”且被压缩时,会用到这里的额外首部信息 */
typedef struct XLogRecordBlockCompressHeader
{
    uint16        hole_length;    /* number of bytes in "hole" */
} 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及其后的版本)图9.10 XLOG记录样例(9.5及其后的版本)

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

9.4.3.1 备份区块

INSERT语句创建的备份区块如图9.10(a)所示,它由如下所示的四个数据结构与一个数据对象组成:

  1. XLogRecord结构 (首部部分)
  2. XLogRecordBlockHeader结构,且包含一个XLogRecordBlockImageHeader
  3. XLogRecordDataHeaderShort结构
  4. 一个备份区块(区块数据)
  5. xl_heap_insert结构 (主数据)

XLogRecordBlockHeader包含了用于在数据库集簇中定位区块的变量 (关系节点,分支编号,以及区块号); XLogRecordImageHeader 包含了当前区块的长度与偏移量(这两个首部结构合起来效果与9.4及先前版本中的BkpBlock结构相同)。

XLogRecordDataHeaderShort存储了xl_heap_insert结构的长度,该结构是当前记录的主数据部分(见下)。

除了某些特例外(例如逻辑解码与推测插入(speculative insertion) ),包含整页镜像的XLOG记录的主数据 不会被使用。它们会在记录重放时被忽略,属于冗余数据,未来可能会对其改进。

此外,备份区块记录的主数据与创建它们的语句相关。例如UPDATE语句就会追加写入xl_heap_lockxl_heap_updated

9.4.3.2 非备份区块

接下来描述由INSERT语句创建的非备份区块,如图9.10(b)所示,它由四个数据结构与一个数据对象组成:

  1. XLogRecord结构 (首部部分)
  2. XLogRecordBlockHeader结构
  3. XLogRecordDataHeaderShort结构
  4. 一条被插入的元组(更精确地说,一个xl_heap_header结构与完整的插入数据)
  5. xl_heap_insert结构 (主数据)

XLogRecordBlockHeader包含三个值 (关系节点,分支编号,以及区块号),用以指明该元组被插入到哪个区块中,以及要插入数据部分的长度。XLogRecordDataHeaderShort存储了xl_heap_insert结构的长度,该结构是当前记录的主数据部分。

新版本的xl_heap_insert仅包含两个值:当前元组在区块内的偏移量,以及一个可见性标记。该结构变得十分简单,因为XLogRecordBlockHeader存储了旧版本中该结构体的绝大多数数据。

/* 这里是关于该INSERT操作,我们所需要知道的一切 */
typedef struct xl_heap_insert
{
    OffsetNumber offnum;        /* 被插入元组的偏移量 */
    uint8        flags;
    /* xl_heap_header & 备份区块0中的元组数据 */
} xl_heap_insert;

最后一个例子,检查点的记录如图9.10(c)所示,它由三个数据结构组成:

  1. XLogRecord结构体(首部部分)
  2. XLogRecordDataHeaderShort结构,包含了主数据的长度。
  3. 结构体CheckPoint(主数据)

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

尽管对我们来说新格式稍显复杂,但它对于资源管理器的解析而言,设计更为合理,而且许多类型的XLOG记录的大小都比先前要小。主要的结构如图9.8和图9.10所示,你可以计算并相互比较这些记录的大小。(新版CheckPoint记录的尺寸要比旧版本大一些,但它也包含了更多的变量)。

下一节:完成了热身练习后,现在我们已经做好理解XLOG记录写入过程的准备了。因此在本节中,我将尽可能仔细地描述。首先,以下列语句的执行为例,让我们来看一看PostgreSQL的内幕。