PostgreSQL-bufmmgr

一、 缓冲区管理器的结构

PostgreSQL缓冲区管理器由三层组成,即缓冲表层缓冲区描述符层缓冲池层

  • **缓冲表(buffer table)**层是一个哈希表,它存储着页面的buffer_tag与描述符的buffer_id之间的映射关系。
  • **缓冲区描述符(buffer descriptors)**层是一个由缓冲区描述符组成的数组。 每个描述符与缓冲池槽一一对应,并保存着相应槽的元数据。
  • **缓冲池(buffer pool)**层是一个数组。 每个槽都存储一个数据文件页,数组槽的索引称为buffer_id

1、缓冲表

缓冲表分为三个部分:散列函数(hash function)、散列桶槽(buckets slots)、数据项(data entries)。

内置散列函数将buffer_tag映射到哈希桶槽。可能会发生冲突,采用了使用链表的分离链接方法来解决冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct RelFileNode
{
Oid spcNode; /* tablespace */
Oid dbNode; /* database */
Oid relNode; /* relation */
} RelFileNode;

/* buftag对应的数据结构 */
typedef struct buftag
{
RelFileNode rnode; /* physical relation identifier */
ForkNumber forkNum; /* 关系的分支编号 */
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;

2、缓冲区描述符

缓冲区描述符保存着页面的元数据,这些与缓冲区描述符对应的页面保存在缓冲池槽中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 缓冲区描述符对应的数据结构 */
typedef struct BufferDesc
{
BufferTag tag; /* ID of page contained in buffer; 存储在缓冲区中页面的标识 */
int buf_id; /* buffer's index number (from 0); 缓冲的索引编号(从0开始) */

/* state of the tag, containing flags, refcount and usagecount */
pg_atomic_uint32 state; /* tag的状态, 包含flags(标记位), refcount(在本缓冲区上持有PIN的后端进程数)
, usagecount(时钟扫描要用到的引用计数)*/
/* 其中 flags保存的状态有以下几种:
1.脏位指明相应页面是否为脏页
2.有效位指明相应页面是否可以被读写
3.IO进行标记位指明缓冲区管理器是否正在从存储中读/写相应页面 */

int wait_backend_pid; /* backend PID of pin-count waiter; 等着PIN本缓冲区的后端进程PID */
int freeNext; /* link in freelist chain; 空闲链表中的链接 */
LWLock content_lock; /* to lock access to buffer contents; 访问缓冲区内容的锁 */
} BufferDesc;

缓冲区描述符有三种状态:

  • :当相应的缓冲池槽不存储页面时,即refcount与usagecount都是0,该描述符的状态为空。
  • 钉住:当相应的缓冲池槽中存储着页面,且有PG进程正在访问相应页面时,即refcount与usagecount都大于等于1,该缓冲区描述符的状态为钉住。
  • 未钉住:当相应的缓冲池槽中存储着页面,且没有PG进程正在访问相应页面时,即usagecount大于或等于1,但refcount为0,该描述符的状态为未钉住。

3、缓冲池

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

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

二、缓冲区管理器的工作原理

当后端进程想要访问所需页面时,它会调用 ReadBufferExtended 函数。

函数 ReadBufferExtended 的行为因场景而异,在逻辑上具体可以分为三种情况。

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

流程描述:

  1. 创建所需页面的buffer tag,计算hash值;

  2. 获取BufMappingLock分区锁,加shared锁;

  3. 从buffer hashtable中找到buffer tag,获取buffer id;

  4. 根据buffer id获取buffer descriptor;pin buffer;释放BufMappingLock分区锁;

  5. 从Buffer Pool中获取buffer id对应的page。

    • 在具体读取页面的行时,进程持有对应buffer descriptor的shared content_lock,多个进程可以并发读取。

    • 当修改该页面的行时,进程获取exclusive content_lock,dirty bit也要设为1。

    • 在获取页面后,对应的refcount要加1。

2、将页面从存储加载到空槽

流程描述:

1、查找buffer hashtable,没有找到对应的buffer tag;

2、从freelist中获取第一个空的buffer descriptor,pin住

	1)获取buffer_strategy_lock自旋锁

	2)从空闲列表中弹出一个可用的buffer descriptor

	3)释放buffer_strategy_lock自旋锁

	4)pin buffer

3、获取BufMappingLock分区锁,加exclusive锁;

4、创建包含buffer tag和buffer id的hash entry,插入buffer hashtable;

5、从存储中加载指定页到buffer pool,使用buffer id为数组下标

	1)获取exclusive io_in_progress_lock

	2)io_in_progress bit置1,防止其他进程获取buffer descriptor

	3)从存储中加载指定页到buffer pool

	4)修改buffer descriptor的状态,io_in_progress bit置0,valid bit置1

	5)释放io_in_progress_lock

6、释放BufMappingLock分区锁;

7、从Buffer Pool中获取buffer id对应的page;

3、将页面从存储加载到受害者缓冲池槽

流程描述:

1、查找buffer hashtable,没有找到对应的buffer tag;

2、使用clock-sweep算法选中牺牲的buffer pool槽位,在buffer hashtable中获取包含牺牲槽位buffer id的旧表项,在buffer descriptor中pin住牺牲槽位;

3、flush(write和fsync)牺牲槽位的页表数据,否则直接到4;

	 脏页必须先落盘,所占的slot才能被牺牲。刷脏包括以下步骤:

	1)获取buffer descriptor的shared content_lock和exclusive io_in_progress_lock

	2)修改buffer descriptor状态,io_in_progress bit置1,drity bit置0

	3)根据具体情况,使用XLogFlush将WAL缓冲区的WAL写入WAL段文件

	4)flush牺牲页

	5)修改buffer descriptor状态,io_in_progress置0,valid bit置1

	6)释放io_in_progress_lock和content_lock

4、获取旧页表的exclusive BufMappingLock分区锁;

5、获取新页表的exclusive BufMappingLock分区锁,添加到buffer table

	1)创建包含新buffer tag和buffer id的hash entry

	2)获取新的exclusive BufMappingLock分区锁

	3)插入到buffer hashtable

6、删除buffer hashtable中旧的hash entry,释放旧的BufMappingLock分区锁;

7、从存储中加载到牺牲slot,更新buffer descriptor的flag:dirty位置0,初始化其他bit;

8、释放新的BufMappingLock分区锁;

9、从Buffer Pool中获取指定buffer id对应的page;