PostgreSQL之buffercache

2023/12/20 14:09
阅读数 19

WAL

DBMS处理的部分数据存储在RAM中,并异步写入磁盘(或其他非易失性存储),即写入被推迟一段时间。这种情况越少发生,输入/输出就越少,系统运行速度就越快。
但是,如果发生故障,例如断电或DBMS或操作系统代码错误,会发生什么?RAM的所有内容都将丢失,只有写入磁盘的数据才能幸存下来(磁盘也不能幸免于某些故障,如果磁盘上的数据受到影响,只有备份副本才能提供帮助)。通常,可以以磁盘上的数据始终一致的方式组织输入/输出,但这很复杂且效率不高(据我所知,只有 Firebird 选择了此选项)。
通常,特别是在PostgreSQL中,写入磁盘的数据似乎不一致,并且在故障后恢复时,需要特殊操作来恢复数据一致性。预写日志记录 (WAL) 只是使之成为可能的一项功能。

Buffer cache

奇怪的是,我们将在讨论缓冲区缓存时开始讨论 WAL。缓冲区缓存不是存储在RAM中的唯一结构,而是其中最关键和最复杂的结构之一。了解它的工作原理本身很重要;此外,我们将使用它作为示例,以熟悉RAM和磁盘如何交换数据。
缓存用于各地的现代计算机系统;仅处理器就有三级或四级缓存。一般来说,需要缓存来缓解两种内存之间的性能差异,其中一种相对较快,但没有足够的内存来绕行,另一种相对较慢,但已经足够了。缓冲区缓存减轻了访问 RAM(纳秒)和磁盘存储(毫秒)之间的差异。
请注意,操作系统还具有解决相同问题的磁盘缓存。因此,数据库管理系统通常试图通过直接访问磁盘而不是通过操作系统缓存来避免双重缓存。但PostgreSQL的情况并非如此:所有数据都是使用正常的文件操作读取和写入的。
此外,磁盘阵列甚至磁盘本身的控制器也有自己的缓存。当我们讨论可靠性时,这将非常有用。
但是,让我们返回到 DBMS 缓冲区缓存。
之所以这样称呼它,是因为它表示为缓冲区数组。每个缓冲区由一个数据页(块)的空间加上标头组成。标头包含以下内容:

  1. 页面在缓冲区中的位置(文件和块号)。

  2. 页面上数据更改的指示器,迟早需要写入磁盘(这样的缓冲区称为脏)。

  3. 缓冲区的使用计数。

  4. 缓冲区的引脚计数。

缓冲区缓存位于服务器的共享内存中,可供所有进程访问。为了处理数据,即读取或更新数据,进程将页面读入缓存。当页面在缓存中时,我们会在 RAM 中使用它并保存在磁盘访问中。

缓存最初包含空缓冲区,并且所有缓冲区都链接到可用缓冲区列表中。指向“下一个受害者”的指针的含义稍后会很清楚。缓存中的哈希表用于快速找到您需要的页面。

在缓存中搜索page

当进程需要读取页面时,它首先尝试通过哈希表在缓冲区缓存中查找它。文件编号和文件中的页号用作哈希键。该过程在相应的哈希存储桶中查找缓冲区编号,并检查它是否确实包含所需的页面。与任何哈希表一样,此处可能会出现冲突,在这种情况下,该过程将不得不检查多个页面。
哈希表的使用长期以来一直是抱怨的来源。像这样的结构能够按页面快速找到缓冲区,但是例如,如果您需要查找某个表占用的所有缓冲区,则哈希表绝对无用。但还没有人提出一个好的替代品。
如果在缓存中找到所需的页面,则进程必须通过增加引脚计数来“固定”缓冲区(多个进程可以同时执行此操作)。固定缓冲区(计数值大于零)时,将认为已使用缓冲区,并且其内容不能“急剧”更改。例如:一个新的元组可以出现在页面上——由于多版本并发和可见性规则,这对任何人都没有伤害。但是,无法将其他页面读入固定缓冲区。

Eviction

有时会经常在缓存中找不到所需的页面。在这种情况下,需要将页面从磁盘读取到某个缓冲区中。
如果空缓冲区在缓存中仍然可用,则选择第一个空缓冲区。但是它们迟早会结束(数据库的大小通常大于为缓存分配的内存),然后我们将不得不选择一个占用的缓冲区,逐出位于那里的页面并将新页面读取到释放的空间中。
逐出技术基于这样一个事实,即对于对缓冲区的每次访问,进程都会递增缓冲区标头中的使用计数。因此,使用频率低于其他缓冲区的缓冲区的计数值较小,因此是逐出的良好候选项。
时钟扫描算法循环遍历所有缓冲区(使用指向“下一个受害者”的指针),并将其使用计数减少 1。为逐出选择的缓冲区是第一个满足以下条件的缓冲区:
1.使用计数为零
2.引脚数为零(即未固定)
请注意,如果所有缓冲区的使用计数都为非零,则算法将不得不在缓冲区中执行多个循环,递减计数值,直到其中一些计数值减少到零。为了使算法避免“跑圈”,使用计数的最大值限制为 5。但是,对于大型缓冲区缓存,此算法可能会导致相当大的开销成本。
找到缓冲区后,将发生以下情况。
固定缓冲区以显示使用它的其他进程。除了固定之外,还使用了其他锁定技术,但我们稍后将更详细地讨论这一点。
如果缓冲区看起来很脏,即包含更改的数据,则不能只是删除页面 - 需要先将其保存到磁盘。这不是一个好情况,因为要读取页面的进程必须等到写入其他进程的数据,但是检查点和后台编写器进程可以缓解这种影响,这将在后面讨论。
然后将新页从磁盘读取到选定的缓冲区中。使用计数设置为 1。此外,必须将对加载页面的引用写入哈希表,以便将来能够查找页面。
对“下一个受害者”的引用现在指向下一个缓冲区,刚刚加载的缓冲区有时间增加使用计数,直到指针循环穿过整个缓冲区缓存并再次返回。

如何查看
像往常一样,PostgreSQL有一个扩展,使我们能够查看缓冲区缓存的内部。
让我们创建一个表并在那里插入一行。

缓冲区缓存将包含哪些内容?至少必须显示添加唯一行的页面。让我们使用以下查询来检查这一点,该查询仅选择与我们的表相关的缓冲区(按数字)并解释:

正如我们所想的:缓冲区包含一页。它是脏的,使用计数等于 1,并且页面未被任何进程固定。
现在,让我们再添加一行并重新运行查询。为了保存击键,我们在另一个会话中插入该行,然后使用该命令重新运行长查询:\g

未添加新缓冲区:第二行适合同一页面。请注意增加的使用计数。

阅读页面后,计数也会增加。
但是,如果我们进行vacuum呢?

VACUUM 创建了能见度map(一页)和自由空间map(有三页,这是这种地图的最小尺寸)。等等。

调整大小

我们可以使用 shared_buffers 参数设置缓存大小。默认值是荒谬的 128 MB。这是在安装 PostgreSQL 后立即增加的参数之一。

请注意,更改此参数需要重新启动服务器,因为缓存的所有内存都是在服务器启动时分配的。
选择合适的值需要考虑什么?
即使是最大的数据库也有一组有限的“热”数据,这些数据一直在密集处理。理想情况下,此数据集必须适合缓冲区缓存(加上一些用于一次性数据的空间)。如果缓存大小较小,则密集使用的页面将不断相互逐出,这将导致过多的输入/输出。但是盲目增加缓存也不好。当缓存很大时,其维护的开销成本会增加,此外,其他用途也需要RAM。
因此,您需要为特定系统选择缓冲区缓存的最佳大小:这取决于数据、应用程序和负载。不幸的是,没有神奇的、一刀切的价值。
通常建议使用1/4的RAM作为第一次近似值(低于10的PostgreSQL版本建议Windows使用较小的尺寸)。
然后我们应该适应这种情况。最好进行实验:增加或减少缓存大小并比较系统特征。为此,您当然需要一个测试环境,并且您应该能够重现工作负载。不建议在生产环境中进行此类实验。
但是,您可以通过相同的扩展直接在实时系统上获取有关所发生情况的一些信息。
例如:您可以按缓冲区的使用情况探索缓冲区的分布:

在这种情况下,计数的多个空值对应于空缓冲区。对于一个什么都没有发生的系统来说,这并不奇怪。
我们可以看到缓存数据库中哪些表的份额以及这些数据的使用强度(“密集使用”是指在此查询中使用计数大于 3 的缓冲区):

例如:我们可以在这里看到该表占用了大部分空间(我们在前面的主题之一中使用了此表),但是它没有被访问很长时间,并且它还没有被逐出,只是因为空缓冲区仍然可用。
您可以考虑其他观点,这些观点将为您提供思考的食粮。您只需要考虑:

  • 您需要多次重新运行此类查询:数字将在一定范围内变化。

  • 不应连续运行此类查询(作为监视的一部分),因为扩展会暂时阻止对缓冲区缓存的访问。

还有一点需要注意。也不要忘记PostgreSQL通过通常的操作系统调用来处理文件,因此会发生双重缓存:页面进入DBMS的缓冲区缓存和操作系统缓存。因此,不命中缓冲区缓存并不总是导致需要实际输入/输出。但是操作系统的逐出策略与DBMS不同:操作系统对读取数据的含义一无所知。

大规模驱逐

批量读取和写入操作容易出现以下风险:有用的页面可能会被“一次性”数据快速逐出缓冲区缓存。
为了避免这种情况,使用了所谓的缓冲环:为每个操作只分配一小部分缓冲缓存。逐出仅在环内执行,因此缓冲区缓存中的其余数据不受影响。
对于大型表(其大小大于缓冲区缓存的四分之一)的顺序扫描,将分配 32 页。如果在扫描表期间,另一个进程也需要这些数据,则它不会从头开始读取表,而是连接到已经可用的缓冲区环。完成扫描后,该过程将继续读取表的“错过”开头。
让我们检查一下。为此,让我们创建一个表,以便一行占据一整页 — 这样计数更方便。缓冲区缓存的默认大小为 128 MB = 16384 页,每页 8 KB。这意味着我们需要在表中插入超过 4096 行,即页面。

现在,我们必须重新启动服务器以清除分析已读取的表数据的缓存。
让我们在重新启动后阅读整个表:

让我们确保表页在缓冲区缓存中仅占用 32 个缓冲区:

但是,如果我们禁止顺序扫描,则将使用索引扫描读取表:

在这种情况下,不使用缓冲区环,整个表将进入缓冲区缓存(以及几乎整个索引):

缓冲环以类似的方式用于vacuum过程(也是 32 页)和批量写入操作复制和创建表作为选择(通常为 2048 页,但不超过缓冲区缓存的 1/8)。

临时表

临时表是通用规则的例外。由于临时数据仅对一个进程可见,因此不需要在共享缓冲区缓存中使用它们。此外,临时数据仅存在于一个会话中,因此不需要针对故障的保护。
临时数据使用拥有表的进程的本地内存中的缓存。由于此类数据仅供一个进程使用,因此不需要使用锁对其进行保护。本地缓存使用正常的逐出算法。
与共享缓冲区缓存不同,本地缓存的内存是根据需要分配的,因为临时表远未在许多会话中使用。单个会话中临时表的最大内存大小受 temp_buffers 参数的限制。


本文分享自微信公众号 - 开源软件联盟PostgreSQL分会(kaiyuanlianmeng)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部