一、缓存区的写入
当后端进程想要访问一个页面时,会调用ReadBufferExtended,一般后端进程访问页面有三种情况
- 页面在缓存池中
- 页面不在缓存池中,从磁盘中将页面加载到缓存描述符空槽
- 页面不在缓存池,从磁盘中将页面加载到缓存描述符受害者槽
1.读取存储在缓存池中的页面
以t1表为例,查询相关视图,得知目前此表在缓存中
test=# SELECT * FROM buffercache('t1');
bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
253 | main | 0 | t | 1 | 0
(1 row)
test=# explain (analyze,buffers,costs off,timing off,summary off) select * from t1;
QUERY PLAN
----------------------------------------
Seq Scan on t1 (actual rows=1 loops=1)
Buffers: shared hit=1
(2 rows)
第二行shared hit 表示缓存命中,即从缓存中读取页面
从缓存中读取页面大致步骤如下:
- 后台进程根据所需页面,创建页面的buffer_tag,上述例子buffer_tag为{(1634,16384,16396),0,0}
- 以buffer_tag作为HASH键,通过HASH函数计算其对应的HASH值,根据HASH值定位到缓存中缓存表对应的HASH桶
- 获取此HASH桶所在分区的BufMappinglock的共享锁。
- 查找此HASH桶下的链表,找到buffer_tag为{(1634,16384,16396),0,0}的数据项,从数据项中获取buffer_id,返回给后台进程
- 后台进程PIN住 该buffer_id对应在缓存池中的buffer,然后释放BufMappinglock
- 获取缓存区首部锁(其实就是 32位 state 字段的1 bit,22位),将state中的18-21 位进行修改,表示usagecount 加1
- 获取缓存内容锁共享模式,访问缓存池
2.磁盘中将页面加载到缓存描述符空槽
重启pg数据库以清空缓存
[pg14@test ~]$ pg_ctl restart -D $PGDATA -l /tmp/logfile
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started
[pg14@test ~]$ psql -Upostgres -d test
psql (14.6)
Type "help" for help.
test=# SELECT * FROM buffercache('t1');
bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
(0 rows)
test=# explain (analyze,buffers,costs off,timing off,summary off) select * from t1;
QUERY PLAN
----------------------------------------
Seq Scan on t1 (actual rows=1 loops=1)
Buffers: shared read=1
Planning:
Buffers: shared hit=16 read=6
(4 rows)
第二行shared read 表示缓存未命中,即从磁盘中读取页面
假设所需页面不在缓存池中,而且空闲链表有空槽,将页面从磁盘读取到缓存,大致步骤如下:
(1)查找缓存表
- 后台进程根据所需页面的buffer_tag,并计算其HASH值
- 以共享模式获取HASH值对应分区的BufMappingLock
- 查找缓存表,没有找到对应的Tag
- 释放BufMappingLock
(2)从freelist 中获取空缓存描述符,并将其钉住
(3)以独占模式获取相应分区的BufMappingLock
(4) 创建一条新的缓存表数据条目 ,(后台进程所需页面的buffer_tag,和钉住的空缓存描述符的buffer_id),并将其连链接对应的HASH 桶链表
(5)获取Buffer I/O锁,buffer io锁不是真正意义上的锁,只是state字段的一个标识位(26位),将所需页面从存储加载到缓存池槽中,修改,修改relcount、usagecount、io标识位(三个操作组合成一个原子操作)。原子操作完成,Buffer I/O锁也就释放了。
(6) 释放相应分区的BufMappingLock
(7)获取buffer 内容锁,访问加载到缓存池中的页面
3.磁盘中将页面加载到缓存描述符受害者槽
当PostgreSQL实例刚启动时,freelist很多,随着不断地从磁盘中缓存页面到缓存池中,freelist最终会被占满,这是就需要选已经被使用的buffer,被选中的buffer 就是“受害者”。PostgreSQL中寻找“受害者”的算法是时钟算法。
时钟算法
时钟的含义就是像时钟的指针一样,一遍一遍的遍历所有的缓存描述符。这个指针在代码的名称是nextVictimBuffer,也称为clock hand。它指向上一次受害者的下一位描述符。当后台进程需要将页面从磁盘读取到内存中,而且缓存描述符列表又没有空槽是,时钟就开始了,步骤如下:
- 获取nextVictimBuffer指针指向的候选缓存区描述符
- 缓存描述符为unpin:如果usage_count 为0 则该缓存区描述符对应的缓存池槽就是”受害者“,否则,usage_count 减一,并迭代至下一缓缓存区描述符
- 缓存描述符为pin:迭代至下一缓存区描述符,并重复检查下一描述符的pin状态,直至找到受害者
usage count 初始为0,当页面第一次加入到缓存池时,对应的缓存描述符的 usage count 加一。再次被访问(查询或修改),再加一。为了避免指针循环次数太多,usage count最大值为5 。
缓存管理器将页面从磁盘加载到缓存池受害者槽步骤如下:
(1)查找缓存表没有找到
(2)时钟算法扫描缓存区描述符,找到受害者缓存区描述符,从受害者缓存表获取buffer_id,并在缓存池中将其PIN住。
(3)如果受害者是脏页,将其数据写入到磁盘
(4)以排他模式获取缓存表旧表项所在分区的BufMappingLock,
(5) 获取新表项所在分区的将新表项BufMappingLock,插入到此缓存表的链表
(6)将旧表项从缓存表链表中删除,释放旧表项的BufMappingLock。
(7) 将目标页数据从存储加载至受害者槽位,用受害者槽位的buffer_id 更新描述符的标识位,将脏位设置为0,。
(8) 释放缓存表上新表项所在分区的BufMappingLock
(9) 访问对应的缓存区槽位
二、缓存区的写出
当脏页被选定为受害者时,需要将缓存区的脏页写入存储,写出步骤如下:
- 获取对应缓存描述符上的共享内容锁,并buffer IO标识位设为1。
- 根据具体情况,调用XlogFlush()函数,将WAL缓存区上的WAL数据写入到当前的WAL段文件,
- 将脏页数据写到磁盘中
- 更新buffer IO 标识位为0,并释放缓存描述符上的内容锁