内容简介:铃木启修 | 作者期待您的加入,一起成长
铃木启修 | 作者
PostgreSQL 已获得 DB-Engines 排行榜 2017 年和2018年的“年度数据库”称号,发展如此迅猛,它究竟有什么内幕呢?接下来,我们将选择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指南:内幕探索》。 基于发挥最大效能的先天动力, 数据库学习者对其内部运行或者实现机制有着本能的兴趣。好在, PostgreSQL 也并未设计成黑盒子。深入了解其机制后,开发人员可以进行高效的应用设计,写出高性能的 SQL 语句;运维人员可以针对性地进行性能优化,快速对问题进行分析、定位和解决。如果,举世能找到的唯一秘辛, 现在 就静静躲在 阅读原文 后面,你会不点开吗?
内容简介 : 本书介绍PostgreSQL内部的工作原理,包括数据库对象的逻辑组织与物理实现,进程与内存的架构,并依次剖析几个重要子系统——查询处理、外部数据包装器、并发控制、清理过程、缓冲区管理、WAL、备份及流复制。本书为 DBA 与系统开发者提供一幅全景概念地图,有助于读者形成对数据库实现的整体认识,亦可作为 PostgreSQL源代码 深入学习的导读手册,对于理解数据库原理与PostgreSQL内部实现大有裨益。本书适合数据库开发人员及相关领域的研究人员、数据库DBA及高等院校相关专业的学生阅读。
新书出版
→
期待您的加入,一起成长
以上所述就是小编给大家介绍的《解读年度数据库PostgreSQL:如何巧妙地实现缓冲区管理器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Fluent Python
Luciano Ramalho / O'Reilly Media / 2015-8-20 / USD 39.99
Learn how to write idiomatic, effective Python code by leveraging its best features. Python's simplicity quickly lets you become productive with it, but this often means you aren’t using everything th......一起来看看 《Fluent Python》 这本书的介绍吧!