合 《PostgreSQL技术内幕——原理探索》第八章 缓冲区管理器
Tags: PGPostgreSQL翻译《PostgreSQL技术内幕——原理探索》
- 8.1 概览
- 8.1.1 缓冲区管理器的结构
- 8.1.2 缓冲区标签(buffer_tag)
- 8.1.3 后端进程如何读取数据页
- 8.1.4 页面置换算法
- 8.1.5 刷写脏页
- 直接I/O(Direct I/O)
- 8.2 缓冲区管理器的结构
- 8.2.1 缓冲表
- 散列函数
- 8.2.2 缓冲区描述符
- 8.2.3 缓冲区描述符层
- 为什么使用freelist来维护空描述符?
- 8.2.4 缓冲池
- 8.3 缓冲区管理器锁
- 8.3.1 缓冲表锁
- 8.3.2 缓冲区描述符相关的锁
- 8.3.2.1 内容锁(content_lock)
- 8.3.2.2 IO进行锁(io_in_progress_lock)
- 8.3.2.3 自旋锁(spinlock)
- 用原子操作替换缓冲区管理器的自旋锁
- 8.4 缓冲区管理器的工作原理
- 8.4.1 访问存储在缓冲池中的页面
- 8.4.2 将页面从存储加载至空槽
- 8.4.3 将页面从存储加载至受害者缓冲池槽中
- 8.4.4 页面替换算法:时钟扫描
- 伪代码:时钟扫描
- 8.5 环形缓冲区
- 为什么批量读取和清理过程的默认环形缓冲区大小为256 KB?
- 8.6 脏页刷盘
- 为什么检查点进程与后台写入器相分离?
缓冲区管理器(Buffer Manager)管理着共享内存和持久存储之间的数据传输,对于DBMS的性能有着重要的影响。PostgreSQL的缓冲区管理器十分高效。
本章介绍了PostgreSQL的缓冲区管理器。第一节概览了缓冲区管理器,后续的章节分别介绍以下内容:
- 缓冲区管理器的结构
- 缓冲区管理器的锁
- 缓冲区管理器是如何工作的
- 环形缓冲区
- 脏页刷写
图8.1 缓冲区管理器,存储和后端进程之间的关系
8.1 概览
本节介绍了一些关键概念,有助于理解后续章节。
8.1.1 缓冲区管理器的结构
PostgreSQL缓冲区管理器由缓冲表,缓冲区描述符和缓冲池组成,这几个组件将在接下来的小节中介绍。 缓冲池(buffer pool)层存储着数据文件页面,诸如表页与索引页,及其相应的自由空间映射和可见性映射的页面。 缓冲池是一个数组,数据的每个槽中存储数据文件的一页。 缓冲池数组的序号索引称为buffer_id
。8.2和8.3节描述了缓冲区管理器的内部细节。
8.1.2 缓冲区标签(buffer_tag
)
PostgreSQL中的每个数据文件页面都可以分配到唯一的标签,即缓冲区标签(buffer tag)。 当缓冲区管理器收到请求时,PostgreSQL会用到目标页面的缓冲区标签。
缓冲区标签(buffer_tag) 由三个值组成:关系文件节点(relfilenode),关系分支编号(fork number),页面块号(block number)。例如,缓冲区标签{(16821, 16384, 37721), 0, 7}
表示,在oid=16821
的表空间中的oid=16384
的数据库中的oid=37721
的表的0号分支(关系本体)的第七号页面。再比如缓冲区标签{(16821, 16384, 37721), 1, 3}
表示该表空闲空间映射文件的三号页面。(关系本体main
分支编号为0,空闲空间映射fsm
分支编号为1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* * Buffer tag 标识了缓冲区中包含着哪一个磁盘块。 * 注意:BufferTag中的数据必需足以在不参考pg_class或pg_tablespace中的数据项 * 的前提下,能够直接确定该块需要写入的位置。不过有可能出现这种情况:刷写缓冲区的 * 后端进程甚至都不认为自己能在那个时刻看见相应的关系(譬如,后段进程对应的的事务 * 开始时间早于创建该关系的事务)。无论如何,存储管理器都必须能应对这种情况。 * * 注意:如果结构中存在任何填充字节,INIT_BUFFERTAG需要将所有字段抹为零,因为整个 * 结构体被当成一个散列键来用。 */ typedef struct buftag { RelFileNode rnode; /* 关系的物理标识符 */ ForkNumber forkNum; /* 关系的分支编号 */ BlockNumber blockNum; /* 相对于关系开始位置的块号 */ } BufferTag; typedef struct RelFileNode { Oid spcNode; /* 表空间 */ Oid dbNode; /* 数据库 */ Oid relNode; /* 关系 */ } RelFileNode; |
8.1.3 后端进程如何读取数据页
本小节描述了后端进程如何从缓冲区管理器中读取页面,如图8.2所示。
图8.2 后端进程如何读取数据页
- 当读取表或索引页时,后端进程向缓冲区管理器发送请求,请求中带有目标页面的
buffer_tag
。 - 缓冲区管理器会根据
buffer_tag
返回一个buffer_id
,即目标页面存储在数组中的槽位的序号。如果请求的页面没有存储在缓冲池中,那么缓冲区管理器会将页面从持久存储中加载到其中一个缓冲池槽位中,然后再返回该槽位的buffer_id
。 - 后端进程访问
buffer_id
对应的槽位(以读取所需的页面)。
当后端进程修改缓冲池中的页面时(例如向页面插入元组),这种尚未刷新到持久存储,但已被修改的页面被称为脏页(dirty page)。
第8.4节描述了缓冲区管理器的工作原理。
8.1.4 页面置换算法
当所有缓冲池槽位都被占用,且其中未包含所请求的页面时,缓冲区管理器必须在缓冲池中选择一个页面逐出,用于放置被请求的页面。 在计算机科学领域中,选择页面的算法通常被称为页面置换算法(page replacement algorithms),而所选择的页面被称为受害者页面(victim page)。
针对页面置换算法的研究从计算机科学出现以来就一直在进行,因此先前已经提出过很多置换算法了。 从8.1版本开始,PostgreSQL使用时钟扫描(clock-sweep)算法,因为比起以前版本中使用的LRU算法,它更为简单高效。
第8.4.4节描述了时钟扫描的细节。
8.1.5 刷写脏页
脏页最终应该被刷入存储,但缓冲区管理器执行这个任务需要额外帮助。 在PostgreSQL中,两个后台进程:检查点进程(checkpointer)和后台写入器(background writer)负责此任务。
8.6节描述了检查点进程和后台写入器。
直接I/O(Direct I/O)
PostgreSQL并不支持直接I/O,但有时会讨论它。 如果你想了解更多详细信息,可以参考这篇文章,以及pgsql-ML中的这个讨论。
8.2 缓冲区管理器的结构
PostgreSQL缓冲区管理器由三层组成,即缓冲表层,缓冲区描述符层和缓冲池层(图8.3):
图8.3 缓冲区管理器的三层结构
- 缓冲池(buffer pool)层是一个数组。 每个槽都存储一个数据文件页,数组槽的索引称为
buffer_id
。 - 缓冲区描述符(buffer descriptors)层是一个由缓冲区描述符组成的数组。 每个描述符与缓冲池槽一一对应,并保存着相应槽的元数据。请注意,术语“缓冲区描述符层”只是在本章中为方便起见使用的术语。
- 缓冲表(buffer table)层是一个哈希表,它存储着页面的
buffer_tag
与描述符的buffer_id
之间的映射关系。
这些层将在以下的节中详细描述。
8.2.1 缓冲表
缓冲表可以在逻辑上分为三个部分:散列函数,散列桶槽,以及数据项(图8.4)。
内置散列函数将buffer_tag
映射到哈希桶槽。 即使散列桶槽的数量比缓冲池槽的数量要多,冲突仍然可能会发生。因此缓冲表采用了使用链表的分离链接方法(separate chaining with linked lists)来解决冲突。 当数据项被映射到至同一个桶槽时,该方法会将这些数据项保存在一个链表中,如图8.4所示。
图8.4 缓冲表
数据项包括两个值:页面的buffer_tag
,以及包含页面元数据的描述符的buffer_id
。例如数据项Tag_A,id=1
表示,buffer_id=1
对应的缓冲区描述符中,存储着页面Tag_A
的元数据。
散列函数
这里使用的散列函数是由
calc_bucket()
与hash()
组合而成。 下面是用伪函数表示的形式。
12 uint32 bucket_slot =calc_bucket(unsigned hash(BufferTag buffer_tag), uint32 bucket_size)
这里还没有对诸如查找、插入、删除数据项的基本操作进行解释。这些常见的操作将在后续小节详细描述。