Postgresql源码(54)visibilitymap基础功能分析

2022-06-15 18:17:51 浏览数 (1)

相关 《Postgresql源码(5)缓冲区管理》 《Postgresql存储底层封装smgr源码分析》

总结

一、映射算法

代码语言:javascript复制
/* 使用方法 */
  BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
  uint32		mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
  uint8		mapOffset = HEAPBLK_TO_OFFSET(heapBlk);

/* 计算公式 */
// 页面能使用的空间:MAPSIZE = 8168 = 8192 - 24
#define MAPSIZE (BLCKSZ - MAXALIGN(SizeOfPageHeaderData))
// 一个字节能存几个页面信息 = 一个字节8位/一个页面需要两位 = HEAPBLOCKS_PER_BYTE = 8 / 2 = 4
#define HEAPBLOCKS_PER_BYTE (BITS_PER_BYTE / BITS_PER_HEAPBLOCK)
// 一个vm页面能存多少个堆页面的可见性信息 = MAPSIZE * HEAPBLOCKS_PER_BYTE = 8168 * 4 = 32672
#define HEAPBLOCKS_PER_PAGE (MAPSIZE * HEAPBLOCKS_PER_BYTE)

// 一个vm页面存32672个信息,这里除32672确定在那个页面上
#define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE)
// 一个字节存4个信息,这里除4确定在那个字节上
#define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE)
// 一个字节8位,两位一个信息,这里先%4等于0-3,确定在哪个信息上
#define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK)

/* 例子 */
heapBlk = 32706
mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk) = 32706/32672 = 1
mapByte = HEAPBLK_TO_MAPBYTE(heapBlk) = 327062672/4 = 34/4 = 8
mapOffset = HEAPBLK_TO_OFFSET(heapBlk) = 32706%4*2 = 2*2 = 4

二、visibilitymap_set可配的可见性状态:

  1. 都可见:VISIBILITYMAP_ALL_VISIBLE
  2. 都可见:全部frozen:VISIBILITYMAP_ALL_FROZEN

三、函数

注意写都是有写锁的,基本加锁顺序是 页面写锁、vm页面写锁、写。页面写锁后,vm加锁发现vm不在内存中是要避免的,见下面“四、race condition”。

  • visibilitymap_get_status 【无锁】:读的时候不需要指定vmbuffer,只需给到堆页面的heapBlk,函数会计算出来vmbuffer并打开,并通过第三个参数返回。(注意调用者要解决race condition)
  • visibilitymap_set 【vm页面写锁】【堆页面无锁】:一般都是用上面函数先读,通过返回的第三个参数vmbuffer传入visibilitymap_set来配置状态位的。状态为使用2个bit表示,可表示4种状态,但现在只使用两种0x01 VISIBILITYMAP_ALL_VISIBLE0x02 VISIBILITYMAP_ALL_FROZEN
  • visibilitymap_clear 【vm页面写锁】【堆页面无锁】
  • visibilitymap_count 【无锁】 :(注意调用者要解决race condition)
  • visibilitymap_prepare_truncate 【无锁】 :无race condition

四、race condition

  • 在heapam.c对堆页面的操作中,当一个页面被修改了,那么这个页面就不在是 VISIBILITYMAP_ALL_VISIBLE 状态了。页面关联的vm位会被清空(visibilitymap_clear)。为了保证crash-safe,需要拿着页面锁然后拿着vm页面锁清空vm位。
  • 【重要】拿着页面锁然后去IO vm页面是要避免的:所以在锁页面之前先检查页面的标志位:PD_ALL_VISIBLE,如果PD_ALL_VISIBLE已经配了,我们就pin vm页面,把页面读上来。然后再加缓冲区锁、在加vm锁,这样避免了拿着缓冲区锁后,需要等待较长时间的vm 页面IO。
代码语言:txt复制
- 这里有一个race condition:拿着页面锁,PD_ALL_VISIBLE被配置了。这种情况需要放页面锁,pin vm,加页面锁,在加vm页面锁,在写vm。

整体结构

  • vm的功能在一些书中都有过介绍,主要是用二进制位标记堆页面内是否存在脏元组,每个页面对应一个二进制位,如果不存在脏元组vacuum可以跳过该页面,避免8k重IO节省大量时间(加速vacuum)。在实践中vm不止这一项功能,在一些可见性判断场景也有应用(例如index only scan下篇介绍)
  • vm对外:提供了8个对外的接口函数(图中extern function)和两个宏:VM_ALL_VISIBLEVM_ALL_FROZEN
  • vm对内:提供2个内部函数(图中static function)
  • vm依赖其他模块:SMGR(扩展时使用)和缓冲区管理器(读、锁页面等),这两部分的内容见文章顶部之前做过分析。
  • 其他模块依赖vm:堆页面操作、vacuum等

详见下图:

extern函数分析

1 visibilitymap_set

  • 参数准入:
    • 不应存在:没在recovery状态,但是传了一个有效的recptr
    • 不应存在:没在recovery状态,但是heapBuf是有效的
    • 必要条件:flag只能是VISIBILITYMAP_ALL_VISIBLEVISIBILITYMAP_ALL_FROZEN

映射算法:

代码语言:javascript复制
/* 使用方法 */
  BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
  uint32		mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
  uint8		mapOffset = HEAPBLK_TO_OFFSET(heapBlk);

/* 计算公式 */
// 页面能使用的空间:MAPSIZE = 8168 = 8192 - 24
#define MAPSIZE (BLCKSZ - MAXALIGN(SizeOfPageHeaderData))
// 一个字节能存几个页面信息 = 一个字节8位/一个页面需要两位 = HEAPBLOCKS_PER_BYTE = 8 / 2 = 4
#define HEAPBLOCKS_PER_BYTE (BITS_PER_BYTE / BITS_PER_HEAPBLOCK)
// 一个vm页面能存多少个堆页面的可见性信息 = MAPSIZE * HEAPBLOCKS_PER_BYTE = 8168 * 4 = 32672
#define HEAPBLOCKS_PER_PAGE (MAPSIZE * HEAPBLOCKS_PER_BYTE)

// 一个vm页面存32672个信息,这里除32672确定在那个页面上
#define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE)
// 一个字节存4个信息,这里除4确定在那个字节上
#define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE)
// 一个字节8位,两位一个信息,这里先%4等于0-3,确定在哪个信息上
#define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK)

/* 例子 */
heapBlk = 32706
mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk) = 32706/32672 = 1
mapByte = HEAPBLK_TO_MAPBYTE(heapBlk) = 327062672/4 = 34/4 = 8
mapOffset = HEAPBLK_TO_OFFSET(heapBlk) = 32706%4*2 = 2*2 = 4

visibilitymap_set可配的可见性状态:

  1. 都可见:VISIBILITYMAP_ALL_VISIBLE
  2. 都可见:全部frozen:VISIBILITYMAP_ALL_FROZEN

代码分析:

代码语言:javascript复制
void
visibilitymap_set(Relation rel, BlockNumber heapBlk, Buffer heapBuf,
				  XLogRecPtr recptr, Buffer vmBuf, TransactionId cutoff_xid,
				  uint8 flags)
{
	BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
	uint32		mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
	uint8		mapOffset = HEAPBLK_TO_OFFSET(heapBlk);
	Page		page;
	uint8	   *map;

  ...
  
	page = BufferGetPage(vmBuf);
	map = (uint8 *) PageGetContents(page);
	LockBuffer(vmBuf, BUFFER_LOCK_EXCLUSIVE);

// 要配置为flags的标志位,但是当前的标志位和flags不同,进入配置
	if (flags != (map[mapByte] >> mapOffset & VISIBILITYMAP_VALID_BITS))
	{
		START_CRIT_SECTION();

		map[mapByte] |= (flags << mapOffset);
		MarkBufferDirty(vmBuf);

		if (RelationNeedsWAL(rel))
		{
			if (XLogRecPtrIsInvalid(recptr))
			{
        // 写一条XLOG_HEAP2_VISIBLE日志发到备机
				recptr = log_heap_visible(rel->rd_node, heapBuf, vmBuf,
										  cutoff_xid, flags);
        ...
			}
      // 注意这里:要给vm页面记录这条日志的lsn
			PageSetLSN(page, recptr);
		}

		END_CRIT_SECTION();
	}
  // 放锁
	LockBuffer(vmBuf, BUFFER_LOCK_UNLOCK);
}

visibilitymap_clear

给堆页面加锁写后处理、并MarkBufferDirty

代码语言:javascript复制
bool
visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer buf, uint8 flags)
{
	BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
	int			mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
	int			mapOffset = HEAPBLK_TO_OFFSET(heapBlk);
	uint8		mask = flags << mapOffset;
	char	   *map;
	bool		cleared = false;

  ...

	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
	map = PageGetContents(BufferGetPage(buf));

	if (map[mapByte] & mask)
	{
		map[mapByte] &= ~mask;

		MarkBufferDirty(buf);
		cleared = true;
	}

	LockBuffer(buf, BUFFER_LOCK_UNLOCK);

	return cleared;
}

visibilitymap_get_status

读的时候不需要指定vmbuffer,只需给到堆页面的heapBlk,函数会计算出来vmbuffer并打开,并通过第三个参数返回。

代码语言:javascript复制
uint8
visibilitymap_get_status(Relation rel, BlockNumber heapBlk, Buffer *buf)
{
	BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
	uint32		mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
	uint8		mapOffset = HEAPBLK_TO_OFFSET(heapBlk);
	char	   *map;
	uint8		result;

	if (BufferIsValid(*buf))
	{
		if (BufferGetBlockNumber(*buf) != mapBlock)
		{
			ReleaseBuffer(*buf);
			*buf = InvalidBuffer;
		}
	}

	if (!BufferIsValid(*buf))
	{
		*buf = vm_readbuf(rel, mapBlock, false);
		if (!BufferIsValid(*buf))
			return false;
	}

	map = PageGetContents(BufferGetPage(*buf));
	result = ((map[mapByte] >> mapOffset) & VISIBILITYMAP_VALID_BITS);
	return result;
}

static函数分析

1 vm_readbuf

1、SMGR打开表文件

2、ReadBufferExtended打开指定blkno

3、如果是新页面PageInit

注意:ReadBufferExtended读出来默认会Pin上

2 vm_extend(Relation rel, BlockNumber vm_nblocks)

1、保证当前rel的vm页面不少与vm_nblocks

2、调用smgrextend自己批量扩展(ReadBuffer_common也能扩展,传invalid block num即可,不过只扩展一个)

3、扩展完了读的时候再PageInit

0 人点赞