暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

dawn1221 2024-09-23
48

一、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); }

关键流程:

  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 函数释放之前加的排他锁。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论