相关 《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可配的可见性状态:
- 都可见:VISIBILITYMAP_ALL_VISIBLE
- 都可见:全部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_VISIBLE
和0x02 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。
- 这里有一个race condition:拿着页面锁,PD_ALL_VISIBLE被配置了。这种情况需要放页面锁,pin vm,加页面锁,在加vm页面锁,在写vm。
整体结构
- vm的功能在一些书中都有过介绍,主要是用二进制位标记堆页面内是否存在脏元组,每个页面对应一个二进制位,如果不存在脏元组vacuum可以跳过该页面,避免8k重IO节省大量时间(加速vacuum)。在实践中vm不止这一项功能,在一些可见性判断场景也有应用(例如index only scan下篇介绍)
- vm对外:提供了8个对外的接口函数(图中extern function)和两个宏:
VM_ALL_VISIBLE
、VM_ALL_FROZEN
- vm对内:提供2个内部函数(图中static function)
- vm依赖其他模块:SMGR(扩展时使用)和缓冲区管理器(读、锁页面等),这两部分的内容见文章顶部之前做过分析。
- 其他模块依赖vm:堆页面操作、vacuum等
详见下图:
extern函数分析
1 visibilitymap_set
- 参数准入:
- 不应存在:没在recovery状态,但是传了一个有效的recptr
- 不应存在:没在recovery状态,但是heapBuf是有效的
- 必要条件:flag只能是
VISIBILITYMAP_ALL_VISIBLE
或VISIBILITYMAP_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可配的可见性状态:
- 都可见:VISIBILITYMAP_ALL_VISIBLE
- 都可见:全部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