前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊PostgreSQL的WAL日志系统

聊聊PostgreSQL的WAL日志系统

作者头像
用户4700054
发布2022-08-17 12:43:18
1.5K0
发布2022-08-17 12:43:18
举报
文章被收录于专栏:存储内核技术交流

wal日志格式

  • wal 日志是由多个固定的段组成,每个段都是单独的wal日志文件。日志文件内部划分类型的page,每个page是有page header(页面头)和 log record(日志记录)组成,每个page默认是8K大小。page中的header分为两类,日志文件中第一个page的header记录了日志文件长度,和page大小,这个是由XLogLongPageHeaderData来描述。日志文件其他类型的page header则是使用XLogPageHeaderData描述,它包含了日志对应版本和时间线信息.
代码语言:javascript
复制
// 日志的版本信息
#define XLOG_PAGE_MAGIC 0xD10D	

typedef uint64 XLogRecPtr;
typedef struct XLogPageHeaderData
{
	// 校验当前事务的版本信息
	uint16		xlp_magic;		
	// 事务中的一些flag信息
	uint16		xlp_info;		
	// 当前页中第一条记录的时间线信息
	TimeLineID	xlp_tli;		
	// 当前日主记录的地址
	XLogRecPtr	xlp_pageaddr;	
	// 日志记录跨页面保存时候使用,保存日志长度
	uint32		xlp_rem_len;	
} XLogPageHeaderData;

typedef struct XLogLongPageHeaderData
{
	// 标准的page header
	XLogPageHeaderData std;		
	// pg_control中的系统标识ID
	uint64		xlp_sysid;		
	// 段大小
	uint32		xlp_seg_size;	
	uint32		xlp_xlog_blcksz;
} XLogLongPageHeaderData;
  • XLogRecord 日志是每条日志中的日志数据的表达形式,日志是记录整个数据库每一次变更的动作。wal日志从实现的角度分析,wal日志page中存储page header和log record。每个page header都存储在每个page的头部,紧接着头部后面都是存储log record的数据。
代码语言:javascript
复制
typedef struct XLogRecord
{
	// 日志记录的长度
	uint32		xl_tot_len;		
	// 事务的ID
	TransactionId xl_xid;		
	// 上一条redord的指针
	XLogRecPtr	xl_prev;		
	uint8		xl_info;		
	// 日志对应的resource manager的ID
	RmgrId		xl_rmid;	

	// 日志数据的crc的数据校验
	pg_crc32c	xl_crc;			/* CRC for this record */

	/* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */

} XLogRecord;
wal日志系统初始化
  • 每次变更事务提交时候,需要将变更事务日志落盘,在PG中为了提高性能,并非采用实时flush到磁盘,而是在PG中提供XLog Buffer空间临时存储提交的事务日志,然后定期flush到磁盘。XLog Buffer的大小是有参数wal_buffers设定,当这个参数设置为-1时候,PG会根据shared_bufferswal_segment_size参数自动计算而得到。
  • 日志系统初始化流程,流程是从main开始,计算需要共享的内存大小,然后通过XLOGShmemInit调用XLOGShmemSize进行初始化Log需要的内存和Log控制信息结构初始化。Log子系统中比较核心的控制字段有struct XLogCtlDatastruct XLogCtlInsert,其中XLogCtlData结构中存储了当前WAL的写入状态、flush状态以及Buffer Page的状态信息;XLogCtlInsert结构存储往WAL日志中写入log record需要各种结构
代码语言:javascript
复制
int main(int argc, char *argv[])
{
#ifdef EXEC_BACKEND
	if (argc > 1 && strncmp(argv[1], "--fork", 6) == 0)
		SubPostmasterMain(argc, argv);	/* does not return */
#endif
}
void SubPostmasterMain(int argc, char *argv[])
{
	CreateSharedMemoryAndSemaphores();
}
void CreateSharedMemoryAndSemaphores(void)
{
	XLOGShmemInit()
}
void XLOGShmemInit(void)
{
		XLogCtl = (XLogCtlData *)
		ShmemInitStruct("XLOG Ctl", XLOGShmemSize(), &foundXLog);
}


	{
		{"wal_buffers", PGC_POSTMASTER, WAL_SETTINGS,
			gettext_noop("Sets the number of disk-page buffers in shared memory for WAL."),
			NULL,
			GUC_UNIT_XBLOCKS
		},
		&XLOGbuffers,
		-1, -1, (INT_MAX / XLOG_BLCKSZ),
		check_wal_buffers, NULL, NULL
	},

// 初始胡Log系统的内部数据结构和Log的共享内存
Size XLOGShmemSize(void)
{
	Size		size;

	// 如果XLOGbuffers=-1,则表示没有设置log_buffer大小,则需要自动计算出一个合理的值。
	// XLOGChooseNumBuffers 逻辑,初始化一个val=1000/32,如果val <8,val=8;如果val > (16*1024*1024)/xlog块大小,则val = (16*1024*1024)/xlog块大小,返回val作为计算出来的XLOGBuffer的大小
	if (XLOGbuffers == -1)
	{
		char		buf[32];

		snprintf(buf, sizeof(buf), "%d", XLOGChooseNumBuffers());
		SetConfigOption("wal_buffers", buf, PGC_POSTMASTER, PGC_S_OVERRIDE);
	}
	Assert(XLOGbuffers > 0);

	/* xlog的控制头,也是共享信息 */
	size = sizeof(XLogCtlData);

	// 日志插入时需要的共享锁需要的空间大小 
	size = add_size(size, mul_size(sizeof(WALInsertLockPadded), NUM_XLOGINSERT_LOCKS + 1));
	// 日志文件块起始lsn需要的空间大小 
	size = add_size(size, mul_size(sizeof(XLogRecPtr), XLOGbuffers));
	// log buffer io的空间大小 
	size = add_size(size, XLOG_BLCKSZ);
	// XLOGBuffers大小,xlog块大小 和XLOGbuffers 乘机
	size = add_size(size, mul_size(XLOG_BLCKSZ, XLOGbuffers));


	return size;
}
wal日志写入

-PostgreSQL高版本中(>9.5)事务的日志不是直接写入到Wal Buffer中,而是先组成XLogRecData链表,然后在转换为一个log record.PG中默认定义了XLogRecData链表数组XLogRecData *rdatas,这个数组长度由XLR_NORMAL_RDATAS=20.另外日志写入中定义了registered_buffer *registered_buffers,用来注册已经被修改的page,数组中每个元素都占用一个槽位。

代码语言:javascript
复制
// 日志链表的数组的定义
static XLogRecData *rdatas;
// 当前已经使用的数组下标
static int	num_rdatas;	
// 最大的数组下标
static int	max_rdatas;		

typedef struct XLogRecData
{
	// 下一个日志data的指针
	struct XLogRecData *next;	
	// 日志数据
	char	   *data;			
	// 数据长度
	uint32		len;			
} XLogRecData;


// 当页面被修改buffer page会被注册到registered_buffers数组
static registered_buffer *registered_buffers;
// registered_buffers最大长度
static int	max_registered_buffers;
// 起始长度
static int	max_registered_block_id = 0;	

typedef struct
{
	//  槽位使用标志
	bool		in_use;			
	// 和buffer page相关的flag信息
	uint8		flags;			
	// buffer page对应的那个表
	RelFileNode rnode;			
	ForkNumber	forkno;
	BlockNumber block;
	// buffer page的指针
	Page		page;			
	// XLogRegisterBufData注册的长度
	uint32		rdata_len;		
	// XLogRegisterBufData注册数据到这个链表
	XLogRecData *rdata_head;	
	// XLogRegisterBufData注册数据到链表尾部
	XLogRecData *rdata_tail;	

	XLogRecData bkp_rdatas[2];	/* temporary rdatas used to hold references to
								 * backup block data in XLogRecordAssemble() */

	// 压缩page的使用临时空间
	char		compressed_page[PGLZ_MAX_BLCKSZ];
} registered_buffer;
  • 日志首先是写入日志前的检查,组装日志相关数据,这个阶段日志相关的数据写入到链表,最后根据链表中的日志相关数据转换wal 日志物理条目,通过申请预留wal_buffer空间和日志数据复制来完成wal的写入。接下来以heap_insert函数中为例,简要分析下其过程
代码语言:javascript
复制
void
heap_insert(Relation relation, HeapTuple tup, CommandId cid,
			int options, BulkInsertState bistate)
{
	/* XLOG stuff */
	if (RelationNeedsWAL(relation))
	{
		// 写入日志前的准备
		XLogBeginInsert();
		
		// 注册生成日志相关的数据
		XLogRegisterData((char *) &xlrec, SizeOfHeapInsert);

		xlhdr.t_infomask2 = heaptup->t_data->t_infomask2;
		xlhdr.t_infomask = heaptup->t_data->t_infomask;
		xlhdr.t_hoff = heaptup->t_data->t_hoff;

		//注册一个buffer,占用了0号槽位buffer
		XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags);
		
		// 在0号槽位的buffer注册相关的数据
		XLogRegisterBufData(0, (char *) &xlhdr, SizeOfHeapHeader);
		XLogRegisterBufData(0,
							(char *) heaptup->t_data + SizeofHeapTupleHeader,
							heaptup->t_len - SizeofHeapTupleHeader);

		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
		
		// 执行日志插入
		recptr = XLogInsert(RM_HEAP_ID, info);
		// 设置page的LSN
		PageSetLSN(page, recptr);
	}
}

针对机械磁盘一般是512个字节的扇区,而os的一般数据block为4K,PG的数据库的每次数据写入是page是8K。这三者的大小完全不对等,所以PG的Page刷新到磁盘并不是原子操作,因此一个Page写入到磁盘很可能是写了一部分,这会导致Page损坏。不同的数据都有对应的解决方案,MSQL是double write机制保证;PG则是Full Page Write.如果在数据更新的某个Page时,这个Page最近修改的LSN小于全局Checkpoint Lsn,修改之前会把这个Page全部写到Wal日志中,然后进行修改操作;PG是默认是通过参数full_page_writes参数来禁用或者启用Full Page Write机制。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 存储内核技术交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • wal日志格式
    • wal日志系统初始化
      • wal日志写入
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档