解读年度数据库PostgreSQL:如何巧妙地实现缓冲区管理器
墨墨导读:PostgreSQL 已获得 DB-Engines 排行榜 2017 年和2018年的“年度数据库”称号,发展如此迅猛,它究竟有什么内幕呢?接下来,我们将选择PostgreSQL重要的子系统之一缓冲区管理器展开介绍,探讨它的工作原理。
此外,我们也成立PostgreSQL学习社群,技术探讨、资料分享、大牛解答,欢迎加入一起进步,入群方式见文末。
之前,我们分享了解读年度数据库PostgreSQL:基础备份与时间点恢复(上),解读年度数据库PostgreSQL:基础备份与时间恢复(下)
缓冲区管理器结构
PostgreSQL 缓冲区管理器非常高效,它管理着共享内存和持久存储之间的数据传输,对于数据库管理系统的性能有着重要的影响。它由缓冲表、缓冲区描述符和缓冲池组成。缓冲表层是一个散列表,它存储着页面buffer_tag与描述符buffer_id之间的映射关系。缓冲区描述符层是一个由缓冲区描述符组成的数组。每个描述符与缓冲池槽一一对应,并保存着相应槽的元数据。请注意,术语“缓冲区描述符层”只是在本章中为方便起见而使用的术语。缓冲池层是一个数组。每个槽都存储一个数据文件页,数组槽的索引称为buffer_id。缓冲区管理器的三层结构如图1所示。
图1 缓冲区管理器的三层结构
缓冲区管理器的工作原理
当后端进程想要访问所需页面时,它会调用ReadBufferExtended函数。
函数ReadBufferExtended的行为因场景而异,在逻辑上具体可以分为三种情况。
访问存储在缓冲池中的页面
当从缓冲池槽中的页面里读取行时,PostgreSQL进程获取相应缓冲区描述符的共享content_lock,因而缓冲池槽可以同时被多个进程读取。
当向页面插入(及更新、删除)行时,该postgres后端进程获取相应缓冲区描述符的独占content_lock(注意,这里必须将相应页面的脏位置设为"1")。
访问完页面后,相应缓冲区描述符的引用计数值减1。
图2是访问存储在缓冲池中的页面示意图。
图2 访问存储在缓冲池中的页面
我们来介绍最简单的情况,即所需页面已经存储在缓冲池中。在这种情况下,缓冲区管理器会执行以下步骤:
创建所需页面的buffer_tag(在本例中buffer_tag是'Tag_C'),并使用散列函数计算与描述符相对应的散列桶槽。
获取相应散列桶槽分区上的BufMappingLock共享锁。
查找标签为'Tag_C'的条目,并从条目中获取buffer_id。本例中buffer_id为2。
将buffer_id=2的缓冲区描述符钉住,即将描述符的refcount和usage_count增加1。
释放BufMappingLock。
访问buffer_id=2的缓冲池槽。
将页面从存储加载到空槽
图3是将页面从存储加载到空槽的示意图。
图3 将页面从存储加载到空槽
在第二种情况下,假设所需页面不在缓冲池中,且freelist中有空闲元素(空描述符)。这时,缓冲区管理器将执行以下步骤:
(查找缓冲区表(本节假设页面不存在,找不到对应页面)。
第一,创建所需页面的buffer_tag(本例中buffer_tag为'Tag_E')并计算其散列桶槽。
第二,以共享模式获取相应分区上的BufMappingLock。
第三,查找缓冲区表(根据假设,这里没找到)。
第四,释放BufMappingLock。
从freelist中获取空缓冲区描述符,并将其钉住。在本例中所获的描述符:buffer_id=4。
以独占模式获取相应分区的BufMappingLock(此锁将在步骤(6)中被释放)。
创建一条新的缓冲表数据项:buffer_tag='Tag_E’, buffer_id=4,并将其插入缓冲区表中。
将页面数据从存储加载至buffer_id=4的缓冲池槽中,如下所示:
第一,以排他模式获取相应描述符的io_in_progress_lock。
第二,将相应描述符的IO_IN_PROGRESS标记位设置为1,以防其他进程访问。
第三,将所需的页面数据从存储加载到缓冲池插槽中。
第四,更改相应描述符的状态,将IO_IN_PROGRESS标记位设置为"0",且VALID标记位设置为"1"。
第五,释放io_in_progress_lock。
释放相应分区的BufMappingLock。
访问buffer_id=4的缓冲池槽。
将页面从存储加载到受害者缓冲池槽
在这种情况下,假设所有缓冲池槽位都被页面占用,且未存储所需的页面。图4是将页面从存储加载到受害者缓冲池槽的示意图。
图4 将页面从存储加载到受害者缓冲池槽
缓冲区管理器将执行以下步骤:
创建所需页面的buffer_tag并查找缓冲表。在本例中假设buffer_tag是'Tag_M'(且相应的页面在缓冲区中找不到)。
使用时钟扫描算法选择一个受害者缓冲池槽位,从缓冲表中获取包含着受害者槽位buffer_id的旧表项,并在缓冲区描述符层将受害者槽位的缓冲区描述符钉住。本例中受害者槽的buffer_id=5,旧表项为Tag_F,id = 5。时钟扫描将在下一节介绍。
如果受害者页面是脏页,则将其刷盘(write & fsync),否则进入步骤4。
在使用新数据覆盖脏页之前,必须将脏页写入存储中。脏页的刷盘步骤如下:
第一,获取buffer_id=5描述符上的共享content_lock和独占io_in_progress_lock。
第二,更改相应描述符的状态:相应IO_IN_PROCESS设置为1,JUST_DIRTIED位设置为0。
第三,根据具体情况,调用XLogFlush()函数将WAL缓冲区上的WAL数据写入当前WAL段文件。
第四,将受害者页面的数据刷盘至存储中。
第五,更改相应描述符的状态;将IO_IN_PROCESS位设置为"0",将VALID位设置为"1"。
第六,释放io_in_progress_lock和content_lock。
以排他模式获取缓冲区表中旧表项所在分区上的BufMappingLock。
获取新表项所在分区上的BufMappingLock,并将新表项插入缓冲表:
第一,首先需要创建一个全新的表项:由buffer_tag='Tag_M'与受害者的buffer_id组成的新表项。
第二,以独占模式获取新表项所在分区上的BufMappingLock。
第三,将新表项插入缓冲区表中。
从缓冲表中删除旧表项,并释放旧表项所在分区的BufMappingLock。
将目标页面数据从存储加载至受害者槽位,然后用buffer_id=5更新描述符的标识字段,将脏位设置为0,并按流程初始化其他标记位。
释放新表项所在分区上的BufMappingLock。
访问buffer_id=5对应的缓冲区槽位。
内容简介:本书介绍PostgreSQL内部的工作原理,包括数据库对象的逻辑组织与物理实现,进程与内存的架构,并依次剖析几个重要子系统——查询处理、外部数据包装器、并发控制、清理过程、缓冲区管理、WAL、备份及流复制。本书为 DBA 与系统开发者提供一幅全景概念地图,有助于读者形成对数据库实现的整体认识,亦可作为PostgreSQL源代码深入学习的导读手册,对于理解数据库原理与PostgreSQL内部实现大有裨益。本书适合数据库开发人员及相关领域的研究人员、数据库DBA及高等院校相关专业的学生阅读。
扩展阅读
公司简介 | 招聘 | DTCC | 数据技术嘉年华 | 免费课程 | 入驻华为严选商城
zCloud | SQM | Bethune Pro2 | zData一体机 | MyData一体机 | ZDBM 备份一体机
Oracle技术架构 | 免费课程 | 数据库排行榜 | DBASK问题集萃 | 技术通讯
升级迁移 | 性能优化 | 智能整合 | 安全保障 | 架构设计 | SQL审核 | 分布式架构 | 高可用容灾 | 运维代维
云和恩墨大讲堂 | 一个分享交流的地方
长按,识别二维码,加入万人交流社群
请备注:云和恩墨大讲堂