目录
1. 结构
1.1 缓冲区标签
tag{(16821,16384,37721),1,3}表示16821号表空间的16384号数据库的37721号表的空闲空间映射(0为关系本体main分支,2位VM分支)的第3页。
1.2 缓冲表
缓冲表为散列表,通过散列函数,可根据缓冲区标签tag找到对应的buffer_id,即数据在缓冲池中的槽位号。散列表通过分离链接方法解决冲突。
1.3 缓冲区描述符
缓冲区描述符层包括与之相关的缓冲池槽位的元数据信息,包括buffer_id,缓冲区标签tag,当前访问页面的进程数refcount,访问次数usage_count,标志脏位的flags,指向下一个描述符的freeNext(所以缓冲区描述符层也是一个链表),以及context_lock与io_in_progress_lock两个锁,后面会介绍。
缓冲区描述符有三种状态:
空:缓冲池槽中无页面,refcount与usage_count均为0。
钉住:当前页面正在被访问,refcount与usage_count均大于等于1。
未钉住:存在该页面,但目前没有进程访问,refcount为0,usage大于等于1(因为至少将其载入缓冲池槽的进程使其加了1)。
其中空缓冲区描述符组成了一个freelist链表。
1.4 缓冲池
缓冲池有多个槽位组成,是一个数组,每个槽位为8k,正好可加载一个磁盘页面。
2. 锁
该锁为缓冲区管理器同步化的一部分,与SQL语句与操作中的锁无关。
2.1 缓冲表锁bufMappingLock
维护缓冲表的锁,分为读取的共享锁与增删的独占锁。
为了不影响其他事务对缓冲表的读写,提高并发量,pg将散列表划分为16组,每组一个锁,一次锁多个桶。
2.2 缓冲区描述符相关锁
内容锁:读取页面时的共享锁以及独占锁,独占锁在以下几种情况下分配:
- 增删改元组t_xmin/t_xmax事务
- 物理删除元组
- 冻结页面中的元组
IO进行锁
进程加载、写入页面时分配。
自旋锁
检查或更新元数据信息字段时获取,如更新refcount等。9.6版后,使用原子操作替代自旋锁。
3. 页面访问
3.1 访问缓冲池中的页面
通过Tag1与散列函数,找到散列表上的桶,
获取桶所在区的缓冲表共享锁,在该桶中找到tag1对应的buffer_id1(一个桶中可能有多个buffer_id)。
钉住buffer_id1的缓冲区描述符,将ref_count与usage_count分别加1.
释放共享锁,访问buffer_id1的缓冲池槽。
3.2 将页面从存储加载到空槽
1.通过Tag1与散列函数,找到散列表上的桶,获取桶所在区的缓冲表共享锁,在该桶中没找到tag1。释放共享锁。
2.从freelist(空缓冲区描述符链表)中获取空缓冲区描述符,并将其钉住,假设获得的是buffer_id2,以独占模式获取桶所在分区的锁。
3.创建一条tag=1,buffer_id=2的数据项,插入桶中。
4.获得排他的IO锁,将数据页面加载到buffer_id=2的缓冲池插槽中,标记vaild为1,释放IO锁。
5.释放桶所在分区的锁。
6.访问buffer_id=2的插槽。
将页面从存储加载到受害者缓冲池槽
- 使用时钟扫描算法选择脏页,将脏页刷到磁盘中
- 在散列表中加新的tag,删旧的tag,并且将脏页腾出的buffer_id设为新tag对应的id。
- 将新数据页面替换缓冲池插槽中的旧数据页面
4. 其他
4.1 时钟扫描的页面置换算法
以时钟的方式扫描缓冲区描述符,跳过被钉住的描述符,对于未被钉住的描述符,如果其usage_count为0,则将其置换出去,若不为0,则将其减1,然后扫描下一个:
(深色的表示被钉住的)
4.2 环形缓冲区
当需要读写大表时,需要大量置换缓冲池,这会导致之后的缓存命中率降低。所以pg使用额外的环形缓冲区来替代缓冲池加载大表。当有以下情况时,会启用环形缓冲区:
- 批量读取。当扫描关系读取数据的大小超过缓冲池的四分之一时,环形缓冲区的大小为 256 KB。(256k正好可以加载进二级缓存)
- 批量写入,当执行下列 SQL 命令时,环形缓冲区大小为 16 MB。
COPY FROM 命令。
CREATE TABLE AS 命令。
CREATE MATERIALIZED VIEW 或 REFRESH MATERIALIZED VIEW 命令。
ALTER TABLE 命令。
- 清理过程,当自动清理守护进程执行清理过程时,环形缓冲区大小为 256 KB。分配的环形缓冲区将在使用后被立即释放。
4.3 脏页刷盘
除了置换脏页时需要脏页刷盘外,检查点进程会在检查点开始时进行脏页刷盘;
后台写入进程通过少量多次的脏页刷盘来减少检查点的密集写入,默认为200ms一次,默认最多刷100个页。