一条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_rmid
与xl_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_rmid
与xl_info
会相应地被设置为RM_HEAP
与XLOG_HEAP_INSERT
。当恢复数据库集簇时,就会按照xl_info
选用资源管理器RM_HEAP
的函数heap_xlog_insert()
来重放当前XLOG记录。 UPDATE
语句与之类似,首部变量中的xl_info
会被设置为XLOG_HEAP_UPDATE
,而在数据库恢复时就会选用资源管理器RM_HEAP
的函数heap_xlog_update()
进行重放。- 当事务提交时,相应XLOG记录首部的变量
xl_rmid
与xl_info
会被相应地设置为RM_XACT
与XLOG_XACT_COMMIT
。当数据库恢复时,RM_XACT
的xact_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.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记录的数据部分可以分为两类:备份区块(完整的页面),或非备份区块(不同的操作相应的数据不同)。
图9.8 XLOG记录的样例(9.4版本或更早)
让我们通过几个具体示例来了解XLOG记录的内部布局。
9.4.2.1 备份区块
备份区块如图9.8(a)所示,它由两个数据结构和一个数据对象组成,如下所述:
- 首部部分,
XLogRecord
结构体 BkpBlock
结构体- 除去空闲空间的完整页面。
BkpBlock
包括了用于在数据库集簇目录中定位该页面的变量(比如,包含该页面的关系表的RelFileNode
与ForkNumber
,以及文件内的区块号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记录是由两个数据结构与一个数据对象组成的:
- 首部部分,
XLogRecord
结构体 xl_heap_insert
结构体- 被插入的元组 —— 更精确地说,是移除了一些字节的元组。
结构体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记录相当简单,它由如下所示的两个数据结构组成:
XLogRecord
结构(首部部分)- 包含检查点信息的
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记录格式
首部部分包含零个或多个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.4.3.1 备份区块
由INSERT
语句创建的备份区块如图9.10(a)所示,它由如下所示的四个数据结构与一个数据对象组成:
XLogRecord
结构 (首部部分)XLogRecordBlockHeader
结构,且包含一个XLogRecordBlockImageHeader
XLogRecordDataHeaderShort
结构- 一个备份区块(区块数据)
xl_heap_insert
结构 (主数据)
XLogRecordBlockHeader
包含了用于在数据库集簇中定位区块的变量 (关系节点,分支编号,以及区块号); XLogRecordImageHeader
包含了当前区块的长度与偏移量(这两个首部结构合起来效果与9.4及先前版本中的BkpBlock
结构相同)。
XLogRecordDataHeaderShort
存储了xl_heap_insert
结构的长度,该结构是当前记录的主数据部分(见下)。
除了某些特例外(例如逻辑解码与推测插入(speculative insertion) ),包含整页镜像的XLOG记录的主数据 不会被使用。它们会在记录重放时被忽略,属于冗余数据,未来可能会对其改进。
此外,备份区块记录的主数据与创建它们的语句相关。例如
UPDATE
语句就会追加写入xl_heap_lock
或xl_heap_updated
。
9.4.3.2 非备份区块
接下来描述由INSERT
语句创建的非备份区块,如图9.10(b)所示,它由四个数据结构与一个数据对象组成:
XLogRecord
结构 (首部部分)XLogRecordBlockHeader
结构XLogRecordDataHeaderShort
结构- 一条被插入的元组(更精确地说,一个
xl_heap_header
结构与完整的插入数据) 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)所示,它由三个数据结构组成:
XLogRecord
结构体(首部部分)XLogRecordDataHeaderShort
结构,包含了主数据的长度。- 结构体
CheckPoint
(主数据)
xl_heap_header
定义于src/include/access/htup.h
中,而CheckPoint
结构定义于src/include/catalog/pg_control.h
.
尽管对我们来说新格式稍显复杂,但它对于资源管理器的解析而言,设计更为合理,而且许多类型的XLOG记录的大小都比先前要小。主要的结构如图9.8和图9.10所示,你可以计算并相互比较这些记录的大小。(新版CheckPoint
记录的尺寸要比旧版本大一些,但它也包含了更多的变量)。
下一节:完成了热身练习后,现在我们已经做好理解XLOG记录写入过程的准备了。因此在本节中,我将尽可能仔细地描述。首先,以下列语句的执行为例,让我们来看一看PostgreSQL的内幕。