检查点
在PostgreSQL中的检查点叫全量检查点,执行时会将buffer中的所有的脏页刷到磁盘,需要在一定时间内完成刷脏页的操作,导致数据库运行性能波动较大。同时全量检查点开始时需要遍寻内存中的所有脏页,内存越大,寻找脏页的时间也越长,具体过程如下:
遍历所有BUFFER,将当前时刻的所有脏块状态改为CHECKPOINT NEEDED,来表示需要将这些脏块写出磁盘。这一步是在内存中完成的不涉及磁盘操作。
刷物理文件,从缓存中将脏块fsync到磁盘。这一步涉及磁盘操作。将标记为CHECKPOINT NEEDED的block写出到磁盘。
Checkpoint本身也会被记录到XLOG中,检查点会被信息刷出到xlog中。
更新控制文件中的检查点信息到当前位置。
这样做总体看上去没什么问题,但是在Buffer容量设置的很大的时候,那么寻找脏块的时间就会很长,并且刷入磁盘的时间也会很长。
对此openGauss新增了增量检查点功能,默认每分钟执行一次。openGauss增量检查点会小批量的分阶段的进行脏页刷盘,同时更新lsn信息,回收不需要的xlog日志。在增量检查点的机制下,会维护一个按照LSN顺序递增排列的脏页面队列,无需每次遍历内存中所有的脏页。
node_name | text | 数据库进程名称。 |
ckpt_redo_point | test | 当前实例的检查点。 |
ckpt_clog_flush_num | bigint | 从启动到当前时间clog刷盘页面数。 |
ckpt_csnlog_flush_num | bigint | 从启动到当前时间csnlog刷盘页面数。 |
ckpt_multixact_flush_num | bigint | 从启动到当前时间multixact刷盘页面数。 |
ckpt_predicate_flush_num | bigint | 从启动到当前时间predicate刷盘页面数。 |
ckpt_twophase_flush_num | bigint | 从启动到当前时间twophase刷盘页面数。 |
MogDB=# select * from dbe_perf.GLOBAL_CKPT_STATUS;
-[ RECORD 1 ]------------+-----------
node_name | dn_6001 --数据库进程名称
ckpt_redo_point | 0/B6818810 --当前实例的检查点
ckpt_clog_flush_num | 20 --从启动到当前时间clog刷盘页面数
ckpt_csnlog_flush_num | 41 --从启动到当前时间csnlog刷盘页面数
ckpt_multixact_flush_num | 1 --从启动到当前时间multixact刷盘页面数
ckpt_predicate_flush_num | 0 --从启动到当前时间predicate刷盘页面数
ckpt_twophase_flush_num | 0 --从启动到当前时间twophase刷盘页面数
node_name | text | 数据库进程名称。 |
pgwr_actual_flush_total_num | bigint | 从启动到当前时间总计刷脏页数量。 |
pgwr_last_flush_num | integer | 上一批刷脏页数量。 |
remain_dirty_page_num | bigint | 当前预计还剩余多少脏页。 |
queue_head_page_rec_lsn | text | 当前实例的脏页队列第一个脏页的recovery_lsn。 |
queue_rec_lsn | text | 当前实例的脏页队列的recovery_lsn。 |
current_xlog_insert_lsn | text | 当前实例XLog写入的位置。 |
ckpt_redo_point | text | 当前实例的检查点。 |
MogDB=# select * from dbe_perf.GLOBAL_PAGEWRITER_STATUS;
-[ RECORD 1 ]---------------+-----------
node_name | dn_6001 --数据库进程名称
pgwr_actual_flush_total_num | 31265 --从启动到当前时间总计刷脏页数量
pgwr_last_flush_num | 2 --上一批刷脏页数量
remain_dirty_page_num | 0 --当前预计还剩余多少脏页
queue_head_page_rec_lsn | 0/0 --当前实例的脏页队列第一个脏页的recovery_lsn
queue_rec_lsn | 0/B6819410 --当前实例的脏页队列的recovery_lsn
current_xlog_insert_lsn | 0/B6819410 --当前实例XLog写入的位置
ckpt_redo_point | 0/B6818810 --当前实例的检查点
双写
名称 | 类型 | 描述 |
node_name | text | 节点名称。 |
curr_dwn | bigint | 当前双写文件的序列号。 |
curr_start_page | bigint | 当前双写文件恢复起始页面。 |
file_trunc_num | bigint | 当前双写文件复用的次数。 |
file_reset_num | bigint | 当前双写文件写满后发生重置的次数。 |
total_writes | bigint | 当前双写文件总的I/O次数。 |
low_threshold_writes | bigint | 低效率写双写文件的I/O次数(一次I/O刷页数量少于16页面)。 |
high_threshold_writes | bigint | 高效率写双写文件的I/O次数(一次I/O刷页数量多于一批,421个页面)。 |
total_pages | bigint | 当前刷页到双写文件区的总的页面个数。 |
low_threshold_pages | bigint | 低效率刷页的页面个数。 |
high_threshold_pages | bigint | 高效率刷页的页面个数。 |
file_id | bigint | 当前双写文件的id号。 |
MogDB=# select * from DBE_PERF.GLOBAL_DOUBLE_WRITE_STATUS;
-[ RECORD 1 ]---------+--------
node_name | dn_6001 -- 节点名称
curr_dwn | 16 -- 当前双写文件的序列号
curr_start_page | 20847 -- 当前双写文件恢复起始页面
file_trunc_num | 563 -- 当前双写文件复用的次数
file_reset_num | 1 -- 当前双写文件写满后发生重置的次数
total_writes | 9004 -- 当前双写文件总的I/O次数
low_threshold_writes | 8778 -- 低效率写双写文件的I/O次数(一次I/O刷页数量少于16页面)
high_threshold_writes | 3 -- 高效率写双写文件的I/O次数(一次I/O刷页数量多于一批,421个页面)。
total_pages | 47088 -- 当前刷页到双写文件区的总的页面个数
low_threshold_pages | 33279 -- 低效率刷页的页面个数
high_threshold_pages | 1802 -- 高效率刷页的页面个数
file_id | 0 -- 当前双写文件的id号
操作系统数据块是4k,数据块一般是8k/16k/32k(openGauss是8k,MySQL是16k),这样有可能造成页面断裂问题,即一个数据库数据块刷到操作系统的过程中可能发生宕机造成数据块损坏导致数据库无法启动。
当数据库正在从内存向磁盘写一个数据页时,数据库宕机,从而导致这个页只写了部分数据,这就是部分写失效,导致数据丢失。这时即使是redo log也无法恢复,因为redo log记录的是对页的物理修改,如果页本身已经损坏,redo log也无能为力。
对于这个问题,PostgreSQL采用的方式是full page write,数据页第一次发生变更时将整个页面记录至xlog日志中,这样即使出错也具备完整的数据页和xlog日志进行数据恢复。但是这样大大增加了xlog的日志量,对性能有一定的影响。
openGauss采用的方式是double write,这类似与MySQL,写数据块的同时将脏页写到一个共享的双写空间中,如果发生问题就从双写空间中找到完整的数据页进行恢复。(备注:双写特性需要配合增量检查点一起使用)
数据页写线程在openGauss数据库中应该包含两个线程:PageWriter和BgWriter。
操作系统数据块大小一般是4k,数据库一般是8k/16k/32k,openGauss默认是8kb,这样就有可能造成页面断裂问题,一个数据库数据块刷到操作系统的过程中可能发生因宕机而造成块损坏从而导致数据库无法启动的问题。pagewriter线程负责将脏页数据拷贝至双写(double-writer)区域并落盘,然后将脏页转发给bgwriter子线程进行数据下盘操作,这样可以防止该现象的发生,因为如果发生数据页"折断"的问题,就会从双写空间里找到完整的数据页进行恢复。
bgwriter线程(BgWriter)主要负责对共享缓冲区的脏页数据进行下盘操作,目的是让数据库线程在进行用户查询时可以很少或者几乎不等待写动作的发生(写动作由后端写线程完成)。这样的机制同样也减少了检查点造成的性能下降。后端写线程将持续的把脏页面刷新到磁盘上,所以在检查点到来的时候,只有少量页面需要刷新到磁盘上。但是这样还是增加了I/O的总净负荷,因为以前的检查点间隔里,一个重复弄脏的页面可能只会冲刷一次,而现在一个检查点间隔内,后端写进程可能会写好几次。但在大多数情况下,连续的低负荷要比周期性的尖峰负荷好一些,毕竟数据库稳定十分重要。
如果bgwriter的刷盘操作不能保证数据库拥有充足的可用共享缓冲区,那么常规的后端线程仍然有权发出刷盘操作。
bgwriter线程也是随着Postmaster线程启动而启动,我们在执行recovery操作的时候也会启动该线程。Postmaster线程可以调用SIGTERM正常关闭,也可以调用SIGQUIT强制关闭。如果bgwriter线程意外崩溃,则Postmaster线程会认为整个openGauss后端线程崩溃,此时将调用SIGQUIT强制关闭所有后端线程,重置共享内存以恢复后端线程。
- PageWriter:
- 主要负责将脏页(即已被修改但尚未写入磁盘的数据页)从内存缓冲区中批量写入到双写(double-writer)区域,并确保这些脏页最终落盘。
- 它的工作涉及推进数据库的检查点(checkpoint),这是确保数据库在发生故障时能够恢复到一致状态的关键机制。
- PageWriter确保脏页在写入文件系统之前,先写入双写文件,以防止页面断裂(即数据库宕机时,页面只有部分写入磁盘导致的不一致情况)。
- BgWriter(Background Writer):
- 主要负责对共享缓冲区的脏页数据进行下盘操作(即将脏页写入磁盘)。
- 它通过定时刷新一些脏页面来减少检查点时的I/O负载,使系统的I/O负载更加平稳。
- BgWriter的写入操作是持续的,旨在确保数据库线程在进行用户查询时可以很少或几乎不等待写动作的发生。
- BgWriter使用一定的算法(如Clock Sweep算法)来选择要刷新的脏页。
- 它定期执行写入操作,但每次写入的脏页数量可能较少,旨在减少一次性I/O操作对系统性能的影响。
- BgWriter的写入操作与PageWriter相比可能更加频繁,但每次的写入量较小。