postgresql源码学习(24)—— 事务日志⑤-日志写入WAL Buffer

67 篇文章 52 订阅
34 篇文章 3 订阅

一、 写入过程简介

1. 写入步骤

日志写入WAL Buffer的过程分为两步:

  • 预留空间:组装完成后日志记录的长度已经确定,因此可以先计算这个长度,并在WAL Buffer里预留空间,空间预留的过程通过XLogCtl->Insert->insertpos_lck锁保护。也就是说,每个需要写入WAL日志记录的进程在预留空间时都是互斥的。
  • 数据复制:一旦空间预留完成,数据复制的过程是可以并发的,PG通过WALInsertLocks锁来控制并发复制的过程。PG声明了NUM_XLOGINSERT_LOCKS(目前是8)个WALInsertLocks,每个WALInsertLocks由轻量级+日志写入位置组成。不同进程的不同事务在刷入日志时会随机(参照自己的MyProc->pgprocno)获取一个WALInsertLocks。

2. WALInsertLocks

/*
 * pg声明了NUM_XLOGINSERT_LOCKS(目前是8)个用于wal插入的锁WALInsertLock
 * 值越大可以并发插入的进程越多,但是CPU负载会越高。
 */
#define NUM_XLOGINSERT_LOCKS  8
/* 每个WALInsertLock由“轻量锁+日志写入位置”组成
 * 想要进行日志写入时,必须持有一个WALInsertLock(随机获取,哪一个无所谓)
*/
typedef struct
{
	LWLock		lock;   // 轻量锁,当锁释放时,代表日志已经写入WAL Buffer
	XLogRecPtr	insertingAt;  // 记录当前日志写入WAL Buffer的进展,不需要跨页写入的小记录不会去更新这个值,通常在日志记录较长时才会更新该值。insertingAt这个变量会在进程将WAL由内存刷往磁盘时读取,以确认所有对该区域的写入操作已完成

	XLogRecPtr	lastImportantAt;   // lastImportantAt contains the LSN of the last important WAL record inserted using a given lock.在待插入的日志记录中,有一些记录是和数据一致性无关的,即使丢失也不影响,这种记录不影响lastImportantAt的值
} WALInsertLock;

这里我们留下两个问题:

  • 为什么数据复制的并发度只设为8,不设更大?
  • 多进程并发复制数据的冲突怎么解决?

        这个问题的答案在WaitXLogInsertionsToFinish函数,下一篇我们会学习它。

        简单来说,每次WAL刷入磁盘,都会调用这个函数,而这个函数需要遍历所有WALInsertLocks,所以NUM_XLOGINSERT_LOCKS不宜过大,目前代码中写死为8。
 

for (i = 0; i < NUM_XLOGINSERT_LOCKS; i++)
{
 ...
}

二、 XLogInsertRecord函数

如前所述,这个代码最重要就干两件事:

  • 调用ReserveXLogInsertLocation函数,为之前组装好的XLOG预留空间,返回预留的StartPos(起始位置)和EndPos(结束位置)。
  • 调用CopyXLogRecordToWAL函数,将XLOG数据复制到WAL Buffer
  • 最后返回的是XLOG的EndPos,即当前写入日志已经到哪个位置了

函数开头是一些检查

XLogRecPtr
XLogInsertRecord(XLogRecData *rdata,
				 XLogRecPtr fpw_lsn,
				 uint8 flags,
				 int num_fpi)
{
	XLogCtlInsert *Insert = &XLogCtl->Insert;
	pg_crc32c	rdata_crc;
	bool		inserted;
	XLogRecord *rechdr = (XLogRecord *) rdata->data;
	uint8		info = rechdr->xl_info & ~XLR_INFO_MASK;
	bool		isLogSwitch = (rechdr->xl_rmid == RM_XLOG_ID &&
							   info == XLOG_SWITCH);
	XLogRecPtr	StartPos;
	XLogRecPtr	EndPos;
	bool		prevDoPageWrites = doPageWrites;
…
START_CRIT_SECTION();
// WAL日志段切换期间会拿排他锁,此时其他进程不能预留空间
	if (isLogSwitch)
		WALInsertLockAcquireExclusive();
	else
		WALInsertLockAcquire();

// 进程当前copy的RedoRecPtr有没有过期,如果过期了(只会发生在恰好做完checkpoint操作),需要回到调用函数重新计算,因此这种场景下会比其他场景慢。
	if (RedoRecPtr != Insert->RedoRecPtr)
	{
		Assert(RedoRecPtr < Insert->RedoRecPtr);
		RedoRecPtr = Insert->RedoRecPtr;
	}

下面这段就是著名的“全页写”,后面会有一篇单独文章学习它,这里先只列出

// 检查是否启用了 fullPageWrites 或者 forcePageWrites
	doPageWrites = (Insert->fullPageWrites || Insert->forcePageWrites);

	if (doPageWrites &&
		(!prevDoPageWrites ||
		 (fpw_lsn != InvalidXLogRecPtr && fpw_lsn <= RedoRecPtr)))
	{
		/*
		 * Oops, some buffer now needs to be backed up that the caller didn't
		 * back up.  Start over.如果人家配了但你没做全页写,需要回炉重做,直接报错返回
		 */
		WALInsertLockRelease();
		END_CRIT_SECTION();
		return InvalidXLogRecPtr;
	}

预留空间部分

	/*
	 * Reserve space for the record in the WAL. This also sets the xl_prev pointer.
     * 预留空间,这步也会设置xl_prev指针
	 */
	if (isLogSwitch)
    // 如果是日志切换记录,恰好需要做日志切换,则可能StartPos和EndPos相同,也就是说不需要记这个WAL日志记录
		inserted = ReserveXLogSwitch(&StartPos, &EndPos, &rechdr->xl_prev);
	else
	{
		ReserveXLogInsertLocation(rechdr->xl_tot_len, &StartPos, &EndPos,
								  &rechdr->xl_prev);
		inserted = true;
	}

数据复制部分

// 预留空间之后,开始做数据复制。inserted 为true,表示非日志切换记录
	if (inserted)
	{
		/*
		 * Now that xl_prev has been filled in, calculate CRC of the record header.目前xl_prev已经填充了,对记录头做cdc校验
		 */
		rdata_crc = rechdr->xl_crc;
		COMP_CRC32C(rdata_crc, rechdr, offsetof(XLogRecord, xl_crc));
		FIN_CRC32C(rdata_crc);
		rechdr->xl_crc = rdata_crc;

		/*
		 * All the record data, including the header, is now ready to be
		 * inserted. Copy the record in the space reserved. 将日志记录复制到WAL Buffer
		 */
		CopyXLogRecordToWAL(rechdr->xl_tot_len, isLogSwitch, rdata,
							StartPos, EndPos);

		/*
		 * Unless record is flagged as not important, update LSN of last
		 * important record in the current slot. When holding all locks, just
		 * update the first one.除非是一些被标记为不重要的数据,否则都需要更新当前槽位的lastImportantAt值,如果holdingAllLocks为真,则更新第一个值
		 */
		if ((flags & XLOG_MARK_UNIMPORTANT) == 0)
		{
			int			lockno = holdingAllLocks ? 0 : MyLockNo;

			WALInsertLocks[lockno].l.lastImportantAt = StartPos;
		}
	}
	else // inserted 为false,表示日志切换记录
	{
		/*
		 * This was an xlog-switch record, but the current insert location was
		 * already exactly at the beginning of a segment, so there was no need
		 * to do anything. 这是一条日志切换记录,但当前插入位置正好在段的开始位置,因此什么都不用干(因为没东西可以复制)。
		 */
	}

	/*
	 * Done! Let others know that we're finished.操作完成,释放锁
	 */
	WALInsertLockRelease();

	MarkCurrentTransactionIdLoggedIfAny();

	END_CRIT_SECTION();
…
	/*
	 * Update our global variables
	 */
	ProcLastRecPtr = StartPos;
	XactLastRecEnd = EndPos;
…
	return EndPos;
}

三、 空间预留函数 ReserveXLogInsertLocation

  • 为WAL记录(在WAL Buffer中)预留适当大小的空间。
  • StartPos是保留位置的开头,*EndPos是保留位置结尾+1(end+1),*PrevPtr是前一条记录的开头位置,它用于设置该记录的 xl_prev
  • 这部分对于XLogInsert函数的性能非常重要,因为这是只能串行执行的,而其余部分基本都可以并发处理。因此要确保这部分尽量简短,insertpos_lck在繁忙系统上可能遇到激烈竞争。
  • 注意:这里的空间计算必须与后面的数据复制函数 CopyXLogRecordToWAL中的代码相匹配。

static void
ReserveXLogInsertLocation(int size, XLogRecPtr *StartPos, XLogRecPtr *EndPos,
                          XLogRecPtr *PrevPtr)
{
    XLogCtlInsert *Insert = &XLogCtl->Insert;
    uint64      startbytepos;
    uint64      endbytepos;
    uint64      prevbytepos;

    size = MAXALIGN(size);

    /* All (non xlog-switch) records should contain data. */
    Assert(size > SizeOfXLogRecord);

    /*
     * 这部分是核心,也是真正串行执行的部分,务必要快
     */
    SpinLockAcquire(&Insert->insertpos_lck);

    startbytepos = Insert->CurrBytePos;
    endbytepos = startbytepos + size;
    prevbytepos = Insert->PrevBytePos;
    Insert->CurrBytePos = endbytepos;
    Insert->PrevBytePos = startbytepos;

    SpinLockRelease(&Insert->insertpos_lck);

    *StartPos = XLogBytePosToRecPtr(startbytepos);
    *EndPos = XLogBytePosToEndRecPtr(endbytepos);
    *PrevPtr = XLogBytePosToRecPtr(prevbytepos);

    /*
     * Check that the conversions between "usable byte positions" and
     * XLogRecPtrs work consistently in both directions.
     */
    Assert(XLogRecPtrToBytePos(*StartPos) == startbytepos);
    Assert(XLogRecPtrToBytePos(*EndPos) == endbytepos);
    Assert(XLogRecPtrToBytePos(*PrevPtr) == prevbytepos);
}

四、 数据复制函数 CopyXLogRecordToWAL

将WAL记录数据复制到WAL Buffer中预留好的空间。

函数参数:

  • write_len:XLOG的总长度,用于做校验。
  • isLogSwitch:该记录是否是日志切换记录
  • rdata:XLogRecData链表,存放了XLOG的数据。
  • StartPos:XLOG的写入开始位置
  • EndPos:XLOG的结束位置,用于做校验
static void
CopyXLogRecordToWAL(int write_len, bool isLogSwitch, XLogRecData *rdata,
                    XLogRecPtr StartPos, XLogRecPtr EndPos)
{
    char       *currpos;
    int         freespace;
    int         written;
    XLogRecPtr  CurrPos;
    XLogPageHeader pagehdr;

    /*
     * Get a pointer to the right place in the right WAL buffer to start
     * inserting to.复制操作的起点
     */
    CurrPos = StartPos;
    currpos = GetXLogBuffer(CurrPos);
    freespace = INSERT_FREESPACE(CurrPos);

    /*
     * there should be enough space for at least the first field (xl_tot_len) on this page.
     */
    Assert(freespace >= sizeof(uint32));

    /* Copy record data,核心代码,循环复制rdata数组中每个元素的数据 */
    written = 0;
    while (rdata != NULL)
    {
        char       *rdata_data = rdata->data;
        int         rdata_len = rdata->len;

/* 用于处理当前需要写入的XLOG长度大于WAL Buffer中当前page的可用空间的情况,此时需要先将XLOG一部分写入当前page,然后再切换到下一个page。 */
        while (rdata_len > freespace)
        {
            /*
             * Write what fits on this page, and continue on the next page.
             */
            Assert(CurrPos % XLOG_BLCKSZ >= SizeOfXLogShortPHD || freespace == 0);
            memcpy(currpos, rdata_data, freespace);
            rdata_data += freespace;
            rdata_len -= freespace;
            written += freespace;
            CurrPos += freespace;

            /*
             *获取下一个page开头位置的指针,并在页头设置xlp_rem_len
             */
            currpos = GetXLogBuffer(CurrPos);
            pagehdr = (XLogPageHeader) currpos;
            pagehdr->xlp_rem_len = write_len - written;
            pagehdr->xlp_info |= XLP_FIRST_IS_CONTRECORD;

            /* skip over the page header,跳过页头部分 */
            if (XLogSegmentOffset(CurrPos, wal_segment_size) == 0)
            {
                CurrPos += SizeOfXLogLongPHD;
                currpos += SizeOfXLogLongPHD;
            }
            else
            {
                CurrPos += SizeOfXLogShortPHD;
                currpos += SizeOfXLogShortPHD;
            }
            freespace = INSERT_FREESPACE(CurrPos);
        }

        Assert(CurrPos % XLOG_BLCKSZ >= SizeOfXLogShortPHD || rdata_len == 0);
        memcpy(currpos, rdata_data, rdata_len);
        currpos += rdata_len;
        CurrPos += rdata_len;
        freespace -= rdata_len;
        written += rdata_len;

        rdata = rdata->next;
    }
    Assert(written == write_len);
    …
    if (CurrPos != EndPos)
        elog(PANIC, "space reserved for WAL record does not match what was written");
}

       内层循环用于处理XLOG长度大于当前page空闲空间的情况,此时需要先将XLOG的一部分存放到当前page的剩余空间中,然后调用GetXLogBuffer为XLOG的剩余部分寻找一个新的page进行写入,而这个新page实际就是当前page的下一个page。如果当前page是WAL Buffer中的最后一个page,那么GetXLogBuffer就会循环到的WAL Buffer的第一个page。

       由于是循环队列,那么当循环到队头后,队头page中的数据就会被新的XLOG覆盖。既然要覆盖,那么在覆盖之前需要先确保对应page中的数据已经落盘。所以GetXLogBuffer还有一个非常重要的功能就是在页面覆盖之前判断这个页面是否是脏页,如果是脏页就需要将脏页落盘。后续文章中我们会继续学习,XLOG是如何落盘的。

五、 GetXLogBuffer函数(1)

       上面CopyXLogRecordToWAL()中用到了一个很重要的函数 GetXLogBuffer,去获取复制操作的起点  currpos = GetXLogBuffer(CurrPos); 。

       CurrPos是LSN号,而currpos是WAL Buffer中的地址指针,因此这里我们只重点看一个功能 —— 如何将LSN转换为WAL Buffer中的地址指针?

1. 知识回顾

回忆一下日志初始化的内容

postgresql源码学习(21)—— 事务日志②-日志初始化_Hehuyi_In的博客-CSDN博客

WAL Buffer由5部分组成

在这里插入图片描述

XLogCtl对应的结构体如下

/*
 * Total shared-memory state for XLOG.
 */
typedef struct XLogCtlData
{
...
	char	   *pages;			/* 指向XLOG BUFFER中尚未写入XLOG的页的指针(起始地址) */
	int			XLogCacheBlck;	/* 存放最大的log buffer页面下标,也就是页面数量-1 */
...
} XLogCtlData;

// 定义对应指针,初始值为空
static XLogCtlData *XLogCtl = NULL;

        在WAL写入部分,需要重点关注的是pages成员,用于指向log buffer的起始地址

        上图中第四部分的pad是为了让这个起始地址对齐为XLOG_BLCKSZ的整数倍,这是为了方便定位,在GetXLogBuffer中会用到这个特性。

       另外一个是XLogCacheBlck成员,用于存放最大的log buffer页面下标。也就是页面数量-1。

2. GetXLogBuffer函数

       WAL Buffer是由连续内存空间组成的循环队列,写入时从前向后写,写满后循环到队头,再重头开始写。涉及这部分操作的核心代码如下:

static char *
GetXLogBuffer(XLogRecPtr ptr)
{
	int			idx;
	XLogRecPtr	endptr;
	static uint64 cachedPage = 0;
	static char *cachedPos = NULL;

	/*
	 * 根据LSN获取buffer page的下标
	 */
	idx = XLogRecPtrToBufIdx(ptr);
    
	/*
	 * Found the buffer holding this page. Return a pointer to the right
	 * offset within the page.
	 */
	cachedPage = ptr / XLOG_BLCKSZ;
	cachedPos  = XLogCtl->pages + idx * (Size) XLOG_BLCKSZ;

	return cachedPos + ptr % XLOG_BLCKSZ;
}

3. XLogRecPtrToBufIdx 的实现

/*
 * XLogRecPtrToBufIdx returns the index of the WAL buffer that holds, or
 * would hold if it was in cache, the page containing 'recptr'.
 */
#define XLogRecPtrToBufIdx(recptr)	\
	(((recptr) / XLOG_BLCKSZ) % (XLogCtl->XLogCacheBlck + 1))

        其中XLogCtl->XLogCacheBlck + 1就是WAL Buffer中page的个数,这其实就是循环队列的标准算法。

参考

《PostgreSQL技术内幕:事务处理深度探索》第4章

https://blog.csdn.net/obvious__/article/details/119242661?spm=1001.2014.3001.5502

PostgreSQL的wal日志并发写入机制 - 知乎

https://blog.csdn.net/asmartkiller/article/details/121375548

https://icode.best/i/12479444350651

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hehuyi_In

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值