背景
在研究PG社区日志并行回放方案时,发现为并行进程中设计了invalid page worker。由于回放的目标数据页可能被后续的drop操作删除,导致页面丢失或数据不一致。可是xlog在进行回放时是按序的,为什么后续对页面的操作会影响到当前的回放呢?为了研究这个问题,需要从drop相关的操作入手。
Drop table
drop table的操作主要调用RemoveRelation函数来完成删除。该函数实现了DROP TABLE, DROP INDEX, DROP SEQUENCE, DROP VIEW等多种操作。其简化流程如下图所示。
doDeletion中物理删除表时,将要删除的对象添加至pendingDeletes链表中,在事务提交时进行真正的删除。
void
RelationDropStorage(Relation rel)
{
PendingRelDelete *pending;
/* Add the relation to the list of stuff to delete at commit */
pending = (PendingRelDelete *)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->relnode = rel->rd_node;
pending->backend = rel->rd_backend;
pending->atCommit = true; /* delete if commit */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
pendingDeletes = pending;
RelationCloseSmgr(rel);
}
事务提交时会通过
smgrdounlinkall
函数对物理文件进行删除。Redo
对涉及数据库页面修改的xlog日志,在进行回放前会通过
XLogReadBufferForRedoExtended
函数获取需要回放的buffer页面。如果之后存在drop等操作,该页面可能为一个invalid page。考虑一个场景:
-
insert/update操作,对pageA写入数据。
-
Drop table,将pageA删除,pageA对应的磁盘blockA也被删除。
-
崩溃。
在进行崩溃恢复时,回放insert/update操作,发现需要回放的page无对应的block,标记为invalid page。回放过程中的invalid page使用hash表进行维护。当回放操作发现需要回放的page是invalid page时,则跳过回放。当回放drop操作的事务提交回放时,将该invalid page从hash表中删除。
当数据达到一致性时,invalid page的数量应该为0。
Full page write
当开启full page write时不会出现上述情况。
在尝试读buffer时会获取或创建相关relation的block。并通过指定blocknumber与获取到的blocknumber进行比较。若新建的blocknumber小于指定blocknumber,则认为原指定的block已不存在,在未开启full page write的情况下会被标记为invalid page。
Buffer
XLogReadBufferExtended(RelFileNode rnode, ForkNumber forknum,
BlockNumber blkno, ReadBufferMode mode)
{
BlockNumber lastblock;
Buffer buffer;
SMgrRelation smgr;
Assert(blkno != P_NEW);
/* Open the relation at smgr level */
smgr = smgropen(rnode, InvalidBackendId);
smgrcreate(smgr, forknum, true);
lastblock = smgrnblocks(smgr, forknum);
if (blkno < lastblock)
{
/* page exists in file */
buffer = ReadBufferWithoutRelcache(rnode, forknum, blkno,
mode, NULL);
}
else
{
/* hm, page doesn't exist in file */
if (mode == RBM_NORMAL)
{
log_invalid_page(rnode, forknum, blkno, false);
return InvalidBuffer;
}
if (mode == RBM_NORMAL_NO_LOG)
return InvalidBuffer;
如果开启full page write,则会尝试回放整个buffer页面。会不断获取新的buffer页面和block,直到分配到的buffer对应的blocknum等于指定的blocknum。
do
{
if (buffer != InvalidBuffer)
{
if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
ReleaseBuffer(buffer);
}
buffer = ReadBufferWithoutRelcache(rnode, forknum,
P_NEW, mode, NULL);
}
while (BufferGetBlockNumber(buffer) < blkno);
并行回放invalid page worker
回放过程中可能会存在invalid page,并由hash表进行记录。当回放完毕时,invalid page的数量应该保持为0。
原本的回放过程中,回放过程遇到invalid page时会使用
log_invalid_page
对其进行记录;在进行 xact_redo_commit
事务提交重做时,如果事务中涉及到drop table,需对产生invalid page进行清理。XLogDropRelation(RelFileNode rnode, ForkNumber forknum)
{
forget_invalid_pages(rnode, forknum, 0);
}
在并行回放方案中,将invalid page的记录和清除交由统一的worker进行处理。
switch(page->cmd)
{
case PR_LOG:
PR_log_invalid_page_int(page->node, page->forkno, page->blkno, page->present);
break;
case PR_FORGET_PAGES:
PR_forget_invalid_pages_int(page->node, page->forkno, page->blkno);
break;
case PR_FORGET_DB:
PR_forget_invalid_pages_db_int(page->dboid);
break;
case PR_CHECK_INVALID_PAGES:
PR_XLogCheckInvalidPages_int();
break;
default:
PR_failing();
elog(PANIC, "Inconsistent internal status.");
break;
}