海山数据库(He3DB)源码详解:海山PG 可见性映射表VM

一、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。其文件块结构如下图所示。

PageHeaderDatabitbitbitbit …

每个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);
}

关键流程:

  1. 调试和跟踪:使用条件编译和日志记录功能,根据是否定义了HEAP_DEBUG1TRACE_VISIBILITYMAP来输出调试信息。
  2. 参数校验:检查是否处于恢复模式(InRecovery),缓冲区是否有效,以及传递的堆缓冲区和可见性映射缓冲区是否与预期的块号相匹配。
  3. 计算映射位置:根据堆块的块号计算对应的可见性映射块号、字节偏移和位偏移。
  4. 加锁:对可见性映射页加排他锁,以确保并发修改的安全。
  5. 比较并更新映射:如果当前可见性映射位与提供的flags不同,则进入临界区,更新映射位,并标记页为脏页。
  6. WAL处理:如果表需要WAL日志,并且当前没有有效的日志位置(recptr),则生成一个日志记录,并更新相关的LSN。同时,如果堆页是“全部可见”的,并且设置了相关的位,也会更新堆页的LSN并标记为脏页。
  7. 解锁:更新完成后,释放对可见性映射页的锁。

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;
}

关键流程:

  1. 调试日志:首先,如果定义了HEAP_DEBUG1宏,则记录要读取的块号和是否扩展的标志。
  2. 获取存储管理器指针:通过RelationGetSmgr(rel)获取当前表的关系存储管理器(SMgrRelation)指针,该指针用于管理表的存储文件。
  3. 检查并缓存可见性映射文件大小:如果还没有缓存可见性映射分支的大小,则检查该分支是否存在,并缓存其大小。如果文件不存在,则设置大小为0。
  4. 处理超出范围的块号:如果请求的块号超出了当前可见性映射文件的大小,根据extend参数决定是扩展文件还是返回InvalidBuffer。如果extendtrue,则调用vm_extend函数扩展文件至请求的块号之后的一个块。
  5. 读取块:使用ReadBufferExtended函数从可见性映射文件中读取指定的块。如果页面是新分配的(即之前未被使用),则加排他锁并初始化页面。初始化包括调用PageInit函数来设置页面的基本属性,如页面大小(BLCKSZ)和页面是否为空(0表示空)。
  6. 返回缓冲区:最后,返回包含读取或新初始化页面的缓冲区。

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);  // 释放扩展锁
}

关键流程:

  1. 调试日志:如果定义了 HEAP_DEBUG1 宏,则记录要扩展到的块数 vm_nblocks

  2. 变量初始化:声明并初始化一些变量。

  3. 加扩展锁:通过 LockRelationForExtension 函数对关系加排他锁,以防止其他后端同时扩展可见性映射文件或主文件。

  4. 获取存储管理器指针:通过 RelationGetSmgr 函数获取当前表的存储管理器指针。

  5. 检查并创建可见性映射文件:如果可见性映射文件不存在,则通过 smgrcreate 函数创建它。

  6. 更新文件大小信息:将 smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] 设置为 InvalidBlockNumber 以强制 smgrnblocks 从内核获取最新的文件大小。然后调用 smgrnblocks 获取当前的可见性映射文件大小 vm_nblocks_now

  7. 扩展文件:在一个循环中,只要当前文件大小 vm_nblocks_now 小于目标大小 vm_nblocks,就执行以下操作:

    • 使用 PageSetChecksumInplace 函数为将要写入的页面设置校验和。
    • 调用 smgrextend 函数将文件扩展到下一个块,并写入之前“初始化”的页面数据。
    • 增加 vm_nblocks_now 以继续循环,直到达到或超过目标块数。
  8. 发送共享无效消息:通过 CacheInvalidateSmgr 函数发送一个共享无效消息,强制其他后端关闭可能持有的该关系的存储管理器引用。这是为了确保所有后端都看到最新的文件扩展情况。

  9. 释放扩展锁:通过 UnlockRelationForExtension 函数释放之前加的排他锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值