暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

PG的检查点与检查点优化

白鳝的洞穴 2021-03-16
2647
检查点是数据库实例恢复机制的关键,其目的是为了确定某个时间点之前的脏数据已经全部存盘了。目前的通用型关系型数据库都是采用WAL日志的方式来确保提交的事务不会丢失的,而为了提高数据库的性能,减少不必要的IO,所有的数据修改都是在数据库缓冲中完成的。一个数据块被读取到数据库缓冲中之后,对它的修改都是在内存中完成。被修改过的数据块或者数据页被称为脏块(脏页)。这些脏块会被延后写盘,为什么要延后写盘呢?比如我们刚刚修改了一条记录,这个脏块就被写盘了,那么几秒钟后这个块里的某条数据又被修改了,此时还要再次存盘,那么前面那次存盘是不是有点亏呢?于是数据库总是尽可能地延后脏块写盘的时间,从而减少IO开销。
在缓冲区中修改数据的好处是避免高开销的IO,从而提升性能。不过没有任何事情是没有任何副作用的。因为数据的修改都在内存中,如果数据库突然宕了,那么没有写盘的脏块上已经修改的数据是不是会丢失呢?因为WAL机制的存中,提交后的数据是不会丢失的。因为如果数据库实例宕了,我们可以取出WAL文件里的数据,把丢失的修改操作再重新操作一遍,这个操作被称为重演(REDO),Oracle的REDO LOG就是因此而命名的。在PG数据库里,这个叫WAL (Write Ahead Log,意思是写操作执行前需要写入的日志,也就是说,所有的数据库写操作,必须先记录在WAL里,然后才去真正修改数据,从而确保写入的数据不会丢失)。数据库实例恢复的时候需要从那个点开始做重演呢?最土的办法就是找出所有的WAL,然后依次重演,不过这样效率太低了。于是就需要引入“检查点(CHEKPOINT)”。检查点实际上是一个时间戳,这个时间戳之前的所有脏块都已经写盘,而这个时间戳之后的数据不一定是否存储。基于这个原则,那么做实例恢复的时候,只需要重演检查点之后的WAL日志记录,之前的不需要考虑了。
Oracle数据库的ckpt进程实际上是一个监工,它的工作是查看DBWR写数据的进度,并定期修改控制文件中的相关数据,从而同步检查点信息。在PG中,检查点工作也是有专门的进程来完成的。

在PG中能够触发检查点的有四种情况:
  • 手动执行CHECKPOINT命令

  • 执行需要隐式的检查点的命令(例如pg_start_backup,CREATE DATABASE,或pg_ctl stop|restart和其他一些命令)

  • 自上一个检查点以来已达到配置的时间量(参数CHECKPOINT_TIMEOUT 设置)

  • 自上一个检查点以来已经新生成了某个限额的的WAL数量(MAX_WAL_SIZE,checkpoint_completion_target)

前两种情况我们不做讨论,都属于被动检查点的情况,后两种情况是在PG优化中经常会遇到的。checkpoint_timeout的缺省值是5分钟,也就是说上一次检查点发生后的5分钟后,如果检查点还没有因为其他原因触发过,那么系统就会自动启动新的检查点。这个检查点参数的设置对于大多数正常的生产系统来说还是太小了,一般来说建议这个参数设置为20分钟以上。有的DBA会把这个参数与RTO指标等同起来,实际上是不对的,因为检查点触发延时20分钟只是当数据库的脏数据写入量很小的情况下,最多20分钟会触发一次检查点,从而避免过多的脏数据没有写盘,或者实例恢复的时候需要读过多的WAL日志。和实际的RTO不是正比关系,不过较大的checkpoint_timeout可能会导致数据文件的脏数据写入的延时加大,从而影响RTO。
当检查点发生的时候,数据库需要完成几个工作,首先是扫描出数据库缓冲区中的所有脏页,然后把这些脏页都写入数据文件,然后执行fsync来确保操作系统缓冲区中的数据都已经真实的写盘。然后我们就可以标注当前数据库的检查点为最晚的脏块的最后修改时间。
上面是关于检查点的较为简洁与标准化的描述,不过上述的操作过于学院派,事实上,如果PG数据库是这么做的,那么在一个IO存在高峰瓶颈的系统中,这种CHECKPOINT机制就是一个灾难,因为IO有可能会被CHECKPOINT占满,从而影响当前的业务系统的运行。正是因为这个原因,从PG 8.3开始引入了spread checkpoint的机制。也就是说CHECKPOINT要写入的脏数据不是一次性完成写入,然后再FSYNC的,而是分批次的写入脏数据,从而避免CHECKPOINT对数据库性能造成致命影响。spread checkpoint把脏块的写入时间拖长,从而减少瞬间的IO量,而fsync操作也不是显式执行,让操作系统自动根据VM的参数定义的策略自动在后台执行,当CHECKPOINT写入最后一批数据的时候,才会显式执行fsync,确保最后一批数据被安全存盘。这种模式与OS的写缓存回写策略有关。比如在LINUX上,vm.dirty_expire_centisecs参数就定义了写缓存老化的延时时间,缺省是30(300cs)秒钟。对于脏数据较多的系统,这个参数可能会太大了,这会导致操作系统回写脏页产生的写IO突然变得十分巨大,从而影响数据库的性能。
下面是PG的checkpointer进程的堆栈:

其中BufferSync()是checkpointer刷新脏数据的核心调用,在这个调用里,checkpointer会扫描所有的buffer,将标识为BM_CHECKPOINT_NEEDED的数据块都找出来,然后对这些脏数据根据块的位置进行排序,从而合并IO,提升写性能。组织好checkpoint写队列后,调用SyncOneBuffer()写盘。如果写IO太大,checkpointer会通过调用CheckpointWriteDelay来调节IO,不至于因为IO过度消耗而导致系统性能问题。因此我们在跟踪checkpointer的时候,有时候会看到这样的堆栈:

这种情况往往是数据块的写IO量很大,checkpointer的写入量很大的时候才会出现。从源码上看,在这个函数里,主要做了一个pg_usleep:

上面讨论的关于CHECKPOINT的原理实际上在各种数据库中大同小异。下面是一些比较实用的干货,那么在一个IO负载较高,或者说写入量较大的系统中,我们该如何来优化CHECKPOINT呢?实际上用过ORACLE的DBA都很有体会,早期的ORACLE版本,比如ORACLE 7/8,当时调整CKPT相关的参数从而提升数据库的性能是DBA十分头痛也必须经常面临的问题,而随着Oracle 数据库的发展,Oracle 10g以后,DBA就很少为这件事头痛了,因为Oracle的CKPT变得更为智能了。
PG也是如此,不过PG还没有像Oracle那么智能,因此我们还需要通过调整几个参数来优化PG的CHECKPOINT。这几个参数是:MAX_WAL_SIZE,checkpoint_timeout和checkpoint_completion_target。
确定max_wal_size参数可以用一个比较简单的算法,我们评估一下大概每分钟产生WAL的大小,然后以30分钟作为checkpoint_timeout的值,计算max_wal_size的最小值,因为WAL的量达到max_wal_size的时候也会触发checkpoint,而实际上有可能系统中的WAL量还没达到wal_max_size就会触发checkpoint,这是因为checkpoint_completion_target参数的存中,当WAL达到了wal_max_size*checkpoint_completion_target的时候,就会触发checkpoint,因此我们还需要考虑这个参数的设置。checkpoint_completion_target的缺省值是0.5,这时候就触发checkpoint有点太早了,会导致wal文件空间的浪费。因此需要加大这个参数的设置。那么设置这个参数为多少合适呢?我们做一个假设,比如checkpoint_timeout是30分钟,那么我们希望在30分钟到达之前2分钟就达到这次checkpoint的触发条件,这样算下来,checkpoint_completion_target的值大约是0.93,舍弃小数就是0.9,这也是大多数关于CHECKPOINT优化的文章里建议这个参数设置为0.9的主要原因。
在一个写负载较高的系统中,根据上面的初始方法去设置这三个参数对于性能优化十分关键。不过我们也不能固化思维这个问题,调整后我们还需要进一步观察系统的实际运行效果,因为每个数据库的应用负载都会不同,不可能有万能的公式来适应千变万化的实际情况。如果我们发现IO的性能和负载过高,那么我们可能还需要通过加大max_wal_size和checkpoint_timeout来缓解IO压力。这种调整后,你的系统才算真正的调整好了。
可能会有一些朋友在自己的生产环境或者测试环境中调整上面所说的参数,不过并没有获得明显的性能提升。实际上确实存在这种情况。在存储性能是严重制约系统性能的时候,检查点的优化会起到立竿见影的效果,而在一般的情况下,只要checkpoint的相关设置没有过分的不合理,那么对数据库的整体性能的影响并没有以前这么明显了。这也是二十年前的DBA还经常要去调整CHECKPOINT以解决IO瓶颈问题,现在的DBA仅仅知道CHECKPOINT而已。对于Oracle如此,对于PG也是如此。
文章转载自白鳝的洞穴,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论