一、VM概述
He3DB for PG中为了实现多版本并发控制,当事务删除或更新元组时,并非从物理上删除,而是将其标记为无效的方式进行标记删除,最终对这些无效元组的清理操作需要调用VACUUM完成。
为了能够加快VACUUM查找包含无效元组的文件块的过程,在PostgreSQL8.4.1中为每个表文件定义了一个新的附属文件–可见性映射表(VM)。VM中为表的每个文件块设置了一位,用来标记该文件块是否存在无效元组。对包含无效元组的文件块,VACUUM有两种方式处理,即快速清理(Lazy VACUUM)和完全清理(Full VACUUM)。VM文件仅在Lazy VACUUM操作中被使用到,而Full VACUUM操作由于要执行跨块清理等复杂操作,需要对整个表文件进行扫描,这时候VM文件的作用并不大。当前,VM文件仅仅是作为一个提示(hint)来加快VACUUM的速度,所以即使VM文件损坏也仅仅会导致VACUUM忽略那些需要清理的页面,而不会对数据产生任何负面影响。
与其他文件一样,VM文件也被划分为若干个文件块(简称VM块)。VM块中除了必要的标记信息外,其他的每一位都对应一个表块,当表块中所有的元组对当前的事务都是可见的时候,表块对应的位才设置为1。其文件块结构如下图所示。
PageHeaderData | bit | bit | bit | bit … |
---|
每个VM文件块中能够记录size = (blcksz - SizeOfPageHeaderData) * 8个表块的信息,第一个VM块记录第1至size号表块的信息,第二个VM块记录第size + 1至2 * size + 1号表块的信息,依此类推。
当对某个表块中的元组进行更新或者删除后,那么该表块在VM文件中对应位置的标志位将被置0,表示有无效元组。在设置标志位的时候,需要对其对应的VM页面加锁。这是为了避免在VACUUM判断该页面是否对所有事务可见的同时,其他进程修改该页面,从而导致VACUUM清理过程中忽略了此页面。
当标志位为1时,表示没有无效元组,VACUUM操作会忽略扫描对应的表块,所以能大大提高VACUUM的效率。由于VM文件不跟踪索引,所以对索引的清理操作还是需要进行完全扫描。
二、VM源码解析
1、visibilitymap_set
功能:
- 设置可见性标志位
void
visibilitymap_set(Relation rel, BlockNumber heapBlk, Buffer heapBuf,
XLogRecPtr recptr, Buffer vmBuf, TransactionId cutoff_xid,
uint8 flags)
{
#ifdef HEAP_DEBUG1
elog(DEBUG1,"visibilitymap_set heapBlk=%u,heapBuf=%d,recptr=%X/%X,vmBuf=%d,cutoff_xid=%u,flags=%u",heapBlk,heapBuf,(uint32) ((recptr) >> 32), ((uint32) (recptr)),vmBuf,cutoff_xid,flags);
#endif
/* 根据堆块号,计算对应的可见性映射块号、字节偏移和位偏移 */
BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
uint32 mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
uint8 mapOffset = HEAPBLK_TO_OFFSET(heapBlk);
Page page;
uint8 *map;
#ifdef TRACE_VISIBILITYMAP
elog(DEBUG1, "vm_set %s %d", RelationGetRelationName(rel), heapBlk); // 输出调试信息,帮助开发者跟踪函数的执行
#endif
Assert(InRecovery || XLogRecPtrIsInvalid(recptr));
Assert(InRecovery || BufferIsValid(heapBuf));
Assert(flags & VISIBILITYMAP_VALID_BITS);
/* 确保传入的堆缓冲区和可见性映射缓冲区与预期的块号相匹配 */
if (BufferIsValid(heapBuf) && BufferGetBlockNumber(heapBuf) != heapBlk)
elog(ERROR, "wrong heap buffer passed to visibilitymap_set");
if (!BufferIsValid(vmBuf) || BufferGetBlockNumber(vmBuf) != mapBlock)
elog(ERROR, "wrong VM buffer passed to visibilitymap_set");
page = BufferGetPage(vmBuf);
map = (uint8 *) PageGetContents(page);
LockBuffer(vmBuf, BUFFER_LOCK_EXCLUSIVE); // 对可见性映射页加排他锁
if (flags != (map[mapByte] >> mapOffset & VISIBILITYMAP_VALID_BITS)) // 比较当前可见性映射位与提供的flags,判断是否需要更新映射
{
START_CRIT_SECTION();
map[mapByte] |= (flags << mapOffset); // 更新可见性映射位
MarkBufferDirty(vmBuf); // 标记页为脏页
if (RelationNeedsWAL(rel)) // 需要wal
{
if (XLogRecPtrIsInvalid(recptr)) // 当前没有有效的日志位置
{
Assert(!InRecovery);
recptr = log_heap_visible(rel->rd_node, heapBuf, vmBuf, cutoff_xid, flags); // 生成日志记录,并更新recptr
if (XLogHintBitIsNeeded())
{
Page heapPage = BufferGetPage(heapBuf);
/* caller is expected to set PD_ALL_VISIBLE first */
Assert(PageIsAllVisible(heapPage));
PageSetLSN(heapPage, recptr);
}
Page heapPage = BufferGetPage(heapBuf);
/* caller is expected to set PD_ALL_VISIBLE first */
PageSetLSN(heapPage, recptr);
MarkBufferDirty(heapBuf);
}
PageSetLSN(page, recptr);
}
END_CRIT_SECTION();
}
LockBuffer(vmBuf, BUFFER_LOCK_UNLOCK);
}
关键流程:
- 调试和跟踪:使用条件编译和日志记录功能,根据是否定义了
HEAP_DEBUG1
或TRACE_VISIBILITYMAP
来输出调试信息。 - 参数校验:检查是否处于恢复模式(
InRecovery
),缓冲区是否有效,以及传递的堆缓冲区和可见性映射缓冲区是否与预期的块号相匹配。 - 计算映射位置:根据堆块的块号计算对应的可见性映射块号、字节偏移和位偏移。
- 加锁:对可见性映射页加排他锁,以确保并发修改的安全。
- 比较并更新映射:如果当前可见性映射位与提供的
flags
不同,则进入临界区,更新映射位,并标记页为脏页。 - WAL处理:如果表需要WAL日志,并且当前没有有效的日志位置(
recptr
),则生成一个日志记录,并更新相关的LSN。同时,如果堆页是“全部可见”的,并且设置了相关的位,也会更新堆页的LSN并标记为脏页。 - 解锁:更新完成后,释放对可见性映射页的锁。
2、vm_readbuf
功能:
- 负责将指定VM页加载至缓冲区中,若有需要会进行
extend
生成新页并进行初始化
static Buffer
vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
{
#ifdef HEAP_DEBUG1
elog(DEBUG1,"vm_readbuf blkno=%u,extend=%d",blkno,extend); // 记录要读取的块号和是否扩展的标志
#endif
Buffer buf;
SMgrRelation reln;
/*
* Caution: re-using this smgr pointer could fail if the relcache entry
* gets closed. It's safe as long as we only do smgr-level operations
* between here and the last use of the pointer.
*/
reln = RelationGetSmgr(rel); // 获取关系的管理器指针,该指针用于管理存储文件
/*
* 如果还没有缓存可见性映射分支的大小,就检查并缓存它。如果文件不存在,则设置大小为0
*/
if (reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] == InvalidBlockNumber)
{
if (smgrexists(reln, VISIBILITYMAP_FORKNUM))
smgrnblocks(reln, VISIBILITYMAP_FORKNUM);
else
reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] = 0;
}
/* 如果请求的块号超出了当前可见性映射文件的大小 */
if (blkno >= reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM])
{
if (extend)
vm_extend(rel, blkno + 1); // 扩展文件
else
return InvalidBuffer; // 不扩展
}
buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
RBM_ZERO_ON_ERROR, NULL); // 读取页面
#ifdef HEAP_DEBUG1
elog(DEBUG1,"vm_readbuf buf=%d",buf);
#endif
if (PageIsNew(BufferGetPage(buf))) // 页面为新
{
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); // 加排他锁
if (PageIsNew(BufferGetPage(buf)))
PageInit(BufferGetPage(buf), BLCKSZ, 0); // 初始化页面
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
}
return buf;
}
关键流程:
- 调试日志:首先,如果定义了
HEAP_DEBUG1
宏,则记录要读取的块号和是否扩展的标志。 - 获取存储管理器指针:通过
RelationGetSmgr(rel)
获取当前表的关系存储管理器(SMgrRelation)指针,该指针用于管理表的存储文件。 - 检查并缓存可见性映射文件大小:如果还没有缓存可见性映射分支的大小,则检查该分支是否存在,并缓存其大小。如果文件不存在,则设置大小为0。
- 处理超出范围的块号:如果请求的块号超出了当前可见性映射文件的大小,根据
extend
参数决定是扩展文件还是返回InvalidBuffer
。如果extend
为true
,则调用vm_extend
函数扩展文件至请求的块号之后的一个块。 - 读取块:使用
ReadBufferExtended
函数从可见性映射文件中读取指定的块。如果页面是新分配的(即之前未被使用),则加排他锁并初始化页面。初始化包括调用PageInit
函数来设置页面的基本属性,如页面大小(BLCKSZ
)和页面是否为空(0
表示空)。 - 返回缓冲区:最后,返回包含读取或新初始化页面的缓冲区。
3、vm_extend
功能:
- 确保可见性映射文件的长度至少为
vm_nblocks
指定的块数。 如果当前长度不足,将扩展文件,并在新添加的页面上填充零。
static void
vm_extend(Relation rel, BlockNumber vm_nblocks)
{
#ifdef HEAP_DEBUG1
elog(DEBUG1,"vm_extend vm_nblocks=%u",vm_nblocks);
#endif
BlockNumber vm_nblocks_now;
PGAlignedBlock pg;
SMgrRelation reln;
PageInit((Page) pg.data, BLCKSZ, 0);
LockRelationForExtension(rel, ExclusiveLock); // 加扩展锁,防止其他后端同时扩展可见性映射文件或主文件
reln = RelationGetSmgr(rel); // 获取关系的存储管理器指针
if ((reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] == 0 ||
reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] == InvalidBlockNumber) &&
!smgrexists(reln, VISIBILITYMAP_FORKNUM))
smgrcreate(reln, VISIBILITYMAP_FORKNUM, false); // 可见性映射文件不存在,则创建
/* Invalidate cache so that smgrnblocks() asks the kernel. */
reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] = InvalidBlockNumber;
vm_nblocks_now = smgrnblocks(reln, VISIBILITYMAP_FORKNUM); // 从内核获取最新的文件大小
#ifdef HEAP_DEBUG1
elog(DEBUG1,"vm_extend vm_nblocks_now=%u",vm_nblocks_now);
#endif
/* Now extend the file */
while (vm_nblocks_now < vm_nblocks)
{
PageSetChecksumInplace((Page) pg.data, vm_nblocks_now); // 设置页面的校验和
smgrextend(reln, VISIBILITYMAP_FORKNUM, vm_nblocks_now, pg.data, false); // 扩展文件
vm_nblocks_now++;
}
CacheInvalidateSmgr(reln->smgr_rnode); // 发送一个共享无效消息,强制其他后端关闭可能持有的该关系的存储管理器引用
UnlockRelationForExtension(rel, ExclusiveLock); // 释放扩展锁
}
关键流程:
-
调试日志:如果定义了
HEAP_DEBUG1
宏,则记录要扩展到的块数vm_nblocks
。 -
变量初始化:声明并初始化一些变量。
-
加扩展锁:通过
LockRelationForExtension
函数对关系加排他锁,以防止其他后端同时扩展可见性映射文件或主文件。 -
获取存储管理器指针:通过
RelationGetSmgr
函数获取当前表的存储管理器指针。 -
检查并创建可见性映射文件:如果可见性映射文件不存在,则通过
smgrcreate
函数创建它。 -
更新文件大小信息:将
smgr_cached_nblocks[VISIBILITYMAP_FORKNUM]
设置为InvalidBlockNumber
以强制smgrnblocks
从内核获取最新的文件大小。然后调用smgrnblocks
获取当前的可见性映射文件大小vm_nblocks_now
。 -
扩展文件:在一个循环中,只要当前文件大小
vm_nblocks_now
小于目标大小vm_nblocks
,就执行以下操作:- 使用
PageSetChecksumInplace
函数为将要写入的页面设置校验和。 - 调用
smgrextend
函数将文件扩展到下一个块,并写入之前“初始化”的页面数据。 - 增加
vm_nblocks_now
以继续循环,直到达到或超过目标块数。
- 使用
-
发送共享无效消息:通过
CacheInvalidateSmgr
函数发送一个共享无效消息,强制其他后端关闭可能持有的该关系的存储管理器引用。这是为了确保所有后端都看到最新的文件扩展情况。 -
释放扩展锁:通过
UnlockRelationForExtension
函数释放之前加的排他锁。