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 | typedef struct RelFileNode |
2、缓冲区描述符
缓冲区描述符保存着页面的元数据,这些与缓冲区描述符对应的页面保存在缓冲池槽中。
1 | /* 缓冲区描述符对应的数据结构 */ |
缓冲区描述符有三种状态:
- 空:当相应的缓冲池槽不存储页面时,即refcount与usagecount都是0,该描述符的状态为空。
- 钉住:当相应的缓冲池槽中存储着页面,且有PG进程正在访问相应页面时,即refcount与usagecount都大于等于1,该缓冲区描述符的状态为钉住。
- 未钉住:当相应的缓冲池槽中存储着页面,且没有PG进程正在访问相应页面时,即usagecount大于或等于1,但refcount为0,该描述符的状态为未钉住。
3、缓冲池
缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引就是buffer_id。
缓冲池槽的大小为8KB,等于页面大小,因此每个槽都能存储整个页面。
二、缓冲区管理器的工作原理
当后端进程想要访问所需页面时,它会调用 ReadBufferExtended 函数。
函数 ReadBufferExtended 的行为因场景而异,在逻辑上具体可以分为三种情况。
1、访问存储在缓冲池中的页面
流程描述:
-
创建所需页面的buffer tag,计算hash值;
-
获取BufMappingLock分区锁,加shared锁;
-
从buffer hashtable中找到buffer tag,获取buffer id;
-
根据buffer id获取buffer descriptor;pin buffer;释放BufMappingLock分区锁;
-
从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;