PostgreSQL 基础模块---缓冲池管理

本文深入解析PostgreSQL的缓冲池操作,包括共享缓冲池结构、缓冲区管理机制、并发控制、替换策略(一般替换与缓冲环策略),以及为何选择特定流程以避免数据不一致和提高性能。

参考资料

《PostgreSQL数据库内核分析》 彭智勇 彭煜玮:P99~P101

概述

在PostgreSQL中,任何对于表、元组、索引等操作都在缓冲池中进行,缓冲池的数据调度都以磁盘块为单位,需要访问的数据块以磁盘块为单位调用函数smgrread写入缓冲区,而smgrwrite将缓冲池数据写回磁盘。调入缓冲池中的磁盘块称为缓冲区,多个缓冲区组成的缓冲池

PostgreSQL有两种缓冲池:共享缓冲池和本地缓冲池。共享缓冲池主要作为普通表的操作场所,本地缓冲池则仅本地可见的临时表的操作场所。本文仅对共享缓冲池进行阐述。

对缓冲池中,缓冲区的管理通过两种机制完成:

  1. pin

    当进程要访问缓冲区前,对于缓冲区加pin,pin的数目保存在缓冲区的refcount属性中。当refcount不为0时表明有进程正在访问缓冲区,此时该缓冲区不能被替换

  2. lock

    lock机制为缓冲区的并发访问提供了保障,当进程对缓冲区进行写操作时加EXCLUSIVE锁,读操作加SHARE锁。比如:Insert操作,在获取到缓冲区后需要先将缓冲区加EXCLUSIVE锁。(加锁操作在RelationGetBufferForTuple函数中进行,详见插入流程)。

初始化共享缓冲区

共享缓冲池的初始化工作由InitBufferPool来完成。在共享缓冲池管理中,使用了一个全局数组BufferDescriptors来管理缓冲池中的缓冲区,其数组元素类型为BufferDesc。另外使用了一个全局指针变量BufferBlocks来存储缓冲池的起始地址。

下面先来看看BufferDesc的定义:

typedef struct BufferDesc
{
   
   
	BufferTag	tag;					/* ID of page contained in buffer */
	int			buf_id;					/* buffer's index number (from 0) */

	/* state of the tag, containing flags, refcount and usagecount */
	pg_atomic_uint32 state;

	int			wait_backend_pid;		/* backend PID of pin-count waiter */
	int			freeNext;				/* link in freelist chain */

	LWLock		content_lock;			/* to lock access to buffer contents */
} BufferDesc;

其中:

  • tag:用于标识该缓冲块的物理信息,具体定义如下:

    typedef struct buftag
    {
         
         
    	RelFileNode rnode;			/* 表所在表空间oid,数据库oid,表本身oid组成 */
    	ForkNumber	forkNum;		/* 枚举类型,标记缓冲区中是什么类型的文件块 */
    	BlockNumber blockNum;		/* 块号 */
    } BufferTag;
    

    tag唯一标识了一个物理块,注意是物理块!(后面的缓冲区加载流程会再次用到tag)

  • buf_id:缓冲区的索引号,buf_id唯一标识了一个缓冲区。对缓冲区的各种操作都会用到buf_id。

    共享缓冲区和本地缓冲区都使用buf_id,他们的编号规则不同:共享缓冲区的buf_id从0开始编号,后续依次加1。而本地缓冲区的buf_id从-2开始编号,后续依次减1。

    /* 本地缓冲区从-2开始编号 */
    #define LocalBufHdrGetBlock(bufHdr) LocalBufferBlockPointers[-((bufHdr)->buf_id + 2)]
    
  • state:由flags、refcount、usagecount组成

    • flags:标志位,表示缓冲区是否为脏等。
    • refcount:表示当前正在引用该块缓冲区的进程数,通过pin操作来修改该字段。
    • usagecount:最近缓冲区使用次数,用于缓冲区替换。
  • wait_backend_pid:用于记录一个请求修改缓冲区的进程号。

  • freeNext:如果当前缓冲区在空闲链中,则freeNext指向下一个空闲缓冲区。

  • content_lock:当进程访问缓冲块时,会在content_lock上加锁,读访问加LW_SHARE锁,写访问加LW_EXCLUSIVE锁,此锁可以防止因多个进程对缓冲区访问的冲突而造成数据不一致。

缓冲区的操作

前面说到共享缓冲池管理中有两个全局变量:BufferDesc数组BufferDescriptors和BufferBlocks指针。那么这两个全局变量之间有什么关系,两者由如何转换?

首先,BufferDescriptors是一个数组,数组元素的个数为N。N=缓冲池中缓冲区的数量,默认值为1000。BufferBlocks是一段连续的内存空间,大小为BLCKSZ*N,所以BufferBlocks也可以理解为一个数组,数组元素个数为N,每个数组元素都是一个缓冲区。

在BufferDesc中有一个成员buf_id,这个值表示了当前的BufferDesc在BufferDescriptors中的下标,即

BufferDesc == BufferDescriptors[BufferDesc ->buf_id]。

所以根据buf_id就可以从BufferDescriptors中获取BufferDesc,也可以从BufferBlocks中获取实际的缓冲区。具体操作见如下函数:

/* 返回一个bufferid,后续的操作都是基于bufferid进行 */
#define BufferDescriptorGetBuffer(bdesc) ((bdesc)->buf_id + 1)
/* 从BufferDescriptors中获取一个BufferDesc */
#define GetBufferDescriptor(id) (&BufferDescriptors[(id)].bufferdesc)
/* 从BufferBlocks中获取一个缓冲区 */
#define BufferGetPage(buffer) ((Page)BufferGetBlock(buffer))

#define BufferIsLocal(buffer)	((buffer) < 0)	/* 判断是否是本地缓冲区 */

#define BufferGetBlock(buffer) \
( \
	AssertMacro(BufferIsValid(buffer)), \
	BufferIsLocal(buffer) ? \
		LocalBufferBlockPointers[-(buffer) - 1] \
	: \
		(Block) (BufferBlocks + ((Size) ((buffer) - 1)) * BLCKSZ) \
)

对于GetBufferDescriptor的调用需要的参数直接是BufferDesc的数组下标,但对于BufferGetBlock的调用需要的参数却必须是BufferDescriptorGetBuffer的返回值,即数组下标+1。目前尚不清楚为什么要这样设计。

InitBufferPool的主要功能

InitBufferPool主要做三件事:

  1. 初始化BufferDescriptors。

  2. 初始化BufferBlocks。

  3. 初始化缓冲区hash表。

    初始化缓冲区hash表,在StrategyInitialize中调用InitBufTable来完成。缓冲区hash表的作用在共享缓冲区的加载中来讲。

共享缓冲区加载(查询)

当PostgreSQL读写一个物理块时,首先需要把物理块读取到共享缓冲区中,然后再从缓冲区中读写数据。从物理块读取到共享缓冲区的过程称为共享缓冲区加载。ReadBuffer_common是所有缓冲区的通用函数,定义了本地缓冲区和共享缓冲区的通用读取方法。代码如下:

static Buffer
ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
				  BlockNumber blockNum, ReadBufferMode mode,
				  BufferAccessStrategy strategy, bool *hit)
{
   
   
	BufferDesc *bufHdr;
	Block		bufBlock;
	bool		found;
	bool		isExtend;
	bool		isLocalBuf = SmgrIsTemp(smgr);

	*hit = false;

	/* Make sure we will have room to remember the buffer pin */
	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);

	isExtend = (blockNum == P_NEW);

	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
									   smgr->smgr_rnode.node.spcNode,
									   smgr->smgr_rnode.node.dbNode,
									   smgr->smgr_rnode.node.relNode,
									   smgr->smgr_rnode.backend,
									   isExtend);

	/* Substitute proper block number if caller asked for P_NEW */
	if (isExtend)
		blockNum = smgrnblocks(smgr, forkNum);

	if (isLocalBuf)
	{
   
   
		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, &found);
		if (found)
			pgBufferUsage.local_blks_hit++;
		else
			pgBufferUsage.local_blks_read++;
	}
	else
	{
   
   
		/*
		 * lookup the buffer.  IO_IN_PROGRESS is set if the requested block is
		 * not currently in memory.
		 */
		bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum,
							 strategy, &found);
		if (found)
			pgBufferUsage.shared_blks_hit++;
		else
			pgBufferUsage.shared_blks_read++;
	}

	/* At this point we do NOT hold any locks. */

	/* if it was already in the buffer pool, we're done */
	if (found)
	{
   
   
		if (!isExtend)
		{
   
   
			/* Just need to update stats before we exit */
			*hit = true;
			VacuumPageHit++;

			if (VacuumCostActive)
				VacuumCostBalance += VacuumCostPageHit;

			TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
											  smgr->smgr_rnode.node.spcNode,
											  smgr->smgr_rnode.node.dbNode,
											  smgr->smgr_rnode.node.relNode,
											  smgr->smgr_rnode.backend,
											  isExtend,
											  found);

			/*
			 * In RBM_ZERO_AND_LOCK mode the caller expects the page to be
			 * locked on return.
			 */
			if (!isLocalBuf)
			{
   
   
				if (mode == RBM_ZERO_AND_LOCK)
					LWLockAcquire(BufferDescriptorGetContentLock(bufHdr),
								  LW_EXCLUSIVE);
				else if (mode 
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值