参考资料
《PostgreSQL数据库内核分析》 彭智勇 彭煜玮:P99~P101
概述
在PostgreSQL中,任何对于表、元组、索引等操作都在缓冲池中进行,缓冲池的数据调度都以磁盘块为单位,需要访问的数据块以磁盘块为单位调用函数smgrread写入缓冲区,而smgrwrite将缓冲池数据写回磁盘。调入缓冲池中的磁盘块称为缓冲区,多个缓冲区组成的缓冲池。
PostgreSQL有两种缓冲池:共享缓冲池和本地缓冲池。共享缓冲池主要作为普通表的操作场所,本地缓冲池则仅本地可见的临时表的操作场所。本文仅对共享缓冲池进行阐述。
对缓冲池中,缓冲区的管理通过两种机制完成:
-
pin
当进程要访问缓冲区前,对于缓冲区加pin,pin的数目保存在缓冲区的refcount属性中。当refcount不为0时表明有进程正在访问缓冲区,此时该缓冲区不能被替换。
-
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主要做三件事:
-
初始化BufferDescriptors。
-
初始化BufferBlocks。
-
初始化缓冲区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

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





