概括起来,XLOG日志分为多个XLOG逻辑日志文件,每个逻辑日志文件包含多个XLOG段文件,每个XLOG段文件包含多个XLOG日志页:
- 每个XLOG逻辑日志文件都有一个ID
- 即LSN中的逻辑日志文件号
- 实际XLOG被分为pg_xlog目录下多个大小为16MB的段文件
- 文件名由时间线TimeLineID(8位16进制)、逻辑日志文件号(8位16进制)和段文件ID(8位16进制)组成
- 每个段文件分为多个8KB的页(块)
- 每个页包含一个头部,头部信息之后才是真正的XLOG日志记录
其中,值得注意的是,每个XLOG段文件大小可以在编译时使用–with-wal-segsize参数来指定,每页的大小可以在编译的时使用–with-wal-blocksize参数来指定,接下来主要介绍XLOG日志每页的组织形式。
XLOG日志页的组织形式
在PostgreSQL中,XLOG日志页可以分为以下几部分:
组成部分 | 具体含义 |
---|---|
PageHeaderData | XLOG日志页面头部信息 |
XLogRecord | XLog日志记录的头部信息 |
Data of RMGR | 资源管理器的数据,长度xl_len |
Backup Block 0 | 备份数据块头部BkpBlock + 块大小的备份数据 |
Backup Block 1 | 备份数据块头部BkpBlock + 块大小的备份数据 |
Backup Block 2 | 备份数据块头部BkpBlock + 块大小的备份数据 |
Backup Block 3 | 备份数据块头部BkpBlock + 块大小的备份数据 |
XLOG日志页头部信息
每个XLOG日志页分为页面头部信息和日志记录,其头部信息XLogPageHeaderData结构如下:
typedef struct XLogPageHeaderData
{
uint16 xlp_magic; /* 校验位,用于识别不同的XLOG版本 */
uint16 xlp_info; /* flag bits, see below */
TimeLineID xlp_tli; /* 页面第一条记录的时间线 */
XLogRecPtr xlp_pageaddr; /* XLOG页面的首地址 */
uint32 xlp_rem_len; /* 前XLOG页面最后一条记录剩余的长度 */
} XLogPageHeaderData;
其中,xlp_info是标志位:
- 0x0001表示该页面包含一个跨页面的记录(上个页面的最后一条记录)
- 0x0002表示该页面为段文件的首个页面,头部是一个长头部
- 0x0004表示该页面备份数据块是可选的
如果当前的页面没有足够的空间来存储一个XLOG日志记录,系统允许将剩余的数据存储到下一个页面,但是XLog日志记录的头部信息,即后文中的XLogRecord是不允许分开存储到两个不同的页面的。
如果该页面为段文件的首个页面,除了上面的标准页面头部信息外,还增加一个长头部用来更精确地定位文件,即XLogLongPageHeaderData:
typedef struct XLogLongPageHeaderData
{
XLogPageHeaderData std; /* 标准页面头部信息 */
uint64 xlp_sysid; /* pg_control 中的系统标识符*/
uint32 xlp_seg_size; /* 段的尺寸 */
uint32 xlp_xlog_blcksz; /* 页(块)的尺寸*/
} XLogLongPageHeaderData;
XLOG日志记录的头部信息
每个XLOG日志页面头部之后才是真正的XLOG日志记录,XLogRecord记录了XLOG的相关数据信息,具体结构如下:
typedef struct XLogRecord
{
uint32 xl_tot_len; /* 整条记录的总长度*/
TransactionId xl_xid; /* 事务ID */
XLogRecPtr xl_prev; /* 上条XLOG日志记录的位置(LSN) */
uint8 xl_info; /* flag bits, see below */
RmgrId xl_rmid; /* 资源管理器ID */
/* 2 bytes of padding here, initialize to zero */
pg_crc32c xl_crc; /* 本记录的CRC校验码 */
/* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
} XLogRecord;
其中,xl_rmid表示资源管理器ID,在PostgreSQL中,资源管理器根据资源种类,可以分为17类,其分别的ID按照以下顺序分别为0-16:
PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, NULL, NULL)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, NULL, NULL)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, NULL, NULL)
PG_RMGR(RM_DBASE_ID, "Database", dbase_redo, dbase_desc, NULL, NULL)
PG_RMGR(RM_TBLSPC_ID, "Tablespace", tblspc_redo, tblspc_desc, NULL, NULL)
PG_RMGR(RM_MULTIXACT_ID, "MultiXact", multixact_redo, multixact_desc, NULL, NULL)
PG_RMGR(RM_RELMAP_ID, "RelMap", relmap_redo, relmap_desc, NULL, NULL)
PG_RMGR(RM_STANDBY_ID, "Standby", standby_redo, standby_desc, NULL, NULL)
PG_RMGR(RM_HEAP2_ID, "Heap2", heap2_redo, heap2_desc, NULL, NULL)
PG_RMGR(RM_HEAP_ID, "Heap", heap_redo, heap_desc, NULL, NULL)
PG_RMGR(RM_BTREE_ID, "Btree", btree_redo, btree_desc, NULL, NULL)
PG_RMGR(RM_HASH_ID, "Hash", hash_redo, hash_desc, NULL, NULL)
PG_RMGR(RM_GIN_ID, "Gin", gin_redo, gin_desc, gin_xlog_startup, gin_xlog_cleanup)
PG_RMGR(RM_GIST_ID, "Gist", gist_redo, gist_desc, gist_xlog_startup, gist_xlog_cleanup)
PG_RMGR(RM_SEQ_ID, "Sequence", seq_redo, seq_desc, NULL, NULL)
PG_RMGR(RM_SPGIST_ID, "SPGist", spg_redo, spg_desc, spg_xlog_startup, spg_xlog_cleanup)
其中,上述引用代码中PG_RMGR函数的参数依次为:
参数名称 | 具体含义 |
---|---|
symname | 资源管理器ID |
name | 资源名称 |
redo | redo恢复函数 |
desc | 描述函数 |
startup | 启动函数 |
cleanup | 清理函数 |
在PostgreSQL中,用xl_rmid和xl_info高4位来唯一地标示该XLOG日志记录对应的数据库操作,例如事务资源管理器(RM_XACT_ID),对应XLogRecord中xl_info字段高4位:
#define XLOG_XACT_COMMIT 0x00
#define XLOG_XACT_PREPARE 0x10
#define XLOG_XACT_ABORT 0x20
#define XLOG_XACT_COMMIT_PREPARED 0x30
#define XLOG_XACT_ABORT_PREPARED 0x40
#define XLOG_XACT_ASSIGNMENT 0x50
#define XLOG_XACT_COMMIT_COMPACT 0x60
例如元组管理器(RM_HEAP_ID),对应xl_info的高4位:
#define XLOG_HEAP_INSERT 0x00
#define XLOG_HEAP_DELETE 0x10
#define XLOG_HEAP_UPDATE 0x20
/* 0x030 is free, was XLOG_HEAP_MOVE */
#define XLOG_HEAP_HOT_UPDATE 0x40
#define XLOG_HEAP_NEWPAGE 0x50
#define XLOG_HEAP_LOCK 0x60
#define XLOG_HEAP_INPLACE 0x70
xl_info字段是个xl_info低4位表示当前XLOG记录数据块备份的情况:
#define XLR_BKP_BLOCK_MASK 0x0F /* all info bits used for bkp blocks */
#define XLR_MAX_BKP_BLOCKS 4
#define XLR_BKP_BLOCK(iblk) (0x08 >> (iblk)) /* iblk in 0..3 */
XLogRecord的xl_crc记录XLOG日志记录的CRC校验,保证写入到磁盘的XLOG记录都是完整的,如果应用不完整的日志记录,PostgreSQL会报错。
XLOG日志记录的资源管理器数据
XLOG日志记录的资源管理器数据由一系列XLogRecData结构体链表组成,之所以要用XLogRecData链,是因为在所要处理的日志记录实体数据在内存空间可能不是连续存储的,而且数据可能分布在多个缓冲区内,需要用XlogRecData链表将它们组织起来。XlogRecData数据结构如下:
typedef struct XLogRecData
{
char *data; /*资源管理器包含数据的开始*/
uint32 len; /*资源管理器包含的数据大小*/
Buffer buffer; /*如果有buffer指明第几个缓冲区*/
bool buffer_std; /*是否含有标准的pd_lower/pd_upper结构*/
struct XLogRecData *next; /*指向下一个结构体*/
} XLogRecData;
其中,buffer_std该值为true,则容许XLOG释放备份页的空闲空间,空闲空间由pd_lower和pd_upper限定:
- pd_lower表示页面起始位置与未分配空间开头的字节偏移
- pd_upper表示页面末尾位置与未分配空间末尾的字节偏移
XLogRecData中data保存每条XLOG日志记录中的数据信息,以INSERT、UPDATE、DELETE为例,XLogRecData中data的大体内容如下(该图引自《Internals Of PostgreSQL Wal》):
备份数据块
备份数据块包含一个头部信息BkpBlock和一块大小的备份数据,其中BkpBlock结构如下:
typedef struct BkpBlock
{
RelFileNode node; /* 用于唯一标示该块所属的关系表,包括表空间OID,数据库OID,关系表OID等*/
ForkNumber fork; /*一个关系表在存储上可能由多个分支组成,每个分支以文件单独存储,RelFileNode对应关系表的分支号*/
BlockNumber block; /*对应块的块号*/
uint16 hole_offset; /*空洞偏移量*/
uint16 hole_length; /* 空洞长度*/
} BkpBlock;
如果需要备份的块存在空洞,则备份的时候只记录这个空洞的偏移量和长度,但没有实际备份它,从而提高备份效率。
XLOG record 结构
一条日志记录的的组织形式如下所示(以下分析基于RDS for PostgreSQL,即9.4版本):
* Fixed-size header (XLogRecord struct) /*日志记录头*/
* rmgr-specific data /*资源管理器数据,对应操作的对象,譬如元组的id等内容*/
* BkpBlock/*备份块块头*/
* backup block data/*操作的一些数据,如更新元组时,要更新的新值存储在这个区域*/
* BkpBlock
* backup block data
* ...
为了更好地探究堆表INSERT操作对应XLOG record 的内容,我们创建一个简单的TABLE,并执行INSERT操作:
create table test(id int);
insert into test values(3);
XLogInsert函数
在执行INSERT操作的时候,PostgreSQL会调用heap_insert函数,其中会调用XLogInsert去插入对应的XLOG record:
recptr = XLogInsert(RM_HEAP_ID, info, rdata);
PageSetLSN(page, recptr);
注意:实际上是调用两次XLogInsert,除了HEAP INSERT操作的XLOG record,还有事务提交的XLOG record。
函数XLogInsert的返回值是XLogRecPtr结构类型,即LSN(log secquence number)。heap_insert函数在执行XLogInsert()后,把其返回值XLogRecPtr记录赋值给对应的page的PageHeaderData结构中,以实现WAL机制(参考PgSQL · 特性分析 · Write-Ahead Logging机制浅析)。
XLogInsert函数中会去包装一个XLOG record,并把它刷写到磁盘,我们接下来分析一下XLogInsert函数。
XLogInsert函数定义:
XLogRecPtr XLogInsert(RmgrId rmid, uint8 info, XLogRecData *rdata)
XLogInsert的三个函数参数分别是:
-
rmid
- RmgrId类型
- 代表本条XLOG record所属的资源管理器类型,例如我们上面的例子中INSERT操作属于RM_HEAP_ID,即堆表资源管理器
-
info
- uint8类型
- 代表资源管理器对应的操作,例如堆表中INSERT操作为0x00
-
rdata
- XLogRecData指针类型(链表)
- 每个XLogRecData结构体存储对应的资源管理器数据rmgr-specific data
之所以要用XLogRecData链,是因为在所要处理的日志记录实体数据在内存空间可能不是连续存储的,而且数据可能分布在多个缓冲区内,需要用XlogRecData链表将它们组织起来。XlogRecData数据结构如下:
typedef struct XLogRecData
{
char *data; /*包含实体数据的起始位置*/
uint32 len; /*包含实体数据大小*/
Buffer buffer; /*如果有buffer指明第几个缓冲区*/
bool buffer_std; /*是否含有标准的pd_lower/pd_upper结构*/
struct XLogRecData *next; /*指向下一个XLogRecData*/
} XLogRecData;
其中,buffer_std该值为true,则容许XLOG释放备份页的空闲空间,空闲空间由pd_lower和pd_upper限定:
- pd_lower表示页面起始位置与未分配空间开头的字节偏移
- pd_upper表示页面末尾位置与未分配空间末尾的字节偏移
通过分析三个XLogInsert函数参数,可以看出XLogInsert主要是将rdata封装成一个XLOG record。接下来我们将分析heap_insert函数内如何对rdata进行赋值。
heap_insert函数
heap_insert函数主要操作HeapTupleData结构体,对应在每个数据页中存储的每个tuple,结构如下图所示:
tuple分为头部信息和数据信息,这里不再展开,我们将在分析PostgreSQL的MVCC机制时,将其中的结构详细分析。
heap_insert函数的主要操作如下:
-
调用RelationGetBufferForTuple方法找到shmem里缓存数据块buffer
-
调用RelationPutHeapTuple方法,把组装好的元组tuple放到对应buffer中合适的位置
-
赋值XLogRecData类型变量rdata,通过代码分析可以看出rdata实际上是对tuple的内容摘要
- XLogRecData rdata[4]; 堆表的INSERT操作有4个XLogRecData结构体组成的链表
- rdata[0].data 存储一个xl_heap_insert结构,用于标示一些基本信息:
-
struct xl_heap_insert
{
xl_heaptid target; /_ inserted tuple id /
uint8 flags;
/ xl_heap_header & TUPLE DATA FOLLOWS AT END OF STRUCT _/
} xl_heap_insert;- rdata[0].buffer = InvalidBuffer - rdata[1].data存储一个xl_heap_header结构,存储tuple头部的简化信息:
-
struct xl_heap_header
{
uint16 t_infomask2;
uint16 t_infomask;
uint8 t_hoff;
} xl_heap_header;- rdata[1].buffer = need_tuple_data ? InvalidBuffer : buffer;如果需要存储整个数据块,则把buffer赋值给rdata - rdata[2].data存储tuple头部后面的数据,比如INSERT操作的插入元组的每列的数值 - rdata[2].buffer = need_tuple_data ? InvalidBuffer : buffer;同rdata[1]
- 调用XLogInsert,将rdata封装成XLOG record写入WAL缓冲区,如果需要切换日志段文件,调用XLogWrite刷写到磁盘
经过以上分析,我们可以知道,XLOG record的核心部分是资源管理器数据(XLogRecData)和备份数据块(backup block data),这两个数据包含了我们恢复时候需要的数据。在各个资源管理器的具体操作调用XLogInsert之前,需要对这两个部分进行填充。