1 Buffer基本介绍
1.1 Buffer解决的问题
实际应用场景中,PostgreSQL可能需要1分钟处理数十万次事务,频繁读写Page。Buffer提供以下2个基本功能:
- 加载Page至内存:PostgreSQL需要读写数时,都需先从磁盘中,将指定的Page加载至内存中,再在内存中操作Page
- 缓存常用Page:Page被加载至内存后,还会在内存停留一段时间,降低PostgreSQL直接从磁盘读写Page的频率
1.2 Buffer的基本功能
Buffer缓存Page时,需考虑以下几个场景:
- 插入Page:大部分事务操作表的Page时,先从Buffer中查找Page,如果Page不在Buffer中,再从磁盘读取Page,将其放入Buffer中,再操作Buffer中的Page
- 查找Page:访问Page时,通过文件名和Page编号等信息可确定唯一Page
- 访问Page:所有事务均可访问Buffer,需考虑多事务并发读写同一Pape的场景
- 淘汰Page:Buffer能缓存的Page数量有限,在Page数量过多时,需淘汰一些Page
- 落盘Page:Buffer中通常缓存表数据文件的Page,前文提到,事务持久化的标志是WAL日志落盘,因此,Buffer中的Page无需实时落盘,仅需异步落盘即可
2 Buffer的基本原理
2.1 插入Page
插入Page主要由读Page操作触发,上层函数读取Page的主要流程如下:
- 上层模块统一调用ReadBuffer()接口,读取指定表的指定Page
- ReadBuffer()中,先从Buffer查找Page,如果找到Page,则直接返回Buffer中的Page
- 如果未从Buffer找到Page,现在Buffer中预留存储Page的空间
- 之后,调用Smgr模块,从磁盘读取指定Page,并将其放入预留的空间内,再返回Buffer中的Page
函数调用关系大概如下图所示:
ReadBuffer(Relation, BlockNumber) # 1
ReadBufferExtended()
ReadBuffer_common()
BufferAlloc() # 2,3 查找Page,如找到,返回;如未找到,预留存储Page的空间
smgrread(.., BlockNumber) # 4 如果未找到Page,从磁盘将Page读入预留的Page空间内,返回
2.2 查找Page
上层函数读Page时,首先进入查找Page阶段。
1.1 buffer池模型
模型:

1.2 buffer池接口
缓冲区接口:
/* bufmgr.h */
/* 1 buffer池管理 */
void InitBufferPool(void);
void InitBufferPoolAccess(void);
void InitBufferPoolBackend(void);
/* 2 buffer管理 */
/* 2.1 读buffer */
Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum, ReadBufferMode mode, BufferAccessStrategy strategy);
Buffer ReadBufferWithoutRelcache(RelFileNode rnode, ForkNumber forkNum, BlockNumber blockNum,ReadBufferMode mode, BufferAccessStrategy strategy);
/* 2.2 清理buffer */
oid ReleaseBuffer(Buffer buffer);
void UnlockReleaseBuffer(Buffer buffer);
void MarkBufferDirty(Buffer buffer);
void IncrBufferRefCount(Buffer buffer);
Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation, BlockNumber blockNum);
/* 2.3 刷盘buffer */
void CheckPointBuffers(int flags);
void FlushOneBuffer(Buffer buffer);
void FlushRelationBuffers(Relation rel);
void FlushDatabaseBuffers(Oid dbid);
void BufmgrCommit(void);
bool BgBufferSync(void);
/* 2.4 删除关联buffer */
void DropRelFileNodeBuffers(RelFileNodeBackend rnode, ForkNumber forkNum, BlockNumber firstDelBlock);
void DropRelFileNodesAllBuffers(RelFileNodeBackend *rnodes, int nnodes);
void DropDatabaseBuffers(Oid dbid);
/* 2.5 锁buffer */
void UnlockBuffers(void);
void LockBuffer(Buffer buffer, int mode);
bool ConditionalLockBuffer(Buffer buffer);
void LockBufferForCleanup(Buffer buffer);
bool ConditionalLockBufferForCleanup(Buffer buffer);
bool HoldingBufferPinThatDelaysRecovery(void);
1.3 buffer调用
插入数据
heap_insert()
RelationGetBufferForTuple()
/* 已有page,查找空闲的page */
GetPageWithFreeSpace()
/* 读取page,放入缓冲池 */
ReadBufferBI()
ReadBuffer() /* buffer接口 */
ReadBufferExtended()
ReadBuffer_common()
BufferAlloc() /* 遍历buffer池,查找 */
查找页面
BufferDesc BufferAlloc()
BufTableHashCode(newtag)
buf_id = BufTableLookup()
if buf_id:
buf_desc = GetBufferDescriptor(buf_id)
PinBuffer(buf_desc)
return buf_desc;
for (;;) {
buf_desc = StrategyGetBuffer()
if buf_desc dirty :
FlushBuffer(buf_desc)
if buf_desc tab_valid: /* 旧buf有数据,但旧buf可用 */
/* partition lock ? */
buf_id = BufTableInsert(newtag, buf_desc->buf_id) /* 可能失败,多进程一起申请同一page */
if buf_id:
return buf_desc
BufTableDelete(newtag)
}
BufTableDelete(oldtag)
2 实现
2.1 ReadBuffer
Buffer ReadBuffer(Relation reln, BlockNumber blockNum)
Buffer buf = ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL) /* 读场景 strategy = NULL */
Buffer buf = ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, BlockNumber blockNum, ReadBufferMode mode, BufferAccessStrategy strategy, bool *hit) {
/* 1. 根据表类型判断,使用LocalBuffer还是共享Buffer */
bool isLocalBuf = SmgrIsTemp(smgr)
if isLocalBuf:
LocalBufferAlloc()
else:
/* 选择1个buffer槽,{fornNum, BlockNum}等信息写入buffer槽 */
BufferDesc *bufHdr = BufferAlloc(smgr, forkNum, blockNum, strategy, &found) {
uint32 newHash = BufTableHashCode({smgr->smgr_rnode.node, forkNum, blockNum})
/* 2. 根据blockNum等信息,从哈希表中查找buf_id */
int buf_id = BufTableLookup(&newTag, newHash)
if buf_id >= 0: /* 如果找到(已在buffer池中) */
/* 3.1 根据buf_id,查找BufferDesc */
BufferDesc *buf = GetBufferDescriptor(buf_id)
/* 3.2 根据BufferDesc,引用计数++,使用计数++ */
PinBuffer(BufferDesc *buf)
buf->refcount++
buf->usage_count++
/* 异常处理 */
return buf
for (;;) /* 如果没找到 */
BufferDesc *buf = StrategyGetBuffer(strategy)
BufferDesc *buf = GetBufferFromRing()
bgwprocno = StrategyControl->bgwprocno
/* 4.1 唤醒bg writer */
SetLatch(&ProcGlobal->allProcs[bgwprocno].procLatch)
/* 4.2 检查是否有空闲Buffer */
if StrategyControl->firstFreeBuffer >= 0:
buf = GetBufferDescriptor(StrategyControl->firstFreeBuffer)
StrategyControl->firstFreeBuffer = buf->freeNext
/* 4.3 TODO */
/* 4.4 根据BufferDesc,引用计数++,使用计数++ */
if buf->flags & BM_DIRTY:
/* 4.5 将选中的Buffer落盘 */
FlushBuffer(buf)
if buf->flags & BM_TAG_VALID:
oldTag = buf->tag
oldHash = BufTableHashCode(&oldTag)
/* 4.6 将hash表中受害者Buffer对应的位置替换为即将写入的的BufferTag,-1表示成功,否则返回已存在的冲突的buffer id */
buf_id = BufTableInsert(&newTag, newHash, buf->buf_id)
/* 4.7 如果其他进程已经将新的buffer写入哈希表,直接返回即可 */
if buf_id >= 0:
PinBuffer(buf)
return buf
/* 4.8 替换映射成功 */
LockBufHdr(buf)
oldFlags = buf->flags
if buf->refcount == 1 && !(oldFlags & BM_DIRTY): /* 4.9 替换映射成功,且受害者buffer没有被重新写入数据 */
break;
/* 4.10 受害者buffer又被重新写入新数据,需要重新进入循环选择新的受害者 */
BufTableDelete(&newTag, newHash)
UnpinBuffer(buf, true)
/* 将受害者buffer对用的信息替换为新page,并将buffer槽标记为VM_INVALID */
buf->tag = newTag
buf->flags &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED | BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT)
buf->usage_count = 1
if oldFlags & BM_TAG_VALID:
BufTableDelete(&oldTag, oldHash)
return buf
}
/* 5. 如果page已在Buffer池中,直接返回即可 */
if found:
*hit = true
return BufferDescriptorGetBuffer(bufHdr)
/* 6. 从磁盘读取page,并且直接读如buffer槽中 */
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr)
smgrread(smgr, forkNum, blockNum, bufBlock)
if !PageIsVerified(bufBlock):
ereport ...
/* 7. 在bufHdr中将buffer设置未BM_VALID */
TerminateBufferIO(bufHdr, false, BM_VALID)
return BufferDescriptorGetBuffer(bufHdr)
}