1.基本原理
缓冲管理器:主要是管理共享内和持久存储之间的数据传输,并可能对 DBMS 的性能产生重大影响。
缓冲区管理器、持久存储和后端进程之间的关系如下图所示:
2.缓冲区管理器结构
缓冲区管理器包括一个 缓冲区表、缓冲区描述符 和 缓冲池。
缓冲区描述符 :保存着页面的元数据,对应的页面则保存在缓冲池的槽位中。
缓冲池 :缓冲池存储数据文件的页,如表的页面。缓冲池是一个数组,每个插槽存储数据文件的一页。缓冲池数组的索引称为buffer_id。
相关函数 :BufferDesc *BufferAlloc
2.1 缓冲区标签
数据库为所有数据文件的每个页面分配一个唯一的标记,即缓冲区标签。
缓冲区标签由关系 文件节点 、 关系分支编号 和 页面块号
typedef struct buftag {
RelFileNode rnode; /* physical relation identifier */
ForkNumber forkNum;
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
(1)关系文件节点用于定位页面所属的关系,包含了表空间、数据库和表的oid。例如:(表空间,数据库, 表) -> (1663, 16384, 37721)
(2)关系分支编号用于定位关系文件的具体分支文件,一个关系可能有三种分支,分别是关系主体(main分支,编号为0)、空闲空间映射( fsm分支,编号为1)及可见性映射(vm分支,编号为2)
(3)页面块号则在具体分支文件中指明相应页面的偏移量,即页面号
例如,{(1663, 16384, 37721), 0, 7} 标签表示,在某个表空间(oid=1663)中,某个数据库(oid=16384)的某张表(oid=37721)的 0 号分支( 0代表关系表本体)的第 7 号页面。再比如,缓冲区标签 {(1663, 16384, 37721), 1, 3} 表示该表空闲空间映射文件的三号页面。关系本体 main 分支编号为0,空闲空间映射 fsm 分支编号为1。
2.2 缓冲区表
缓冲区表 :一个散列表(hash 表),存储着页面的buffer_tag与缓冲区描述符的 buffer_id 之间的映射关系。
缓冲表在逻辑上可分为三部分: 散列函数、散列桶槽、数据项
散列函数 :hashquickany
散列桶槽 :key值
数据项 : (页面的 buffer_tag,包含页面元数据的描述符的 buffer_id)
Notes
为了避免哈希函数的冲突,缓冲表采用了使用链表的分离链接方法来解决冲突。当数据项被映射至同一个桶槽时,该方法会将数据项(包括两个值,即页面的 buffer_tag 和包含页面元数据的描述符的 buffer_id)保存在一个链表中。例如,数据项 (Tag_A,id=1)表示在 buffer_id=1 对应的缓冲区描述符中,存储着页面 Tag_A 的元数据
页面简单访问流程:根据后端进程发送的请求,创建目标页面的 buffer_tag,然后将 buffer_tag 通过内置的散列函数映射到哈希桶槽,并分配 buffer_id, 即目标页面在缓冲池数组中存储的槽位的序号。
2.3缓冲区描述符
缓冲区描述符:保存着页面的元数据,对应的页面则保存在缓冲池的槽位中。缓冲区描述符的结构由BufferDesc结构定义。
2.3.1 缓冲区描述符结构体
结构体BufferDesc定义如下:
typedef struct BufferDesc {
BufferTag tag; /* ID of page contained in buffer */
/* state of the tag, containing flags, refcount and usagecount */
pg_atomic_uint64 state;
int buf_id; /* buffer's index number (from 0) */
ThreadId wait_backend_pid; /* backend PID of pin-count waiter */
LWLock* io_in_progress_lock; /* to wait for I/O to complete */
LWLock* content_lock; /* to lock access to buffer contents */
BufferDescExtra *extra;
#ifdef USE_ASSERT_CHECKING
volatile uint64 lsn_dirty;
endif
} BufferDesc;
参数说明:
tag :保存着目标页面的buffer_tag(2.1 缓冲区标签),缓冲区标签中对应的页面存储在相应的缓冲池槽中。
buffer_id :标识了缓冲区描述符,相当于对应缓冲池槽的buffer_id。
context_lock :轻量级锁,用于控制对相关页面的访问。
io_in_progress_lock : 轻量级锁,用于控制对相关页面的访问。
state : 用于保存相应页面的状态,主要状态如下:
/*
* Flags for buffer descriptors
*
* Note: TAG_VALID essentially means that there is a buffer hashtable
* entry associated with the buffer's tag.
*/
16 << 32) /* buffer is migrating */ define BM_IN_MIGRATE (1U <<
define BM_IS_TMP_BUF (1U << 21 << 32) /* temp buf, can not write to disk */
define BM_IS_META (1LU << 17 << 32)
define BM_LOCKED (1LU << 22 << 32) /* buffer header is locked */
define BM_DIRTY (1LU << 23 << 32) /* data needs writing */
define BM_VALID (1LU << 24 << 32) /* data is valid */
define BM_TAG_VALID (1LU << 25 << 32) /* tag is assigned */
define BM_IO_IN_PROGRESS (1LU << 26 << 32) /* read or write in progress */
define BM_IO_ERROR (1LU << 27 << 32) /* previous I/O failed */
define BM_JUST_DIRTIED (1LU << 28 << 32) /* dirtied since write started */
define BM_PIN_COUNT_WAITER (1LU << 29 << 32) /* have waiter for sole pin */
define BM_CHECKPOINT_NEEDED (1LU << 30 << 32) /* must write for checkpoint */
define BM_PERMANENT (1LU << 31 << 32) /* permanent relation (notunlogged, or init fork) */
BM_DIRTY : 脏位指明相应页面是否为脏页。
BM_VALID : 有效位指明相应页面是否可以被读写(有效)。例如,如果该位被设置为 “valid”,那就意味着对应的缓冲池槽中存储着一个页面,而该描述符中保存着该页面的元数据,因而可以对该页面进行读写。反之如果有效位被设置为 “invalid”,那就意味着该描述符中并没有保存任何元数据,即对应的页面无法读写,缓冲区管理器可能正在将该页面换出。
buf_state |= BM_VALID; /* TerminateBufferIO */
buf_state &= ~BM_VALID;
BM_IO_IN_PROGRESS : IO进行标记位。指明缓冲区管理器是否正在从存储中读/写相应页面。换句话说,该位指示是否有一个进程正持有此描述符上的 io_in_pregress_lock。(StartBufferIO)
refcount :保存当前访问相应页面的线程数(业务后端数量),也被称为钉数。当openGauss访问相应页面时,其引用计数必须自增1(refcount ++)。访问结束后其引用计数必须减1(refcount––)。当 refcount为零,即页面当前并未被访问时,页面将取钉,否则它会被钉住。
usage_count :保存着相应页面加载至相应缓冲池槽后的访问次数。usage_count会在页面置换算法中被用到。
为了简化后续的描述,这里定义三种描述符状态。
空 :当相应的缓冲池槽不存储页面时,即 refcount 与 usage_count 都是0,该描述符的状态为空。
钉住 :当相应缓冲池槽中存储着页面,且有 PostgreSQL 进程正在访问的相应页面时, 即 refcount和 usage_count 都大于等于1,该缓冲区描述符的状态为钉住。
未钉住 :当相应的缓冲池槽存储页面,但没有PostgreSQL进程正在访问相应页面时,即 usage_count 大于或等于1,但 refcount 为0,该缓冲区描述符的状态为未钉住。每个描述符都处于上述状态之一,描述符的状态会根据特定条件而改变。
2.3.2 缓冲区描述符初始状态
缓冲区描述符的集合构成了一个数组,称该数组为缓冲区描述符层。当openGauss服务器启动时,所有缓冲区描述符的状态都为空。
2.4 缓冲池
2.4.1 缓冲池结构
缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是 buffer_id。缓冲池槽的大小为 8KB,等于页面大小,因而每个槽都能存储整个页面。
数据类型:数组 key: buffer_id value:block
define BufferGetBlock(buffer) \
(BufferIsLocal(buffer) ? u_sess->storage_cxt.LocalBufferBlockPointers[-(buffer)-1] \
: IsNvmBufferID((buffer-1)) ? (Block)(t_thrd.storage_cxt.NvmBufferBlocks + ((Size)((uint)(buffer)-1-NvmBufferStartID)) * BLCKSZ) \
: IsSegmentBufferID(buffer-1) ? (Block)(t_thrd.storage_cxt.BufferBlocks + ((Size)((uint)(buffer)-NVM_BUFFER_NUM-1)) * BLCKSZ) \
: (Block)(t_thrd.storage_cxt.BufferBlocks + ((Size)((uint)(buffer)-1)) * BLCKSZ))
2.4.2 buffer候选队列
buffer候选队列 :即candidate_buffer,内核中与normal_list、candidate_free_map联合一起管理空闲buffer的使用。
candidate_buffer :数据类型:数组 key: 0~(N-1) value:buf_id
normal_list :数据类型:CandidateList类型链表 size: 自增++/自减-- head: pop(buf_id) tail: push(buf_id) value:buf_id
candidate_free_map :数据类型:数组 key: buf_id value:true/false
candidate_buffer候选队列和缓冲区、缓冲区描述的关系如下:
(1).初始化
BufferBlocks 、BufferDescriptors、candidate_buffers 、candidate_free_map 在函数InitBufferPool中初始化
normal_list 在函数init_candidate_list中初始化,将candidate_buffers分为pagewriter_thread_num份,将candidate_buffers均分后的每一份起始地址分别赋值给n个normal_list
(2).pagewriter 线程
在incre_ckpt_pgwr_scan_candidate_list函数中,每一个pagewriter线程遍历自己的normal_list,判断是否为脏(!(local_buf_state & BM_DIRTY)),非脏则将buf_id逐次添加至链表尾部,并将对应candidate_free_map[buf_id]置为true。数据库启动第一次执行到这里的时候,buf_id对应的candidate_free_map会全部是true。后续数据库正常运行状态时,pagewriter 线程会重复上述逻辑。
(3).业务线程读取页面,获取一个free buffer
在缓冲区中没有找到对应的页面后,会进入StrategyGetBuffer流程,先寻找一个空闲的buffer,如果没有空闲buffer,则会通过ClockSweepTick找到一个受害者缓冲池槽。
(3.1) 寻找一个空闲的buffer:在StrategyGetBuffer->get_buf_from_candidate_list函数中,获取一个随机的thread_id(最大不超过pagewriter_thread_num),然后找到第thread_id的pagewriter线程的normal_list,从头部pop出第一个buf_id,检查candidate_free_map是否是空闲,refcount是否为0,是否是脏页等,检验全部通过后返回buf_id,即从候选队列中找到一个空闲可用的buffer
(3.2) 寻找一个受害者缓冲池槽:ClockSweepTick 基于时钟扫描的页面置换算法
2.5 缓冲区相关的锁
Notes: 本节描述的锁,指的是缓冲区管理器同步机制的一部分。它们与SQL语句和SQL操作中的锁没有任何关系。
2.5.1 缓冲表锁
缓冲表锁 BufMappingLock 保护整个缓冲表的数据完整性。它是一种轻量级的锁,有共享模式与独占模式。在缓冲表中查询条目时,后端进程会持有共享的 BufMappingLock。插入或删除条目时,后端进程会持有独占的BufMappingLock。BufMappingLock 会被分为多个分区,以减少缓冲表中的争用(默认为128个分区)。每个BufMappingLock 分区都保护着一部分相应的散列桶槽。
下图给出了一个 BufMappingLock 分区的典型示例。两个后端进程可以同时持有各自分区的 BufMappingLock 独占锁,以插入新的数据项。如果 BufMappingLock 是系统级的锁,那么其中一个进程就需要等待另一个进程完成处理。
缓冲表也需要许多其他锁。例如,在缓冲表内部会使用自旋锁(spin lock)来删除数据项。不过本章不需要这些锁的其他相关知识,因此这里省略了对其他锁的介绍。
2.5.2 缓冲区描述符相关的锁
每个缓冲区描述符都会用到内容锁(content_lock)与IO进行锁(io_in_progress_lock)这两个轻量级锁,以控制对相应缓冲池槽页面的访问。当检查或更改描述符本身字段的值时,就会用到自旋锁。
内容锁(content_lock) : 是一个典型的强制限制访问的锁,它有共享与独占两种模式。
获取共享模式的content_lock的场景:当读取页面时,后端进程以共享模式获取页面相应缓冲区描述符中的 content_lock。执行下列操作之一时,则会获取独占模式的content_lock。
获取独占模式的content_lock的场景:(1)将行(即元组)插入页面,或更改页面中元组的 t_xmin/t_xmax 字段时 (2)物理移除元组,或压紧页面上的空闲空间(由清理过程和HOT执行) (3) 冻结页面中的元组。
content_lock, LW_EXCLUSIVE);
content_lock);
IO 进行锁(io_in_progress_lock) : 用于等待缓冲区上的I/O完成。当PostgreSQL进程加载/写入页面数据时,该进程在访问页面期间,持有对应描述符上独占的io_in_progres_lock。
io_in_progress_lock, LW_EXCLUSIVE);
自旋锁 : 当检查或更改标记字段与其他字段时,例如 refcount和 usage_count,会用到自旋锁。
钉住缓冲区描述符的场景:获取缓冲区描述符上的自旋锁,将其refcount和usage_count的值增加1,释放自旋锁。
将脏位设置为"1"的场景:获取缓冲区描述符上的自旋锁,使用位操作将脏位置位为"1",释放自旋锁。
Notes: 总结来说就是涉及到缓冲区描述符的state字段的检查和修改,需要获取自旋锁
/*
* Lock buffer header - set BM_LOCKED in buffer state.
*/
{
uint64 LockBufHdr(BufferDesc *desc)
ifndef ENABLE_THREAD_CHECK
SpinDelayStatus delayStatus = init_spin_delay(desc);
endif
uint64 old_buf_state;
while (true) {
/* set BM_LOCKED flag */
old_buf_state = pg_atomic_fetch_or_u64(&desc->state, BM_LOCKED);
/* if it wasn't set before we're OK */
if (!(old_buf_state & BM_LOCKED))
break;
ifndef ENABLE_THREAD_CHECK
perform_spin_delay(&delayStatus);
endif
}
ifndef ENABLE_THREAD_CHECK
finish_spin_delay(&delayStatus);
endif
/* ENABLE_THREAD_CHECK only, acquire semantic */
TsAnnotateHappensAfter(&desc->state);
return old_buf_state | BM_LOCKED;
}
自旋锁技术博客:https://zhuanlan.zhihu.com/p/659527782
3.缓冲区管理器的工作原理
本节介绍缓冲区管理器的工作原理。当后端进程想要访问所需页面时,它会调用ReadBufferExtended函数。
ReadBufferExtended —> buffer_read_extended_internal —> ReadBuffer_common —> BufferAlloc
3.1 访问存储在缓冲池中的页面
当从缓冲池槽中的页面里读取行时,会获取相应缓冲区描述符的共享content_lock,因而缓冲池槽可以同时被多个进程读取。当向页面插入(及更新、删除)行时,后端进程会获取相应缓冲区描述符的独占content_lock。访问完页面后,相应缓冲区描述符的引用计数值减1。
介绍最简单的情况,即所需页面已经存储在缓冲池中。在这种情况下,postgresql-16.2最新版本的缓冲区管理器会执行以下步骤:
(1)创建所需页面的 buffer_tag(在本例中 buffer_tag 是’Tag_C’),并使用散列函数计算与描述符相对应的散列桶槽。
(2)获取相应散列桶槽分区上的 BufMappingLock 共享锁。
(3)查找标签为 ‘Tag_C’ 的条目,并从条目中获取 buf_id。本例中 buf_id 为2。
(4)将 buffer_id=2 的缓冲区描述符钉住,即将描述符的 refcount 和 usage_count 增加1。
(5)释放BufMappingLock。
(6)访问buffer_id=2的缓冲池槽。
openGauss优化代码:半无锁dynamic hash逻辑 (bufferalloc半无锁hash :https://gitee.com/opengauss/openGauss-server/pulls/3935/files
(1)创建所需页面的 buffer_tag(在本例中 buffer_tag 是’Tag_C’),并使用散列函数计算与描述符相对应的散列桶槽。
(2)查找标签为 ‘Tag_C’ 的条目,并从条目中获取 buf_id。本例中 buf_id 为2。
(3)将 buffer_id=2 的缓冲区描述符钉住,即将描述符的 refcount 和 usage_count 增加1。
(4)判断buf->tag 与 new_tag(即‘Tag_C)是否相等,如果不相等说明buf_id对应的缓冲区描述符已经被替换,则unpinbuffer,回到步骤(1)
(5)访问buffer_id=2的缓冲池槽。
3.2 将页面从存储加载到空槽
假设所需页面不在缓冲池中,且 candidate_buffer(2.4.2节介绍) 中有空闲元素(空描述符)。这时,openGauss缓冲区管理器将执行以下步骤:
(1)创建所需页面的 buf_tag(本例中 buffer_tag 为 ‘Tag_E’ )并计算其散列桶槽。查找缓冲区表(假设页面不存在,在缓冲表中找不到对应页面)。(2)从 normal_list(2.4.2节介绍)中获取空闲缓冲区描述符,返回一个持有spin lock的buf_id(在本例中所获的描述符:buf_id=4)。
(3)释放spin lock,pin住buf_id对应的缓冲区描述符。
(4)由于是从normal_list获取到的buf_id,不会是脏页,脏页情况在3.3中讲解。
(5)以独占模式获取相应分区的 BufMappingLock。
(6)将buf_tag插入缓冲表数据项:可能存在someone else allocated another buffer for the same block we want to read in 的情况,因此这里有两种场景:
-
(6.1) 没有其他线程在我们之前读取同样block,则创建一条新的缓冲表数据项buf_tag=‘Tag_E’, buf_id=4,并将其插入缓冲区表中。
-
(6.2) buf_tag已存在于缓冲表中,则接下来参考3.1节的(3)~(5)步骤
(7)给缓冲区描述符赋值,((BufferDesc *)buf)->tag = new_tag,并给相应描述符的BM_TAG_VALID标记位置为"1"。
(8)释放相应分区的 BufMappingLock。
(9)此时buf_tag是有效的,但是buf_tag对应的缓冲池槽(即页面)还是无效的。
(10)将页面数据从存储加载至 buffer_id=4 的缓冲池槽中,如下所示:buf_id
-
(10.1)以排他模式获取相应描述符的 io_in_progress_lock。
-
(10.2)将相应描述符的 IO_IN_PROGRESS 标记位设置为1,以防其他进程访问。
-
(10.3)将所需的页面数据从存储加载到缓冲池插槽中。
-
(10.4)更改相应描述符的状态,将 IO_IN_PROGRESS 标记位设置为"0",且相应描述符的VALID标记位设置为"1"。
-
(10.5)释放 io_in_progress_lock。
(11)访问 buffer_id=4 的缓冲池槽。
4. 相关函数解析
函数PageCheckIfCanEliminate
BufferAlloc -> PageCheckIfCanEliminate: 当前选中的buf是否是BM_TAG_VALID,并且磁盘中的lsn_on_disk是否和内存buf的lsn一样,并且是否非脏页,是否处于回放状态中。如果满足则提示check lsn is not matched on disk,并且置脏。
此种情况出现的场景:淘汰一个buf,需要检查该buf是否为脏页,主机运行状态不会出现没有置脏的情况,代码中额外加了限制条件RecoveryInProgress,表明只有回放状态时,可能存在内存buf的lsn和磁盘页面的lsn不一致,并且内存buf没有置脏的情况,因此在这里做了一次置脏。
if ((*oldFlags & BM_TAG_VALID) && !XLByteEQ(buf->extra->lsn_on_disk, PageGetLSN(tmpBlock)) &&
!(*oldFlags & BM_DIRTY) && RecoveryInProgress()) {
int mode = DEBUG1;
ifdef USE_ASSERT_CHECKING
mode = PANIC;
endif
const uint32 shiftSize = 32;
ereport(mode, (errmodule(MOD_INCRE_BG),
errmsg("check lsn is not matched on disk:%X/%X on page %X/%X, relnode info:%u/%u/%u %u %u",
(uint32)(buf->extra->lsn_on_disk >> shiftSize), (uint32)(buf->extra->lsn_on_disk),
(uint32)(PageGetLSN(tmpBlock) >> shiftSize), (uint32)(PageGetLSN(tmpBlock)),
buf->tag.rnode.spcNode, buf->tag.rnode.dbNode, buf->tag.rnode.relNode,
buf->tag.blockNum, buf->tag.forkNum)));
SimpleMarkBufDirty(buf);
*oldFlags |= BM_DIRTY;
*needGetLock = true;
}
5.相关系统对象
5.1 参数
shared_buffers
5.2 函数或视图
(1)系统函数pg_buffercache_pages
select count(*) from pg_buffercache_pages();
观测参数:bufferid, relfilenode, bucketid, storage_type, reltablespace, reldatabase, relforknumber, relblocknumber, isdirty, isvalid, usage_count, pinning_backends
(2)系统函数local_candidate_stat
select count(*) from local_candidate_stat;
观测参数:node_name, candidate_slots, get_buf_from_list, get_buf_clock_sweep, seg_candidate_slots, seg_get_buf_from_list, seg_get_buf_clock_sweep
本文借鉴博客
https://blog.csdn.net/hmxz2nn/article/details/118526453
自旋锁技术博客:https://zhuanlan.zhihu.com/p/659527782
本文分享自微信公众号 - openGauss(openGauss)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“ OSC源创计划 ”,欢迎正在阅读的你也加入,一起分享。