一、 日志写入的整体流程
从功能上看
从对应函数来看
本节主要介绍日志注册(蓝色)部分。
二、 主要结构体与变量
1. registered_buffer结构体与registered_buffers数组
这俩名字很像,registered_buffers是一个数组,其中的每个元素由registered_buffer结构体组成。该数组用于注册被修改的页面信息,XLogRegisterBuffer函数每注册一个页面时,会在其中占用一个槽位。
/*
* For each block reference registered with XLogRegisterBuffer, we fill in
* a registered_buffer struct.
*/
typedef struct
{
bool in_use; /* is this slot in use? 该槽位是否在使用? */
uint8 flags; /* REGBUF_* flags,buffer相关的标记位,参考下方 */
RelFileNode rnode; /* identifies the relation and block,表对应的块物理信息 */
ForkNumber forkno;
BlockNumber block;
Page page; /* page content,页面内容指针 */
uint32 rdata_len; /* total length of data in rdata chain,rdata链表(由XLogRegisterBufData函数注册,下面会介绍)的总长度 */
XLogRecData *rdata_head; /* head of the chain of data registered with this block,rdata链表的第一个块*/
XLogRecData *rdata_tail; /* last entry in the chain, or &rdata_head if empty,,rdata链表的最后一个块*/
XLogRecData bkp_rdatas[2]; /* temporary rdatas used to hold references to backup block data in XLogRecordAssemble(),XLogRecordAssemble函数不方便再调用XLogRegister*函数,这里设定两个临时变量 */
/* buffer to store a compressed version of backup block image,存储备份区块镜像压缩版本信息的buffer */
char compressed_page[PGLZ_MAX_BLCKSZ];
} registered_buffer;
static registered_buffer *registered_buffers; // 定义registered_buffers数组
static int max_registered_buffers; /* allocated size,数组最大空间 */
static int max_registered_block_id = 0; /* highest block_id + 1 currently registered,当前已注册的最大块号+1 */
2. XLogRecData结构体与rdatas数组
rdatas是一个数组,其中的每个元素由XLogRecData结构体组成。事务日志并不直接写入WAL Buffer,而是先将XLogRecData组成链表,然后将此链表转为一条事务日志。
/*
* The functions in xloginsert.c construct a chain of XLogRecData structs
* to represent the final WAL record.位于xlog_internal.h
*/
typedef struct XLogRecData
{
struct XLogRecData *next; /* next struct in chain, or NULL,指向链表中下一个元素 */
char *data; /* start of rmgr data to include,rmgr data起始位置 */
uint32 len; /* length of rmgr data to include,rmgr data长度 */
} XLogRecData;
/*
* An array of XLogRecData structs, to hold registered data.位于xloginsert.c
*/
static XLogRecData *rdatas;
static int num_rdatas; /* entries currently used */
static int max_rdatas; /* allocated size */
两个结构体和数组关系如下:
三、 起始函数 XLogBeginInsert
这个函数主要干两件事:
- 通过设置begininsert_called标志,防止递归调用日志生成函数
- 通过XLogInsertAllowed函数和一些Assert做代码安全检查工作
/*
* Begin constructing a WAL record. This must be called before the
* XLogRegister* functions and XLogInsert().
*/
void
XLogBeginInsert(void)
{
Assert(max_registered_block_id == 0);
Assert(mainrdata_last == (XLogRecData *) &mainrdata_head);
Assert(mainrdata_len == 0);
/* cross-check on whether we should be here or not,恢复期间不能执行WAL插入 */
if (!XLogInsertAllowed())
elog(ERROR, "cannot make new WAL entries during recovery");
/* 避免写日志的时候又要生成日志进行插入,陷入死循环 */
if (begininsert_called)
elog(ERROR, "XLogBeginInsert was already called");
/* 标记位设置 */
begininsert_called = true;
}
四、 注册函数
主要是以下3个函数(都在xloginsert.c文件中):
- XLogRegisterData:将WAL日志数据注册到rdatas数组
- XLogRegisterBuffer:将被修改的buffer页面信息注册到registerd_buffers数组。还有一个类似的函数XLogRegisterBlock,负责注册不在shared buffer pool中的块。
- XLogRegisterBufData:将rdatas数组与registerd_buffers数组联系起来,这样就可以找到哪个buffer页中记录了哪些日志数据。
1. XLogRegisterData
将WAL数据日志注册到rdatas数组。找到rdatas数组中第一个空的位置,将传入的参数赋值给这个空位置。每注册一个就会占用一个槽位。
/*
* Add data to the WAL record that's being constructed.将数据加入构建好的WAL记录
*
* The data is appended to the "main chunk", available at replay with XLogRecGetData().
这些数据会被加到main chunk部分,在XLogRecGetData函数执行replay操作时会用到
*/
void
XLogRegisterData(char *data, int len)
{
XLogRecData *rdata;
Assert(begininsert_called);
if (num_rdatas >= max_rdatas)
elog(ERROR, "too much WAL data");
rdata = &rdatas[num_rdatas++]; //rdata是rdatas数组中的一个元素,前面我们也提到过,rdatas数组中的元素就是XLogRecData结构体。
rdata->data = data;
rdata->len = len;
/*
* we use the mainrdata_last pointer to track the end of the chain, so no need to clear 'next' here. mainrdata_last->next始终指向链表末端
*/
mainrdata_last->next = rdata;
mainrdata_last = rdata;
mainrdata_len += len;
}
2. XLogRegisterBuffer
将被修改的buffer页面信息注册到registerd_buffers数组。
在pg 9.5之前,与数据页面相关的信息同样会被注册到rdatas数组,这种实现逻辑不清晰。因此9.5之后改进了日志记录的产生和组织方式,通过XLogRegisterBuffer函数把被修改的页面注册到registerd_buffers数组中,每个页面在registerd_buffers数组中都占一个槽位。
/*
* Register a reference to a buffer with the WAL record being constructed.
* This must be called for every page that the WAL-logged operation modifies.
* 将构建好的WAL记录注册到buffer中,凡是需要记录日志的操作,其中的每个页都会调用该函数
*/
void
XLogRegisterBuffer(uint8 block_id, Buffer buffer, uint8 flags)
{
registered_buffer *regbuf; //registerd_buffer结构体,是registerd_buffers数组中的一个元素
/* NO_IMAGE doesn't make sense with FORCE_IMAGE,如果设置了FORCE_IMAGE,再设NO_IMAGE不会生效 */
Assert(!((flags & REGBUF_FORCE_IMAGE) && (flags & (REGBUF_NO_IMAGE))));
Assert(begininsert_called);
if (block_id >= max_registered_block_id)
{
if (block_id >= max_registered_buffers)
elog(ERROR, "too many registered buffers");
max_registered_block_id = block_id + 1;
}
regbuf = ®istered_buffers[block_id]; // 取出registerd_buffers数组中的一个元素,分配给刚创建的变量regbuf,后续由其赋值
BufferGetTag(buffer, ®buf->rnode, ®buf->forkno, ®buf->block);
regbuf->page = BufferGetPage(buffer);
regbuf->flags = flags;
regbuf->rdata_tail = (XLogRecData *) ®buf->rdata_head;
regbuf->rdata_len = 0;
/*
* Check that this page hasn't already been registered with some other
* block_id.检查这个page有没被其他块号注册过
*/
#ifdef USE_ASSERT_CHECKING
{
int i;
for (i = 0; i < max_registered_block_id; i++)
{
registered_buffer *regbuf_old = ®istered_buffers[i];
if (i == block_id || !regbuf_old->in_use)
continue;
Assert(!RelFileNodeEquals(regbuf_old->rnode, regbuf->rnode) ||
regbuf_old->forkno != regbuf->forkno ||
regbuf_old->block != regbuf->block);
}
}
#endif
// 打个标记,本页已经在用了
regbuf->in_use = true;
}
3. XLogRegisterBufData
起到一个链接的作用,将registered_buffers数组与rdatas数组联系起来。
/*
* Add buffer-specific data to the WAL record that's being constructed.
*/
void
XLogRegisterBufData(uint8 block_id, char *data, int len)
{
registered_buffer *regbuf;
XLogRecData *rdata;
Assert(begininsert_called);
/* find the registered buffer struct,第一个参数block_id就是XLogRegisterBuffer函数注册页面后占用的槽位 */
regbuf = ®istered_buffers[block_id];
if (!regbuf->in_use)
elog(ERROR, "no block with id %d registered with WAL insertion",
block_id);
if (num_rdatas >= max_rdatas)
elog(ERROR, "too much WAL data");
rdata = &rdatas[num_rdatas++];
/* 将rdatas数组与registerd_buffers数组联系起来 */
rdata->data = data;
rdata->len = len;
regbuf->rdata_tail->next = rdata;
regbuf->rdata_tail = rdata;
regbuf->rdata_len += len;
}
五、 insert语句调用注册函数
当我们执行一个最简单的insert语句,会看到在heap_insert函数中调用注册函数如下:
//xlrec为xl_heap_insert结构体
XLogRegisterData((char *) &xlrec, SizeOfHeapInsert);
XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags);
//xlhdr为xl_heap_header结构体
XLogRegisterBufData(0, (char *) &xlhdr, SizeOfHeapHeader);
//(char *) heaptup->t_data + SizeofHeapTupleHeader为实际元组
XLogRegisterBufData(0,
(char *) heaptup->t_data + SizeofHeapTupleHeader,
heaptup->t_len - SizeofHeapTupleHeader);
可以看到,heap_insert注册了xl_heap_insert(main data)、xl_heap_header(block data)、实际元组数据。回顾下前面阐述过的XLOG的组成部分:
注册之后XLOG填充进度为(红色暂无数据、绿色已有数据):
XLogRecord+XLogRecordBlockHeader+RelFileNode+BlockNumber + mainrdata_len +
xl_heap_header + 实际元组数据 + xl_heap_insert
下一步,日志组装函数XLogRecordAssemble就负责填充红色部分,并将以上所有数据组装成XLogRecData链表。
参考
《PostgreSQL技术内幕:事务处理深度探索》第4章
postgres预写式日志的内核实现详解-wal记录写入-低调大师优秀的个人博客
https://cloud.tencent.com/developer/article/2000646