存储引擎源码解析 | 磁盘引擎(17)

2023年 11月 23日 55.5k 0

4.2.9 持久化及故障恢复机制

1. 行存储持久化和检查点机制

如“4.2.8 日志系统”节中所述,通过采用WAL日志的方式可以在对性能影响较小的情况下保障用户事务对数据库修改的持久化。然而如果只是依赖日志来保障持久化的话,那么数据库服务(故障)重启之后将需要回放大量的日志数据量,这会导致很大的RTO,对业务的可用性影响极大。因此共享缓冲区中的脏页也需要异步地写入磁盘中,来减少宕机重启后所需要回放的日志数据量,降低系统的RTO时间。
如果数据库系统在事务提交之后、异步写入磁盘的脏页写入磁盘之前发生宕机,那么需要在数据库再次启动之后,首先把那些宕机之前还没有来得及写入磁盘的脏页上的修改所对应的日志进行回放,使得这些脏页可以恢复到宕机之前的内容。
基于如上原理,可以得出数据库持久化的一个关键是:在宕机重启的时候,通过某种机制确定从WAL的哪个lsn开始进行恢复;可以保证在该lsn之前的那些日志,它们涉及的数据页面修改已经在宕机之前完成写入磁盘。这个恢复起始的lsn,即是数据库的检查点。
在“4.2.6 行存储缓存机制”节介绍行存储缓存加载和淘汰机制中,已经知道参与脏页写入磁盘的主要有两类线程:bgwriter和pagewriter。前者负责脏页持久化的主体工作;后者负责数据库检查点lsn的推进。openGauss采用一个无锁的全局脏页队列数组来依次记录曾经被用户写操作置脏的那些数据页面。该队列数组成员为DirtyPageQueueSlot结构体,定义代码如下,其中:buffer为队列成员对应的buffer(该值为buffer id + 1),slot_state为该队列成员的状态。

volatile int buffer;
pg_atomic_uint32 slot_state;
} DirtyPageQueueSlot;


图4-34 全局脏页队列的运行机制和检查点的推进机制

全局脏页队列的运作机制如图4-34所示,它的实现方式是一个多生产者、单消费者的循环数组。单个/多个业务线程是脏页队列的生产者,在其要修改数据页面之前,首先判断该页面buffer desc的首次脏页lsn是否非0:若该脏页buffer desc中的首次脏页lsn已经非0,说明该脏页在之前置脏的时候就已经被加入到脏页队列中,那么本次就跳过加入脏页队列的步骤;否则,对当前脏页队列的tail位置进行CAS加1操作,完成队列占位,同时,在上述CAS操作中,获取了脏页队列的lsn位置lsn1。然后,将占据的槽位位置(即CAS之前的tail值)和lsn1记录到脏页的buffer desc中。接着,将脏页的“buffer id”记录到占位的槽位中,再将槽位状态置为valid。最后,记录页面修改的日志,并尝试将该日志的位置lsn2更新到脏页队列的lsn中(如果此时脏页队列的lsn值已经被其他写业务更新为更大的值,则本线程就不更新了,也是一个CAS操作)。
基于上面这种机制,当将脏页队列中某个成员对应的脏页写入磁盘之后,检查点即可更新到该脏页“buffer desc”中记录的lsn位置。小于该lsn位置的日志,它们对应修改的页面,已经在记录这些日志之前就被加入到脏页队列中,亦即这些脏页在全局脏页队列中的位置一定比当前脏页更靠前,因此一定已经保证写入磁盘了。在图4-34中,“pagewriter”线程作为全局脏页队列唯一的消费者,负责从脏页队列中批量获取待写入磁盘的脏页,在完成写入磁盘操作之后,“pagewriter”自身不负责检查点的推进,而只是推进整个脏页队列的队头到下一个待写入磁盘的槽位位置。
实际检查点的推进由“Checkpointer”线程来负责。这是因为“pagewriter” 线程的写入磁盘操作,只是将共享缓冲区中的脏页写入到文件系统的缓存中,(由于文件系统的I/O合并优化)此时可能并没有真正写入磁盘。因此,在“Checkpointer”线程中,其先获取当前全局脏页队列的队头位置,以及对应槽位中脏页的首次脏页lsn值,然后对截至目前所有被写入文件系统的文件进行fsync(刷盘)操作,保证文件系统将它们写入物理磁盘中。然后就可以将上述lsn值作为检查点位置更新到control文件中,用于数据库重启之后回放日志的起始位置。
上述这套持久化和检查点推进机制的主要控制信息,保存在knl_g_ckpt_context结构体中,该结构体定义代码如下:

uint64 dirty_page_queue_reclsn;
uint64 dirty_page_queue_tail;
CkptSortItem* CkptBufferIds;

/* 脏页队列相关成员 */
DirtyPageQueueSlot* dirty_page_queue;
uint64 dirty_page_queue_size;
pg_atomic_uint64 dirty_page_queue_head;
pg_atomic_uint32 actual_dirty_page_num;

/* pagewriter线程相关成员 */
PageWriterProcs page_writer_procs;
uint64 page_writer_actual_flush;
volatile uint64 page_writer_last_flush;

/* 全量检查点相关信息成员 */
volatile bool flush_all_dirty_page;
volatile uint64 full_ckpt_expected_flush_loc;
volatile uint64 full_ckpt_redo_ptr;
volatile uint32 current_page_writer_count;
volatile XLogRecPtr page_writer_xlog_flush_loc;
volatile LWLock *backend_wait_lock;

volatile bool page_writer_can_exit;
volatile bool ckpt_need_fast_flush;

/* 检查点刷页相关统计信息(除数据页面外) */
int64 ckpt_clog_flush_num;
int64 ckpt_csnlog_flush_num;
int64 ckpt_multixact_flush_num;
int64 ckpt_predicate_flush_num;
int64 ckpt_twophase_flush_num;
volatile XLogRecPtr ckpt_current_redo_point;

uint64 pad[TWO_UINT64_SLOT];
} knl_g_ckpt_context;

其中和当前上述检查点机制相关的成员有:
(1) dirty_page_queue_reclsn是脏页队列的lsn位置,dirty_page_queue_tail是脏页队列的队尾,这两个成员构成一个16字节的整体,通过128位的CAS操作进行整体原子读、写操作,保证脏页队列中每个成员记录的lsn一定随着入队顺序单调递增。
(2) CkptBufferIds是每批pagewriter待刷脏页数组。
(3) dirty_page_queue是全局脏页队列数组。
(4) dirty_page_queue_size是脏页数组长度,等于“g_instance.attr.attr_storage.NBuffers * PAGE_QUEUE_SLOT_MULTI_NBUFFERS”,当前PAGE_QUEUE_SLOT_MULTI_NBUFFERS取值5,以防止脏页队列因为DDL(data definition language,数据定义语言)等操作引入的空洞过多,导致脏页队列撑满阻塞业务的场景。
(5) dirty_page_queue_head是脏页队列头部。
(6) actual_dirty_page_num是脏页队列中实际的脏页数量。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论