《PostgreSQL技术内幕——原理探索》第八章 缓冲区管理器

0    185    1

Tags:

👉 本文共约15243个字,系统预计阅读时间或需58分钟。

缓冲区管理器(Buffer Manager)管理着共享内存和持久存储之间的数据传输,对于DBMS的性能有着重要的影响。PostgreSQL的缓冲区管理器十分高效。

本章介绍了PostgreSQL的缓冲区管理器。第一节概览了缓冲区管理器,后续的章节分别介绍以下内容:

  • 缓冲区管理器的结构
  • 缓冲区管理器的锁
  • 缓冲区管理器是如何工作的
  • 环形缓冲区
  • 脏页刷写

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

C76949F9-6362-4AA8-A5FB-9537C9A9B970

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)

8.1.3 后端进程如何读取数据页

本小节描述了后端进程如何从缓冲区管理器中读取页面,如图8.2所示。

图8.2 后端进程如何读取数据页

img

  1. 当读取表或索引页时,后端进程向缓冲区管理器发送请求,请求中带有目标页面的buffer_tag
  2. 缓冲区管理器会根据buffer_tag返回一个buffer_id,即目标页面存储在数组中的槽位的序号。如果请求的页面没有存储在缓冲池中,那么缓冲区管理器会将页面从持久存储中加载到其中一个缓冲池槽位中,然后再返回该槽位的buffer_id
  3. 后端进程访问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 缓冲区管理器的三层结构

img

  • 缓冲池(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 缓冲表

img

数据项包括两个值:页面的buffer_tag,以及包含页面元数据的描述符的buffer_id。例如数据项Tag_A,id=1 表示,buffer_id=1对应的缓冲区描述符中,存储着页面Tag_A的元数据。

散列函数

这里使用的散列函数是由calc_bucket()hash()组合而成。 下面是用伪函数表示的形式。

这里还没有对诸如查找、插入、删除数据项的基本操作进行解释。这些常见的操作将在后续小节详细描述。

8.2.2 缓冲区描述符

本节将描述缓冲区描述符的结构,下一小节将描述缓冲区描述符层。

缓冲区描述符保存着页面的元数据,这些与缓冲区描述符相对应的页面保存在缓冲池槽中。缓冲区描述符的结构由BufferDesc结构定义。这个结构有很多字段,主要字段如下所示:

  • tag 保存着目标页面的buffer_tag,该页面存储在相应的缓冲池槽中(缓冲区标签的定义在8.1.2节给出)。
  • buffer_id 标识了缓冲区描述符(亦相当于对应缓冲池槽的buffer_id)。
  • refcount 保存当前访问相应页面的PostgreSQL进程数,也被称为钉数(pin count)。当PostgreSQL进程访问相应页面时,其引用计数必须自增1(refcount ++)。访问结束后其引用计数必须减1(refcount--)。 当refcount为零,即页面当前并未被访问时,页面将取钉(unpinned) ,否则它会被钉住(pinned)
  • usage_count 保存着相应页面加载至相应缓冲池槽后的访问次数。usage_count会在页面置换算法中被用到(第8.4.4节)。
  • context_lockio_in_progress_lock是轻量级锁,用于控制对相关页面的访问。第8.3.2节将介绍这些字段。
  • flags 用于保存相应页面的状态,主要状态如下:
    • 脏位(dirty bit 指明相应页面是否为脏页。
    • 有效位(valid bit 指明相应页面是否可以被读写(有效)。例如,如果该位被设置为"valid",那就意味着对应的缓冲池槽中存储着一个页面,而该描述符中保存着该页面的元数据,因而可以对该页面进行读写。反之如果有效位被设置为"invalid",那就意味着该描述符中并没有保存任何元数据;即,对应的页面无法读写,缓冲区管理器可能正在将该页面换出。
    • IO进行标记位(io_in_progress) 指明缓冲区管理器是否正在从存储中读/写相应页面。换句话说,该位指示是否有一个进程正持有此描述符上的io_in_pregress_lock
  • freeNext 是一个指针,指向下一个描述符,并以此构成一个空闲列表(freelist),细节在下一小节中介绍。

结构BufferDesc定义于src/include/storage/buf_internals.h中。

为了简化后续章节的描述,这里定义三种描述符状态:

  • 空(Empty:当相应的缓冲池槽不存储页面(即refcountusage_count都是0),该描述符的状态为
  • 钉住(Pinned:当相应缓冲池槽中存储着页面,且有PostgreSQL进程正在访问的相应页面(即refcountusage_count都大于等于1),该缓冲区描述符的状态为钉住
  • 未钉住(Unpinned:当相应的缓冲池槽存储页面,但没有PostgreSQL进程正在访问相应页面时(即 usage_count大于或等于1,但refcount为0),则此缓冲区描述符的状态为未钉住

每个描述符都处于上述状态之一。描述符的状态会根据特定条件而改变,这将在下一小节中描述。

在下图中,缓冲区描述符的状态用彩色方框表示。

  • ${□}$(白色)

  • $\color{blue}{█}$(蓝色)钉住

  • $\color{cyan}{█}$(青色)未钉住

此外,脏页面会带有“X”的标记。例如一个未固定的脏描述符用 $\color{cyan}☒$ 表示。

8.2.3 缓冲区描述符层

缓冲区描述符的集合构成了一个数组。本书称该数组为缓冲区描述符层(buffer descriptors layer)

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

图8.5 缓冲区管理器初始状态

img

请注意PostgreSQL中的freelist完全不同于Oracle中freelists的概念。PostgreSQL的freelist只是空缓冲区描述符的链表。PostgreSQL中与Oracle中的freelist相对应的对象是空闲空间映射(FSM)(第5.3.4节)。

图8.6展示了第一个页面是如何加载的。

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

第二页,以及后续页面都以类似方式加载,其他细节将在第8.4.2节中介绍。

图8.6 加载第一页

img

freelist中摘出的描述符始终保存着页面的元数据。换而言之,仍然在使用的非空描述符不会返还到freelist中。但当下列任一情况出现时,描述符状态将变为“空”,并被重新插入至freelist中:

  1. 相关表或索引已被删除。
  2. 相关数据库已被删除。
  3. 相关表或索引已经被VACUUM FULL命令清理了。

为什么使用freelist来维护空描述符?

保留freelist的原因是为了能立即获取到一个描述符。这是内存动态分配的常规做法,详情参阅这里的说明

缓冲区描述符层包含着一个32位无符号整型变量nextVictimBuffer。此变量用于8.4.4节将介绍的页面置换算法。

8.2.4 缓冲池

缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是buffer_id

缓冲池槽的大小为8KB,等于页面大小,因而每个槽都能存储整个页面。

8.3 缓冲区管理器锁

缓冲区管理器会出于不同的目的使用各式各样的锁,本节将介绍理解后续部分所必须的一些锁。

注意本节中描述的锁,指的是是缓冲区管理器同步机制的一部分。它们与SQL语句和SQL操作中的锁没有任何关系。

8.3.1 缓冲表锁

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

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

图8.7给出了一个BufMappingLock分区的典型示例。两个后端进程可以同时持有各自分区的BufMappingLock独占锁以插入新的数据项。如果BufMappingLock是系统级的锁,那么其中一个进程就需要等待另一个进程完成处理。

图8.7 两个进程同时获取相应分区的BufMappingLock独占锁,以插入新数据项

img

本人提供Oracle(OCP、OCM)、MySQL(OCP)、PostgreSQL(PGCA、PGCE、PGCM)等数据库的培训和考证业务,私聊QQ646634621或微信dbaup66,谢谢!
AiDBA后续精彩内容已被站长无情隐藏,请输入验证码解锁本文!
验证码:
获取验证码: 请先关注本站微信公众号,然后回复“验证码”,获取验证码。在微信里搜索“AiDBA”或者“dbaup6”或者微信扫描右侧二维码都可以关注本站微信公众号。

标签:

Avatar photo

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复