[译] 8. PG缓冲区管理器(Buffer Manager)

[译] 8. 缓冲区管理器(Buffer Manager)

原文地址:https://www.interdb.jp/pg/pgsql02.html

原文作者:Hironobu SUZUKI

缓冲管理器管理共享内存和持久存储之间的数据传输,并且可以对 DBMS 的性能产生重大影响。 PostgreSQL 缓冲区管理器的工作效率很高。

在本章中,将描述 PostgreSQL 缓冲区管理器。第一部分提供其概述,后续部分描述以下主题:

  • 缓冲管理器结构
  • 缓冲管理器锁
  • 缓冲管理器工作原理
  • 环形缓冲区(Ring Buffer)
  • 刷新脏页

Fig. 8.1. Relations between buffer manager, storage, and backend processes.

Fig. 8.1. Relations between buffer manager, storage, and backend processes.

8.1 概述

本节介绍了便于后续章节所需的关键概念。

8.1.1 缓冲管理器结构

PostgreSQL 缓冲管理器包含在下一节中介绍的缓冲表,缓冲区描述符和缓冲池。缓冲池存储了数据文件页面,如表和索引,以及可用空间映射和可见性映射。它是一个数组,即每个槽存储一个数据文件的一页。缓冲池数组的索引(下标)称为buffer_id

8.2 和 8.3 节描述了缓冲区管理器内部的细节。

8.1.2 缓冲区标签(Buffer Tag)

在PostgreSQL中,所有数据文件的每一页都可以分配一个唯一的标签,即缓冲区标签。当缓冲区管理器收到请求时,PostgreSQL 使用所需页面的 buffer_tag。

buffer_tag由三个值组成:RelFileNode及其页面所属关系(表)的fork编号,以及其页面的块编号。表、可用空间映射和可见性映射的fork编号分别定义为 0、1 和 2。

例如,缓冲区标签'{(16821, 16384, 37721), 0, 7}' 标识了表的第7块block的页面,其relation OID 为 37721,fork 编号是0。该 relation 属于 OID 为16821的数据库中,位于 OID 为 16384 的表空间下。类似的,缓冲区标签 '{(16821, 16384, 37721), 1, 3}' 标识可用空间映射的第3块的页面的OID 是 37721,fork 编号为1。

缓冲区标签格式:

8.1.3 后端进程如何读取页面

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

Fig. 8.2. How a backend reads a page from the buffer manager.

Fig. 8.2. How a backend reads a page from the buffer manager.

(1) 读取表或索引页时,后端进程向缓冲区管理器发送包含该页的 buffer_tag 的请求。

(2) 缓冲区管理器返回存储请求页的槽的buffer_ID。如果请求的页面未存储在缓冲池中,则缓冲管理器将页面从持久存储加载到缓冲池其中的一个槽上,然后返回 该槽的buffer_ID。

(3) 后端进程访问buffer_ID的槽(slot)(读取想要的页面)。

当后端进程修改缓冲池中的页面时(例如,通过插入元组),尚未刷新到存储的修改页面称为脏页面。

8.4 节描述了缓冲区管理器是如何工作的。

8.1.4 页面替换算法

当所有缓冲池槽都被占用但请求的页面没有被存储时,缓冲管理器必须在缓冲池中选择一页将被请求的页面替换。通常,在计算机科学领域,页面选择算法被称为页面替换算法,被选中的页面被称为"受害者页面(victim page)"。

自计算机科学出现以来,页面替换算法的研究一直在前进;从而,之前已经提出了许多替换算法。从 PostgreSQL 8.1 版本开始使用时钟扫描(clock sweep)算法,因为它比以前版本中使用的 LRU 算法更简单、更高效。

8.4.4 节描述了时钟扫描的细节。

8.1.5 刷新脏页

脏页最终应该被刷新到存储中;但是,缓冲区管理器需要帮助才能执行此任务。在 PostgreSQL 中,有两个后台进程( checkpointer 和 background writer )帮助完成这个任务。

8.6 节描述了检查点进程(checkpointer )和 后台写进程(background writer)。

Direct I/O

PostgreSQL 不支持直接 I/O,尽管有时已经讨论过了。如果您想了解更多详细信息,请参阅有关 pgsql-ML 的讨论论文

8.2 缓冲区管理器结构

PostgreSQL 缓冲区管理器包括三层,即缓冲区表、缓冲区描述符和缓冲池(图 8.3):

Fig. 8.3. Buffer manager's three-layer structure.

Fig. 8.3. Buffer manager's three-layer structure.

  • buffer pool:缓冲池是一个数组。每个槽存储一个数据文件页。数组槽的索引称为 buffer_ids。

  • buffer descriptors:缓冲区描述符层是一个缓冲区描述符数组。每个描述符与缓冲池槽一一对应,并在对应槽中保存存储页面的元数据。

    请注意,为方便起见,采用了术语“缓冲区描述符层”,并且仅在本文档中使用。

  • buffer table:缓冲区表是一个哈希表,它存储了存放页面的buffer_tag和保存了存放各自元数据描述符的buffer_id之间的关系。

下面小节中将会介绍这些层的详细内容。

8.2.1 缓冲区表(buffer table)

缓冲区表在逻辑上可以分为三部分:哈希函数(hash function)、哈希桶槽(hash bucket slots)和数据条目(data entries)(图 8.4)。

Fig. 8.4. Buffer table.

内建的哈希函数将buffer_tag 映射到哈希桶槽。即使哈希桶槽的数量大于缓冲池槽的数量,可能发生(哈希)碰撞。哈希区表使用单独的链接列表的方法解决冲突问题。当数据条目映射到同一个桶槽时,该方法将条目存储在同一个链表中,如图8.4所示。

数据条目包含两个值:页面的 buffer_tag 和保存页面元数据的描述符的 buffer_id。例如,数据条目‘Tag_A, id=1’意味着buffer_id为1的缓冲区描述符存储了带有Tag_A标签的页面的元数据。

hash function(哈希函数)

哈希函数是 calc_bucket() 和 hash() 的复合函数。以下是其伪代码的表示。

uint32 bucket_slot = calc_bucket(unsigned hash(BufferTag buffer_tag), uint32 bucket_size)

请注意,这里未介绍基本操作(数据条目的查找、插入和删除)。这些是非常常见的操作,将在以下部分中进行说明。

8.2.2 缓冲区描述符(Buffer Desciptor)

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

缓冲区描述符保存了对应缓冲区池槽存储的页面的元数据。它由 [BufferDesc](javascript:void(0))结构定义。该结构由多个字段,下面显示一些主要的列:

  • tag: 保存了存储缓冲池槽对应页的 buffer_tag 信息(缓冲区标签在第8.1.2节定义)

  • buffer_id:标识描述符(相当于对应缓冲池槽的buffer_id)

  • refcount:保存当前正在访问相关存储页的PostgreSQL进程数量,也称为pin count。当PostgreSQL访问存储页时,refcount必须增加1(refcount++),访问过后,refcount 必须减1(refcout--)

    当 refcount 为0时,即当前未访问相关的存储页时,该页面为unpinned,否则是pinned

  • usage_count:保存相关页面自加载到相应缓冲池槽以来被访问次数。请注意,usage_count用于页面替换算法(第8.4.4节)

  • context_lock 和 io_in_progress_lock:用于控制访问相关存储页面的轻量级锁。它在第8.3.2节介绍说明

  • flags:保存存储页面的几种状态。下面显示了主要的状态值

    • dirty bit:标识存储的页面是否为脏页
    • valid bit:标识存储页面能否读或写(valid)。例如:如果该标识位的值为'valid',则对应的缓冲池槽存储了一个页面并且描述符(valid bit)保存了该页的元数据;因此,可以读取或写入存储的页面。如果值为'invalid',则该描述符未保存任何元数据;这意味着存储的页面无法读取或写入,或者缓冲区管理器正在替换存储的页面
    • io_in_progress bit:表示缓冲区管理器正在读取/写入相关存储页面。换句话说,它表示单个进程是否持有该描述符的io_in_progress_lock
  • freeNext:指向下一个描述符的指针以生成一个 freelist。将在下一小节中介绍

为了简化以下描述,定义了三个描述符状态:

  • Empty:当对应的缓冲池槽未存储页面时(即 refcount 和 usage_count都为0),描述符的状态为empty
  • Pinned:当对应的缓冲池槽存储了一个页面且PostgreSQL进程可访问该页面(即refcount 和 usage_count 的值都大于或等于1),该缓冲区描述符的状态为 pinned
  • Unpinned:当对应的缓冲池槽存储了一个页面但PostgreSQL进程不可访问该页面(即 usage_count 的值都大于或等于1,但refcount值为0),该缓冲区描述符的状态为 unpinned

每个描述符将具有上述状态之一。描述符状态相对于特定条件发生变化而改变,这将在下一小节中描述。

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

image-20220323213717912

此外,脏页表示为“X”。例如,未固定的脏描述符由image-20220323214030148表示。

8.2.3 缓冲区描述符层(Buffer Descriptors Layer)

缓冲区描述符的集合形成一个数组。在本文档中,该数组被称为缓冲区描述符层。

PostgreSQL server启动时,所有缓冲区描述符的状态为empty。在PostgreSQL 中,这些描述符构成一个链接列表称为freelist(图8.5)。

Fig. 8.5. Buffer manager initial state.

Fig. 8.5. Buffer manager initial state.

请注意,PostgreSQL中的 freelist 和 Oracle 中的 freelist 完全不同的概念。PostgreSQL 中的 freelist 只是empty 缓冲区描述的链表。在 5.3.4 节中描述的PostgreSQL 可用空间映射(VM) 与 Oracle中的 freelist 作用相同。

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

(1) 从 freelist 的顶部回收一个empty缓冲区描述符并将其固定(pin)。(即将refcount 和 usage_count 增加 1)

(2) 在缓冲区表(哈希表)中插入新条目,它保存了第一页的标签和上一步回收的描述符的buffer_id之间的关系

(3) 将新页面从存储加载到对应的缓冲池槽

(4) 将新页面的元数据保存到之前回收的描述符中。

Fig. 8.6. Loading the first page.

Fig. 8.6. Loading the first page.

从freelist中回收的描述符总是包含页面的元数据。换句话说,non-empty 描述符可以继续被使用而不会返回到freelist中。但是,当发生以下情形之一时,描述符的状态变为empty并且重新添加到freelist:

  1. 表或索引已被删除
  2. 数据库已被删除
  3. 表或索引已使用vacuum full命令清理

为什么empty描述符组成freelist?

为了立刻获取第一个描述符。这是动态内存资源分配的常用手段。请参阅文档

缓冲区描述符层包含一个无符号的 32 位整数变量,即 nextVictimBuffer。该变量用于第 8.4.4 节中描述的页面替换算法。

8.2.4 缓冲池(Buffer Pool)

缓冲池是一个简单数组,用于存储数据文件页面,如表和索引。缓冲池数组的下标称为 buffer_id

缓冲池槽的大小为8KB,和页面的大小相等。因此,每一个槽能够存储整个页面。

8.3 缓冲区管理器锁

缓冲区管理器为不同的目的使用不同的锁。本节描述了后续章节中所需锁的说明。

请注意,本节中描述的锁是缓冲区管理器同步机制的一部分;它们与任何 SQL 语句和 SQL 选项无关。

8.3.1 缓冲区表锁(Buffer Table Locks)

BufMappingLock 保护整个缓冲区表的数据完整性。它是一种可用于共享(shared )模式和独占(exclusive)模式的轻量级锁。在缓冲区表中检索条目时,该后端进程便持有了一个共享的BufMappingLock。在插入或更新条目时,后端进程持有一个独占锁。

将 BufMappingLock 拆分为分区以减少缓冲表中的争用(默认128个分区)。每个BufMappingLock 分区保护部分对应的哈希桶槽(hash bucket slot)。

图8.7 展示了一个典型的拆分 BufMappingLock 效果的例子。两个后端进程可以同时以独占模式持有各自的 BufMappingLock 分区,以便插入新的数据条目。如果 BufMappingLock 是单个系统范围(system-wide)的锁,则两个进程都应等待另一个进程的处理,具体取决于哪个进程先开始处理。

Fig. 8.7. Two processes simultaneously acquire the respective partitions of BufMappingLock in exclusive mode to insert new data entries.

Fig. 8.7. Two processes simultaneously acquire the respective partitions of BufMappingLock in exclusive mode to insert new data entries.

缓冲表需要许多其他锁。例如,缓冲区表在内部使用自旋锁(spin lock)来删除条目。但是,本文省略了对这些锁的描述,因为后续未涉及到它们。

在 9.4 版本之前,BufMappingLock 默认被拆分为 16 个单独的锁。

8.3.2 每个缓冲区描述符的锁

每个缓冲区描述符使用两个轻量级锁 content_lock 和 io_in_progress_lock 来控制对相应缓冲池槽中存储页面的访问。当检查或更改自己字段的值时,使用自旋锁。

8.3.2.1 content_lock

content_lock 是一个典型的强制性访问限制的锁,它可用于共享和独占模式。

在读取一个页面时,后端进程请求获取存储该页面缓冲区描述符的shared conntent_lock。

但是,在执行以下其中一个操作时,需要获取独占模式的content_lock:

  1. 往存储页面插入行(元组)或更改存储页面内元组的t_xmin/t_xmax 列(t_xmin/t_xmax 已在第5.2节介绍过;简单地说,当删除或更新行时,元组的这些字段就会改变)
  2. 物理删除元组或压缩存储页面的可用空间(分别通过执行第6章描述的vacuum处理和第7章描述的HOT)
  3. 冻结存储页面中的元组(冻结在第 5.10.1 节和第 6.3 节中描述)

官方README 文件显示了更多详细内容。

8.3.2.2 io_in_progress_lock

io_in_progress 锁用于等待缓冲区上的I/O完成。当 PostgreSQL 进程从/向磁盘加载/写入页面数据时,该进程在访问磁盘时持有相应描述符的独占 io_in_progress 锁。

8.3.3.3 spinlock(自旋锁)

当检查或更改标志(flag)或其他字段(例如 refcount 和 usage_count)时,将使用自旋锁。下面给出了使用自旋锁的两个具体示例:

(1) 下面显示了如何固定(pin)缓冲区描述符:

  1. 获取缓冲区描述符的自旋锁
  2. 将refcount 和 usage_count 的值加1
  3. 释放自旋锁
LockBufHdr(bufferdesc);    /* Acquire a spinlock */
bufferdesc->refcont++;
bufferdesc->usage_count++;
UnlockBufHdr(bufferdesc); /* Release the spinlock */

(2) 下面显示了如何将脏位(dirty bit)设置为'1':

  1. 获取缓冲区描述符的自旋锁
  2. 使用按位运算将脏位设置为'1'
  3. 释放自旋锁
#define BM_DIRTY             (1 << 0)    /* data needs writing */
#define BM_VALID             (1 << 1)    /* data is valid */
#define BM_TAG_VALID         (1 << 2)    /* tag is assigned */
#define BM_IO_IN_PROGRESS    (1 << 3)    /* read or write in progress */
#define BM_JUST_DIRTIED      (1 << 5)    /* dirtied since write started */

LockBufHdr(bufferdesc);
bufferdesc->flags |= BM_DIRTY;
UnlockBufHdr(bufferdesc);

以相同的方式更改其他位。

用原子操作替换缓冲区管理器自旋锁

在 9.6 版本中,缓冲区管理器的自旋锁将被替换为原子操作。查看 commitfest 的结果。如果您想了解详细信息,请参阅此讨论

8.4 缓冲区管理器工作原理

本节描述缓冲区管理器的工作原理。后端进程想要访问所需的页面时,调用ReadBufferExtended 函数。

ReadBufferExtended 函数的行为取决于三种逻辑情况。以下每一小节描述一种情况,此外,PostgreSQL 的时钟扫描页面替换算法在最后一小节描述。

8.4.1 访问存储在缓冲池中的页面

首先,描述最简单的情况,即所需页面已经存储在缓冲池中。在这种情况下,缓冲区管理器执行以下步骤:

(1) 创建所需页面的buffer_tag(在本例中,buffer_tag 为'Tag_C')并使用哈希函数计算包含所创建的buffer_tag 的相关条目的哈希桶槽。

(2) 以共享模式获取覆盖所获得的哈希桶槽的BufMappingLock分区(该锁将在步骤(5)中释放)。

(3) 查找标签为'Tag_C'的条目,并从该条目中获取buffer_id。在本例中,buffer_id 为 2。

(4) 为 buffer_id 2 (pin)固定缓冲区描述符,即为描述符的 refcount 和 usage_count 增加 1(第 8.3.2 节描述了 pinning)。

(5) 释放BufMappingLock

(6) 访问 buffer_id 为 2 的缓冲池槽。

Fig. 8.8. Accessing a page stored in the buffer pool.

Fig. 8.8. Accessing a page stored in the buffer pool.

然后,当从缓冲池槽中的页面读取行时,PostgreSQL 进程获取相应缓冲区描述符的共享 content_lock。因此,缓冲池槽可以被多个进程同时读取。

当往页面插入(和更新或删除)行时,Postgres 进程获取相应缓冲区描述符的独占 content_lock(注意页面的脏位必须设置为 '1')。

访问页面后,相应缓冲区描述符的 refcount 值减 1。

8.4.2 将页面从磁盘(存储)储加载到空槽

在第二个案例中,假设所需的页面不在缓冲池中,并且freelist中有空元素(空描述符)。在这种情况下,缓冲区管理执行以下步骤:

(1) 查找缓冲表(我们假设它没有找到)。

  1. 为所需页面创建buffer_tag(在本例中,buffer_tag 为 'Tag_E')并计算哈希桶槽
  2. 以共享模式获取 BufMappingLock 分区
  3. 查找缓冲表(根据假设没有找到)
  4. 释放BufMappingLock

(2) 从freelist中获取空缓冲描述符并固定(pin)它,在本例中,获取到的描述符的buffer_id为4

(3) 以独占模式获取BufMappingLock 分区(该锁将在步骤(6)释放)

(4) 创建一个包含buffer_tag ‘Tag_E’和buffer_id 4的新数据条目;将创建的条目插入缓冲表。

(5) 将所需的页面数据从磁盘加载到 buffer_id 为 4 的缓冲池槽中,如下所示:

  1. 获取对应描述符的独占io_in_progress_lock
  2. 设置对应描述符的io_in_progress位为'1',防止其他进程访问
  3. 将所需的页面数据从磁盘加载到缓冲池槽
  4. 改变对应缓冲区描述符的状态;io_in_progress 设置为'0',valid 设置为'1'
  5. 释放io_in_progress_lock

(6) 释放BufMappingLock

(7) 访问 buffer_id 为 4 的缓冲池槽

Fig. 8.9. Loading a page from storage to an empty slot.

Fig. 8.9. Loading a page from storage to an empty slot.

8.4.3 将页面从磁盘加载到已占满的缓冲池槽

在这个案例中,假设所有缓冲池槽已被页面占用,但没有存储所需的页面。缓冲区管理器执行以下步骤以获取所需的页面:

(1) 为所需页面创建buffer_tag并查找缓冲表。在此例中,我们假设 buffer_tag 为 'Tag_M'(未找到所需页面)

(2) 使用时钟扫描(clock-sweep)算法选择一个“牺牲”缓冲池槽,从缓冲区表中获取包含“牺牲”缓冲池槽的buffer_id的旧条目,并将“牺牲”缓冲池槽pin(固定)到缓冲区描述符层中。在此例中,“牺牲”缓冲池槽的buffer_id 是5,旧条目是'Tag_F, id=5'。时钟扫描将在下一节中描述。

(3) 如果“牺牲”页面数据是脏的,则刷新(写入和fsync)到磁盘。否则,执行步骤(4)

​ 脏页必须在新数据覆盖前写入到存储(磁盘)。刷新脏页的操作如下:

  1. 获取buffer_id为5的描述符的共享content_lock和独占io_in_progress锁(在步骤6中释放)
  2. 更改对应描述符的状态;io_in_progress 设置为'1',just_dirtied 设置为'0'
  3. 根据情况,调用XLogFlush()函数将WAL缓冲区上的WAL数据写入当前WAL文件(具体省略,WAL和XLogFlush函数在第9章介绍)
  4. 将“牺牲”页数据刷新到存储
  5. 更改对应描述符的状态;io_in_progress 设置为'0',valid 设置为'1'
  6. 释放the io_in_progress 和 content_lock 锁

(4) 以独占模式获取覆盖包含旧条目的槽的旧BufMappingLock分区

(5) 获取新的 BufMappingLock 分区并将新条目插入到缓冲表中:

  1. 创建由新的 buffer_tag 'Tag_M' 和“牺牲” buffer_id 组成的新条目
  2. 以独占模式获取覆盖包含新条目的槽的新 BufMappingLock 分区
  3. 将新条目插入缓冲表

Fig. 8.10. Loading a page from storage to a victim buffer pool slot.

Fig. 8.10. Loading a page from storage to a victim buffer pool slot.

(6) 从缓冲区表删除旧条目并释放旧BufMappingLock 分区

(7) 从存储中加载所需页面数据到“牺牲”缓冲区槽。然后,更新buffer_id 5的描述符标签;dirty bit设置为0并初始化其它标识位

(8) 释放新BufMappingLock 分区

(9) 访问buffer_id 5的缓冲池槽

Fig. 8.11. Loading a page from storage to a victim buffer pool slot (continued from Fig. 8.10).

Fig. 8.11. Loading a page from storage to a victim buffer pool slot (continued from Fig. 8.10).

8.4.4 页面替换算法:时钟扫描(clock-sweep)

本节的其余部分描述时钟扫描算法。该算法是 NFU (Not Frequently Used) 的一种变体,开销较低。它有效地选择不常用的页面。

将缓冲区描述符想象成一个循环列表(图 8.12)。nextVictimBuffer 是一个无符号的 32 位整数,始终指向其中一个缓冲区描述符并顺时针旋转。该算法的伪代码和描述如下:

Pseudocode: clock-sweep

  WHILE true
(1)     Obtain the candidate buffer descriptor pointed by the nextVictimBuffer
(2)     IF the candidate descriptor is unpinned THEN
(3)	       IF the candidate descriptor's usage_count == 0 THEN
	            BREAK WHILE LOOP  /* the corresponding slot of this descriptor is victim slot. */
	       ELSE
		    Decrease the candidate descriptpor's usage_count by 1
            END IF
      END IF
(4)     Advance nextVictimBuffer to the next one
   END WHILE 
(5) RETURN buffer_id of the victim

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

(2) 如果候选缓冲区描述符是unpinned,则进行步骤(3);否则,进行步骤(4)

(3) 如果候选缓冲区描述符的 usage_count 为0,则选择该描述符对应的槽作为"牺牲者"并执行步骤(5);否则,将该描述符的usage_count 减1,然后执行步骤(4)

(4) 将 nextVictimBuffer 推进到下一个描述符(如果在最后,则回绕)并返回步骤(1)。重复直到找到"牺牲者"

(5) 返回"牺牲者"的buffer_id

具体示例如图 8.12 所示。缓冲区描述符显示为蓝色或青色框,框中的数字显示每个描述符的 usage_count。

Fig. 8.12. Clock Sweep.

Fig. 8.12. Clock Sweep.

  1. nextVictimBuffer 指向第一个描述符(buffer_id 1);但是,此描述符被跳过,因为它是pinned状态
  2. nextVictimBuffer 指向第二个描述符(buffer_id 2)。此描述符是unpinned ,但其 usage_count 为2;因此,usage_count 减 1,nextVictimBuffer 前进到第三个候选者
  3. extVictimBuffer 指向第三个描述符(buffer_id 3)。这个描述符是unpinned并且它的usage_count是0;因此,这是本轮的"牺牲者"

每当 nextVictimBuffer 扫过一个 unpinned 描述符时,它的 usage_count 就会减 1。因此,如果缓冲池中存在 unpinned 描述符,该算法总是可以通过轮换 nextVictimBuffer 找到一个usage_count 为0 的"牺牲者"。

8.5 环形缓冲区(Ring Buffer)

当读取或写入一个巨大的表时,PostgreSQL 使用环形缓冲区而不是缓冲池。环形缓冲区是一个小且临时的缓冲区区域。当满足下面列出的任何条件时,将向共享内存分配一个环形缓冲区:

  1. 批量读(Bulk-reading)

    当扫描大小超过缓冲池大小 (shared_buffers/4) 四分之一的关系(表)时。在这种情况下,环形缓冲区大小为 256 KB。

  2. 批量写(Bulk-writing)

    当执行下面列出的 SQL 命令时。在这种情况下,环形缓冲区大小为 16 MB。

  3. Vacuum处理

​ 当 autovacuum 执行vacuum处理时。在这种情况下,环形缓冲区大小为 256 KB。

分配的环形缓冲区在使用后立即释放。

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

为什么批量读取和vacuum处理的默认环形缓冲区大小为 256 KB?

为什么是 256 KB?答案在位于缓冲区管理器源目录下的 README 中进行了解释。

对于顺序扫描,使用 256 KB 的环。这已足以适合 L2 缓存,使得将页面从 OS 缓存传输到共享缓冲区缓存有效。通常甚至更少就足够了,但是环必须足够大以容纳同时扫描所有页面的pinned。

8.6 刷新脏页

除了替换"牺牲者"页面之外,检查点和后台写进程将脏页刷新到存储中。两个进程具有相同的功能(刷新脏页);但是,他们有不同的角色和行为。

checkpointer 进程将检查点记录写入 WAL 文件,并在检查点开始时刷新脏页。9.7 节描述了检查点以及它何时开始。

background writer 进程的作用是减少检查点密集写入的影响。它持续一点一点地刷新脏页,对数据库的性能你影响最小。默认情况下,bgwriter 每 200 毫秒(由 bgwriter_delay 定义)唤醒一次,并且最多刷新 bgwriter_lru_maxpages(默认为 100 页)。

为什么检查点与 bgwriter 分开?

在 9.1 或更早的版本中,bgwriter 会定期进行检查点处理。在 9.2 版本中,checkpointer 进程已从bgwriter 进程中分离处理。在标题为“Separating bgwriter and checkpointer”的提案中描述了原因,因此其原文如下所示。

Currently(in 2011) the bgwriter process performs both background writing, checkpointing and some other duties. This means that we can't perform the final checkpoint fsync without stopping background writing, so there is a negative performance effect from doing both things in one process.
Additionally, our aim in 9.2 is to replace polling loops with latches for power reduction. The complexity of the bgwriter loops is high and it seems unlikely to come up with a clean approach using latches.

(snip)

翻译

目前(2011 年)bgwriter 进程同时执行后台写入、检查点和其他一些职责。这意味着我们无法在不停止后台写入的情况下执行最终检查点 fsync,因此在一个进程中执行这两项操作会对性能产生负面影响。

此外,我们在 9.2 中的目标是用闩锁(latch)替换轮询循环以降低功耗。 bgwriter 循环的复杂性很高,似乎不太可能提出使用闩锁的完整方法。

(裁剪)

附录

BufferDesc 结构

BufferDesc 结构在 src/include/storage/buf_internals.h 文件中定义。

/*
 * Flags for buffer descriptors
 *
 * Note: TAG_VALID essentially means that there is a buffer hashtable
 * entry associated with the buffer's tag.
 */
#define BM_DIRTY                (1 << 0)    /* data needs writing */
#define BM_VALID                (1 << 1)    /* data is valid */
#define BM_TAG_VALID            (1 << 2)    /* tag is assigned */
#define BM_IO_IN_PROGRESS       (1 << 3)    /* read or write in progress */
#define BM_IO_ERROR             (1 << 4)    /* previous I/O failed */
#define BM_JUST_DIRTIED         (1 << 5)    /* dirtied since write started */
#define BM_PIN_COUNT_WAITER     (1 << 6)    /* have waiter for sole pin */
#define BM_CHECKPOINT_NEEDED    (1 << 7)    /* must write for checkpoint */
#define BM_PERMANENT            (1 << 8)    /* permanent relation (not unlogged) */

src/include/storage/buf_internals.h
typedef struct sbufdesc
{
   BufferTag    tag;                 /* ID of page contained in buffer */
   BufFlags     flags;               /* see bit definitions above */
   uint16       usage_count;         /* usage counter for clock sweep code */
   unsigned     refcount;            /* # of backends holding pins on buffer */
   int          wait_backend_pid;    /* backend PID of pin-count waiter */
   slock_t      buf_hdr_lock;        /* protects the above fields */
   int          buf_id;              /* buffer's index number (from 0) */
   int          freeNext;            /* link in freelist chain */

   LWLockId     io_in_progress_lock; /* to wait for I/O to complete */
   LWLockId     content_lock;        /* to lock access to buffer contents */
} BufferDesc;
posted @ 2022-03-27 20:14  KuBee  阅读(953)  评论(0编辑  收藏  举报