openGauss源码解析(47)
openGauss源码解析:存储引擎源码解析(15)
5. pagewriter线程组
“pagewriter”线程组由多个“pagewriter”线程组成,线程数量等于GUC参数(g_instance.ckpt_cxt_ctl->page_writer_procs.num)的值。“pagewriter”线程组分为主“pagewriter”线程和子“pagewriter”线程组。主“pagewriter”线程只有一个,负责从全局脏页队列数组中批量获取脏页面、将这些脏页批量写入双写文件、推进整个数据库的检查点(故障恢复点)、分发脏页给各个pagewriter线程,以及将分发给自己的那些脏页写入文件系统。子“pagewriter”线程组包括多个子“pagewriter”线程,负责将主“pagewriter”线程分发给自己的那些脏页写入文件系统。
每个“pagewriter”线程的信息保存在PageWriterProc结构体中,该结构体的定义代码如下:
typedef struct PageWriterProc {
PGPROC* proc;
volatile uint32 start_loc;
volatile uint32 end_loc;
volatile bool need_flush;
volatile uint32 actual_flush_num;
} PageWriterProc;
其中:
(1) proc成员为“pagewriter”线程属性信息。
(2) start_loc为分配给本线程待写入磁盘的脏页在全量脏页队列中的起始位置。
(3) end_loc为分配给本线程待写入磁盘的脏页在全量脏页队列中的结尾位置。
(4) need_flush为是否有脏页被分配给本“pagewriter”的标志。
(5) actual_flush_num为本批实际写入磁盘的脏页个数(有些脏页在分配给本“pagewriter”线程之后,可能被“bgwriter”线程写入磁盘,或者被DROP(删除)类操作失效)。
“pagewriter”线程与“bgwriter”线程的差别:“bgwriter”线程主要负责将脏页写入磁盘,以便留出非脏的缓冲区页面用于加载新的物理数据页;“pagewriter”线程主要的任务是推进全局脏页队列数组的进度,从而推进整个数据库的检查点和故障恢复点。数据库的检查点是数据库(故障)重启时需要回放的日志的起始位置lsn。在检查点之前的那些日志涉及的数据页面修改,需要保证在检查点推进时刻已经写入磁盘。通过推进检查点的lsn,可以减少数据库宕机重启之后需要回放的日志量,从而降低整个系统的恢复时间目标(recovery time objective,RTO)。关于“pagewriter”的具体工作原理,将在“4.2.9 持久化及故障恢复机制”小节进行更详细的描述。
6. 双写文件
一般磁盘的最小I/O单位为1个扇区(512字节),大部分文件系统的I/O单位为8个扇区。数据库最小的I/O单位为一个页面(16个扇区),因此如果在写入磁盘过程中发生宕机,可能出现一个页面只有部分数据写入磁盘的情况,会影响当前日志恢复的一致性。为了解决上述问题,openGauss引入了双写文件。所有页面在写入文件系统之前,首先要写入双写文件,并且双写文件以“O_SYNC | O_DIRECT”模式打开,保证同步写入磁盘。因为双写文件是顺序追加的,所以即使采用同步写入磁盘,也不会带来太明显的性能损耗。在数据库恢复时,首先从双写文件中将可能存在的部分写入磁盘的页面进行修复,然后再回放日志进行日志恢复。
此外也可以采用FPW(full page write,全页写)技术解决部分数据写入磁盘问题:在每次检查点之后,对于某个页面首次修改的日志中记录完整的页面数据。但是为了保证I/O性能的稳定性,目前openGauss默认使用增量检查点机制(关于增量检查点机制,参见“4.2.9 持久化及故障恢复机制”节),而该机制与FPW技术无法兼容,所以在openGauss中目前采用双写技术来解决部分数据写入磁盘问题。
结合图4-20,缓冲区页面查找的流程如下。
(1) 计算“buffer tag”对应的哈希值和分区值。
(2) 对“buffer id”哈希表加分区共享锁,并查找“buffer tag”键值是否存在。
(3) 如果“buffer tag”键值存在,确认对应的磁盘页面是否已经加载上来。如果是,则直接返回对应的“buffer id + 1”;如果不是,则尝试加载到该“buffer id”对应的缓冲区内存中,然后返回“buffer id + 1”。
(4) 如果“buffer tag”键值不存在,则寻找一个“buffer id”来进行替换。首先尝试从各个“bgwriter”线程的空闲“buffer id”队列中获取可以用来替换的“buffer id”;如果所有“bgwriter” 线程的空闲buffer id队列都为空队列,那么采用clock-sweep算法,对整个buffer缓冲区进行遍历,并且每次遍历过程中将各个缓冲区的使用计数减一,直到找到一个使用计数为0的非脏页面,就将其作为用来替换的缓冲区。
(5) 找到替换的“buffer id”之后,按照分区号从小到大的顺序,对两个“buffer tag”对应的分区同时加上排他锁,插入新“buffer tag”对应的元素,删除原来“buffer tag”对应的元素。然后再按照分区号从小到大的顺序释放上述两个分区排他锁。
(6) 最后确认对应的磁盘页面是否已经加载上来。如果是,则直接返回上述被替换的“buffer id + 1”;如果不是,则尝试加载到该“buffer id”对应的buffer内存中,然后返回“buffer id + 1”。
行存储共享缓冲区访问的主要接口和含义如表4-27所示。
表4-27 行存储共享缓冲区访问的主要接口
函数名 |
操作含义 |
ReadBufferExtended |
读、写业务线程从共享缓冲区获取页面用于读、写查询 |
ReadBufferWithoutRelcache |
恢复线程从共享缓冲区获取页面用于回放日志 |
ReadBufferForRemote |
备机页面修复线程从共享缓冲区获取页面用于修复主机损坏页面 |