PostgreSQL数据库WAL——XLogInsertRecord将日志记录写入WAL日志缓存_database


如图所示,一个PostgreSQl进程在组装wal数据之后,会在local内存中形成一个wal的数据链表。这个进程希望将链表中的数据拷贝到wal的buffer中。首先进程需要获取wal插入锁槽,然后根据当前已经写的wal位置和希望插入的wal记录的长度,在wal buffer中预置其wal位置,最后将local内存的wal数据链表中的数据拷贝到预置的wal页buffer中。在整个写入过程中,插入锁槽是一直持有的,在图中我们可以看到一共有8个插入锁槽,也就是说PostgreSQL目前只支持8个并发进程同时修改wal日志buffer。在预置wal位置时需要获取排他锁,因此预置wal位置的过程在PostgreSQL运行过程中是无法并行的

预留空间:由于日志已经完成Assemble,所以日志的长度已经确定。因此可以先计算WAL日志记录的长度,然后按照该长度从WAL Buffer中预留空间,空间预留的过程通过XLogCtl->Insert->insertpos_lck锁保护。也就是说,每个需要写入WAL日志记录的进程在预留空间时都是互斥的
数据复制:将Assemble之后的数据复制到预留空间中,这个操作可以并发完成,由于每个进程都将WAL日志复制至自己预留的空间中,因此向WAL Buffer中复制的过程不会产生冲突。由于多个进程需要向同一个WAL Buffer中并发写入日志,所以这是数据库的一个性能瓶颈点,PG将预留空间和数据复制做分离就是一个非常重要的优化。

在XLogCtl->Insert->insertpos_lck锁的保护下预留空间,保证了各个事务占用的空间不会重叠。一旦事务所占用的空间被预留,则数据复制的过程是可以并发的,PG通过WALInsertLocks锁来控制并发复制的过程。PG声明了NUM_XLOGINSERT_LOCKS(目前是8)个WALInsertLocks,每个WALInsertLocks由轻量级+日志写入位置组成。不同backends不同事务在刷入日志时会随机(参照自己的MyProc->pgprocno)获取一个WALInsertLocks。

读者可能会疑惑这个插入锁槽的作用,看起来锁槽只有8个会降低wal插入的并发度,它存在的意义是什么呢?那我现在再抛出另外一个问题:下图中多个进程同时在写wal日志,此时进程4发生事务提交,那么就要立即触发wal刷写操作,此时进程应该刷写哪些wal日志呢?

PostgreSQL数据库WAL——XLogInsertRecord将日志记录写入WAL日志缓存_日志记录_02

那进程4发生了事务提交,为了保证刷写的数据的正确性和完整性,就需要等待lsn4_e之前的预留位置全部写完,事实上PostgreSQL的wal刷写代码对8个wal插入锁槽进行一次遍历(忽略未使用的锁槽,对于正在使用锁槽会等待其发生一次锁释放或锁槽状态改变),这个遍历的过程大家有兴趣可以阅读WaitXLogInsertionsToFinish()函数,遍历完成后就可以保证lsn4_e之前的预留空间的并发写入已经完成。由于每一次wal刷写都会遍历wal插入锁槽,所以锁槽个数不宜过大,在PostgreSQL代码中将锁槽的个数写死为8个,可以参阅NUM_XLOGINSERT_LOCKS宏。

typedef struct{
LWLock lock;// 轻量锁,当锁释放时,代表WAL日志记录已经写入WAL buffer
// 记录当前WAL日志记录刷入WAL buffer的进展,对于一些小型的WAL日志记录,通常不会跨页面写入,也就不会更新insertingAt,通常在WAL日志记录比较长的时候才可能更新这个变量
XLogRecPtr insertingAt;
// 在WAL日志记录中,有一些和数据无关的日志记录,这些记录不影响lastImportantAt的值
// 注:这种与数据无关的日志记录是数据库运行的状态信息,这些日志记录即使丢失,也不影响数据库数据的一致性
XLogRecPtr lastImportantAt;
}

在事务提交时,需要将WAL日志刷入磁盘文件,如果当前事务的WAL日志已经全部写入WAL Buffer,但是有另一个事务先于本事务预留了空间,而WAL日志记录还没有完成WAL Buffer的复制,则本事务就无法把WAL Buffer中的数据刷出去,否则会有一段WAL日志不完整,此时需要借助WALInsertLocks中的insertingAt来解决。
一个事务是否能将WAL Buffer中的数据刷入磁盘,取决于两个因素:

  • 如果当前没有其他进程持有WALInsertLocks锁,就代表之前的WAL日志记录肯定已经完成了数据复制
  • 如果有其他进程获得了WALInsertLocks,但是它的insertingAt大于当前事务要刷入的LSN,则WAL Buffer刷入磁盘也没有问题
XLogRecPtr XLogInsertRecord(XLogRecData *rdata, XLogRecPtr fpw_lsn, uint8 flags) {
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;

/* cross-check on whether we should be here or not */
if (!XLogInsertAllowed()) elog(ERROR, "cannot make new WAL entries during recovery");
// 如果进行WAL日志段的切换,则此时其他进程就不能预订空间
START_CRIT_SECTION();
if (isLogSwitch) WALInsertLockAcquireExclusive();
else WALInsertLockAcquire();

if (RedoRecPtr != Insert->RedoRecPtr) {
RedoRecPtr = Insert->RedoRecPtr;
}

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. */
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;
}

if (inserted) {
/* Now that xl_prev has been filled in, calculate CRC of the record header. 将WAL日志记录复制到WAL Buffer */
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. */
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. */
if ((flags & XLOG_MARK_UNIMPORTANT) == 0) {
int lockno = holdingAllLocks ? 0 : MyLockNo;
WALInsertLocks[lockno].l.lastImportantAt = StartPos;
}
}else{
/* 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 shared LogwrtRqst.Write, if we crossed page boundary. */
if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
{
SpinLockAcquire(&XLogCtl->info_lck);
/* advance global request to include new block(s) */
if (XLogCtl->LogwrtRqst.Write < EndPos)
XLogCtl->LogwrtRqst.Write = EndPos;
/* update local result copy while I have the chance */
LogwrtResult = XLogCtl->LogwrtResult;
SpinLockRelease(&XLogCtl->info_lck);
}

/*
* If this was an XLOG_SWITCH record, flush the record and the empty
* padding space that fills the rest of the segment, and perform
* end-of-segment actions (eg, notifying archiver).
*/
if (isLogSwitch)
{
TRACE_POSTGRESQL_WAL_SWITCH();
XLogFlush(EndPos);

/*
* Even though we reserved the rest of the segment for us, which is
* reflected in EndPos, we return a pointer to just the end of the
* xlog-switch record.
*/
if (inserted)
{
EndPos = StartPos + SizeOfXLogRecord;
if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
{
uint64 offset = XLogSegmentOffset(EndPos, wal_segment_size);

if (offset == EndPos % XLOG_BLCKSZ)
EndPos += SizeOfXLogLongPHD;
else
EndPos += SizeOfXLogShortPHD;
}
}
}

/* Update our global variables */
ProcLastRecPtr = StartPos;
XactLastRecEnd = EndPos;

return EndPos;
}

PostgreSQL数据库WAL——XLogInsertRecord将日志记录写入WAL日志缓存_database_03