当前位置: 首页 > article >正文

PostgreSQL数据库缓冲区管理模块

1. 结构

图1. 缓冲区管理器的三层结构

表1. 缓冲区管理器的结构及其功能

结构

功能

缓冲表

(buffer table)

是一个哈希表,存储着页面的buffer_tag与描述符的buffer_id之间的映射关系。

缓冲区描述符

(buffer descriptors)

每个描述符与缓冲池槽一一对应,并保存着相应槽的元数据。

缓冲池

(buffer pool)

存储着数据文件页面,及其相应的自由空间映射和可见性映射的页面。缓冲池是一个数组,数据的每个槽中存储数据文件的一页。缓冲池数组的序号索引称为buffer_id。

2. 缓冲区管理器,存储和后端进程之间的关系

图3. 后端进程读取数据页

后端进程读取数据页的整体流程:

  1. 当读取表或索引页时,后端进程向缓冲区管理器发送请求,请求中带有目标页面的buffer_tag。
  2. 缓冲区管理器会根据buffer_tag返回一个buffer_id,即目标页面存储在数组中的槽位的序号。如果请求的页面没有存储在缓冲池中,那么缓冲区管理器会将页面从持久存储中加载到其中一个缓冲池槽位中,然后再返回该槽位的buffer_id。
  3. 后端进程访问buffer_id对应的槽位(以读取所需的页面)。

1.1 缓冲表

图4.缓冲表

表2. 表的逻辑组成及功能

组成

功能

散列函数

散列函数将buffer_tag映射到哈希桶槽

散列桶槽

缓冲表采用了使用链表的分离链接方法(separate chaining with linked lists)来解决冲突。当数据项被映射到至同一个桶槽时,该方法会将这些数据项保存在一个链表中。

数据项

包括页面的buffer_tag,以及包含页面元数据的描述符的buffer_id。

  • buffer_tag

每个数据文件页面都可以分配到唯一的标签,即缓冲区标签(buffer tag)。

组成:关系文件节点(relfilenode),关系分支编号(fork number),页面块号(block number)。例如,缓冲区标签{(16821, 16384, 37721), 0, 7}表示,在oid=16821的表空间中的oid=16384的数据库中的oid=37721的表的0号分支(关系本体)的第七号页面。(关系本体main分支编号为0,空闲空间映射fsm分支编号为1,可见性映射vm分支编号为2)。

1.2 缓冲区描述符

保存着页面的元数据,这些与缓冲区描述符相对应的页面保存在缓冲池槽中。缓冲区描述符的结构由BufferDesc结构定义。

表3. BufferDesc结构定义

结构

定义

tag

保存着目标页面的buffer_tag,该页面存储在相应的缓冲池槽中。

buffer_id

标识了缓冲区描述符(亦相当于对应缓冲池槽的buffer_id)。

refcount

保存当前访问相应页面的PostgreSQL进程数,也被称为钉数(pin count)。当PostgreSQL进程访问相应页面时,其引用计数必须自增1(refcount ++)。访问结束后其引用计数必须减1(refcount--)。当refcount为零,即页面当前并未被访问时,页面将取钉(unpinned),否则它会被钉住(pinned)。

usage_count

保存着相应页面加载至相应缓冲池槽后的访问次数。

context_lock

io_in_progress_lock

轻量级锁,用于控制对相关页面的访问。

flags

用于保存相应页面的状态:

  • 脏位(dirty bit):指明相应页面是否为脏页。
  • 有效位(valid bit):指明相应页面是否可以被读写(有效)。例如,如果该位被设置为"valid",那就意味着对应的缓冲池槽中存储着一个页面,而该描述符中保存着该页面的元数据,因而可以对该页面进行读写。反之如果有效位被设置为"invalid",那就意味着该描述符中并没有保存任何元数据;即,对应的页面无法读写,缓冲区管理器可能正在将该页面换出。
  • IO进行标记位(**io_in_progress**):指明缓冲区管理器是否正在从存储中读/写相应页面。换句话说,该位指示是否有一个进程正持有此描述符上的io_in_pregress_lock。

freeNext

一个指针,指向下一个描述符,并以此构成一个空闲列表(freelist)

当PostgreSQL服务器启动时,所有缓冲区描述符的状态都为空。在PostgreSQL中,这些描述符构成了一个名为freelist的链表,如图5所示。

图5.缓冲区管理器初始状态

6. 加载第一页

加载第一页的具体流程:

  1. 从freelist的头部取一个空描述符,并将其钉住(即,将其refcount和usage_count增加1)。
  2. 在缓冲表中插入新项,该缓冲表项保存了页面buffer_tag与所获描述符buffer_id之间的关系。
  3. 将新页面从存储器加载至相应的缓冲池槽中。
  4. 将新页面的元数据保存至所获取的描述符中。

1.3 缓冲池

缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是buffer_id。缓冲池槽的大小为8KB,等于页面大小,因而每个槽都能存储整个页面。

2 缓冲区管理

2.1 缓冲表锁

BufMappingLock:保护整个缓冲表的数据完整性。它是一种轻量级的锁,有共享模式与独占模式。在缓冲表中查询条目时,后端进程会持有共享的BufMappingLock。插入或删除条目时,后端进程会持有独占的BufMappingLock。

BufMappingLock会被分为多个分区,以减少缓冲表中的争用(默认为128个分区)。每个BufMappingLock分区都保护着一部分相应的散列桶槽。

缓冲表也需要许多其他锁。例如,在缓冲表内部会使用自旋锁(spin lock)来删除数据项。

2.2 缓冲区描述符相关的锁

每个缓冲区描述符都会用到两个轻量级锁——content_lock与io_in_progress_lock,来控制对相应缓冲池槽页面的访问。当检查或更改描述符本身字段的值时,则会用到自旋锁。

表4.缓冲区描述符相关的锁及功能

功能

内容锁

(content_lock)

共享(shared)

  • 当读取页面时,后端进程以共享模式获取页面相应缓冲区描述符中的content_lock。

独占(exclusive)

  • 将行(即元组)插入页面,或更改页面中元组的t_xmin/t_xmax字段时
  • 物理移除元组,或压紧页面上的空闲空间
  • 冻结页面中的元组

io_in_progress_lock

用于等待缓冲区上的I/O完成。当PostgreSQL进程加载/写入页面数据时,该进程在访问页面期间,持有对应描述符上独占的io_in_progres_lock。

自旋锁

(spinlock)

当检查或更改标记字段与其他字段时(例如refcount和usage_count),会用到自旋锁。

3. 缓冲区管理器的工作原理

当后端进程想要访问所需页面时,它会调用ReadBufferExtended函数。

图7. 将页面从存储加载至受害者缓冲池槽中

假设所有缓冲池槽位都被页面占用,且未存储所需的页面。缓冲区管理器将执行以下步骤:

(1)创建所需页面的buffer_tag并查找缓冲表。在本例中假设buffer_tag ’Tag_M’(且相应的页面在缓冲区中找不到)。

(2)使用时钟扫描算法选择一个受害者缓冲池槽位,从缓冲表中获取包含着受害者槽位buffer_id的旧表项,并在缓冲区描述符层将受害者槽位的缓冲区描述符钉住。本例中受害者槽的buffer_id=5,旧表项为Tag_F,id = 5。

(3)如果受害者页面是脏页,将其刷盘(write & fsync),否则进入步骤(4)。在使用新数据覆盖脏页之前,必须将脏页写入存储中。脏页的刷盘步骤如下:

  1. 获取buffer_id=5描述符上的共享content_lock和独占io_in_progress_lock(在步骤6中释放)。
  2. 更改相应描述符的状态:相应IO_IN_PROCESS位被设置为"1",JUST_DIRTIED位设置为"0"。
  3. 根据具体情况,调用XLogFlush()函数将WAL缓冲区上的WAL数据写入当前WAL段文件。
  4. 将受害者页面的数据刷盘至存储中。
  5. 更改相应描述符的状态;将IO_IN_PROCESS位设置为"0",将VALID位设置为"1"。
  6. 释放io_in_progress_lock和content_lock。

(4)以排他模式获取缓冲区表中旧表项所在分区上的BufMappingLock。

(5)获取新表项所在分区上的BufMappingLock,并将新表项插入缓冲表:

  1. 创建由新表项:由buffer_tag='Tag_M'与受害者的buffer_id组成的新表项。
  2. 以独占模式获取新表项所在分区上的BufMappingLock。
  3. 将新表项插入缓冲区表中。

(6)从缓冲表中删除旧表项,并释放旧表项所在分区的BufMappingLock。

将目标页面数据从存储加载至受害者槽位。然后用buffer_id=5更新描述符的标识字段;将脏位设置为0,并按流程初始化其他标记位。

(8)释放新表项所在分区上的BufMappingLock。

(9)访问buffer_id=5对应的缓冲区槽位。

4. 页面替换算法:时钟扫描

将缓冲区描述符想象为一个循环列表。而nextVictimBuffer是一个32位的无符号整型变量,它总是指向某个缓冲区描述符并按顺时针顺序旋转。

具体原理:

(1)获取nextVictimBuffer指向的候选缓冲区描述符(candidate buffer descriptor)。

(2)如果候选描述符未被钉住(unpinned),则进入步骤(3),否则进入步骤(4)。

(3)如果候选描述符的usage_count为0,则选择该描述符对应的槽作为受害者,并进入步骤(5);否则将此描述符的usage_count减1,并继续执行步骤(4)。

(4) 将nextVictimBuffer迭代至下一个描述符(如果到末尾则回绕至头部)并返回步骤(1)。重复至找到受害者。

(5)返回受害者的buffer_id。

当nextVictimBuffer扫过未固定的描述符时,其usage_count会减1。因此只要缓冲池中存在未固定的描述符,该算法总能在旋转若干次nextVictimBuffer后,找到一个usage_count为0的受害者。

  • 环形缓冲区(ring buffer)

在读写大表时,PostgreSQL会使用环形缓冲区(ring buffer)而不是缓冲池。环形缓冲器是一个很小的临时缓冲区域。分配的环形缓冲区将在使用后被立即释放。

环形缓冲区的好处显而易见,如果后端进程在不使用环形缓冲区的情况下读取大表,则所有存储在缓冲池中的页面都会被移除(踢出),因而会导致缓存命中率降低。环形缓冲区可以避免此问题。

  • 脏页刷盘

除了置换受害者页面之外,检查点进程(Checkpointer)进程和后台写入器进程也会将脏页刷写至存储中。尽管两个进程都具有相同的功能(刷写脏页),但它们有着不同的角色和行为。

检查点进程将检查点记录(checkpoint record)写入WAL段文件,并在检查点开始时进行脏页刷写。

后台写入器的目的是通过少量多次的脏页刷盘,减少检查点带来的密集写入的影响。后台写入器会一点点地将脏页落盘,尽可能减小对数据库活动造成的影响。默认情况下,后台写入器每200毫秒被唤醒一次(由参数bgwriter_delay定义),且最多刷写bgwriter_lru_maxpages个页面(默认为100个页面)。


http://www.kler.cn/a/460566.html

相关文章:

  • MySQL UNION
  • 算法-判断4的次幂
  • Spring Boot 中的 classpath详解
  • 第二十六天 自然语言处理(NLP)词嵌入(Word2Vec、GloVe)
  • C语言----指针数组
  • DataV数据可视化
  • Binlog 深度解析:数据灾难下的绝地反击
  • 洛谷 P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布
  • [实用指南]如何将视频从iPhone传输到iPad
  • XGPT用户帮助手册
  • SQLiteDataBase数据库
  • Python 青铜宝剑十六维,破医疗数智化难关(下)
  • docker compose部署kafka集群
  • Linux -- 死锁、自旋锁
  • Oracle库锁表处理
  • 在Ubuntu下通过Docker部署MySQL服务器
  • 论文分享 | PromptFuzz:用于模糊测试驱动程序生成的提示模糊测试
  • 【Docker】:Docker容器技术
  • SAP B1 认证考试习题 - 解析版(六)
  • ChatGPT-4助力学术论文提升文章逻辑、优化句式与扩充内容等应用技巧解析。附提示词案例
  • 百度贴吧的ip属地什么意思?怎么看ip属地
  • 2024年前端工程师总结
  • 提示词工程教程(零):提示词工程教程简介
  • 【基于语义地图的机器人路径覆盖】Radiant Field-Informed Coverage Planning (RFICP)高斯扩散场轨迹规划算法详解
  • 详细了解Redis分布式存储的常见方案
  • 在虚幻引擎4(UE4)中使用蓝图的详细教程