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

深入搞懂Checkpoint调优基础及原理

数据库杂记 2024-07-24
510

前言

在执行大量写操作的系统上,调优检查点对于获得良好的性能至关重要。然而,检查点是我们经常发现混淆和配置问题的地方之一,无论是在社区邮件列表中,还是在为客户提供支持和咨询期间。这篇文章旨在解释检查点是什么——目的和数据库如何实现它——以及如何调优它们。

注:这是在2016年最初原作者写的一篇博文的更新版本,更新后反映了PostgreSQL配置的各种变化。否则,总体调优方法基本保持不变。

检查点的意义是什么?

PostgreSQL是依赖于预写日志(write-ahead log, WAL)的数据库之一——在对数据文件进行任何更改之前,它被记录在一个单独的日志(一个更改流)中。提供了持久性,因为在发生崩溃的情况下,数据库可以使用WAL执行恢复—从WAL读取更改并将其重新应用到数据文件中。

乍一看,这似乎效率不高,因为它使写入量增加了一倍——我们在更改数据文件的同时也将更改写入日志。但它实际上可能会提高性能,原因有几个。COMMIT只需要等待WAL持久(写入并刷新到磁盘),而数据文件只在内存(共享缓冲区)中修改,然后可能在稍后写入磁盘/刷新。这很好,因为WAL是以顺序的方式写入的,而对数据文件的写入通常是相当随机的,因此刷新WAL要便宜得多。此外,在共享缓冲区中,数据页可以被修改多次,然后在单个物理I/O写入中持久化——这是另一个显著的好处。

假设系统崩溃了,数据库需要执行恢复。最简单的方法是从头开始——从一个新的实例开始,从一开始就应用所有的WAL。最后,我们应该得到一个完整的(和正确的)数据库。当然,明显的缺点是需要保存和重放自创建实例以来的所有WAL。我们经常处理的数据库不是很大(比如几百GB),但非常活跃,每天产生几TB的WAL。想象一下,在运行数据库一年的时间里,需要多少磁盘空间来保存所有的WAL,以及恢复需要多长时间。显然,这似乎不是一个非常实用的方法。

但是,如果数据库能够保证对于给定的WAL位置(日志中的偏移量,称为“日志序列号”—LSN),到该位置的所有数据文件更改都被刷新到磁盘上,那会怎么样呢? 然后,它可以在恢复期间确定此位置,并仅重播WAL的其余部分(从此位置开始)。这将大大减少恢复时间,并且还可以在“已知的刚刚好的”位置之前丢弃WAL,因为恢复不需要它。

这正是检查点的魅力所在——确保恢复不再需要某些LSN之前的WAL,从而减少磁盘空间需求和恢复时间。数据库只是查看当前的WAL位置,并将所有未完成的更改(可能需要旧的WAL)刷新到磁盘,并记录LSN以备需要恢复时使用。

这些检查点由数据库定期生成,根据时间或生成的WAL(从上一个检查点开始)。

注:如果你是一名玩家,你可能对检查点的概念很熟悉——你的角色在游戏中通过一个特定的点,如果你没能打败下一关或掉进一个熔岩湖,你就从最后一个点开始,而不是从头开始。让我们看看如何在PostgreSQL中实现这一点;-)

稍后我们将讨论影响检查点发生频率的配置部分,但让我们简要讨论两种极端配置。

我们已经描述了一种极端情况——检查点根本不发生,或者很少发生。这样可以最大限度地提高一些好处(将数据文件更改合并为单个异步写操作,并减少对数据文件进行写操作的需要),但它也有缺点,即必须保留所有的WAL,并在崩溃/不干净关机后执行冗长的恢复。

我们还来讨论另一种极端情况——**执行非常频繁的检查点(比如,每秒钟左右)。**这似乎是一个好主意,因为它只允许保留少量的WAL,并且恢复也将非常快(只需要重播少量的WAL)。但它也会将对数据文件的异步写入转变为同步写入,并使多个更改合并为单个物理写入的可能性大大降低。这将严重影响用户(例如,增加COMMIT延迟,降低吞吐量)。

因此,在实践中,这是一种权衡——我们希望检查点发生的频率足够低,以免影响用户,但也要足够频繁,以限制恢复持续时间和磁盘空间需求。

触发检查点

触发检查点的方式有三到四种:

1. 直接执行CHECKPOINT命令
2. 执行一个需要检查点的命令(如pg_backup_start, CREATE DATABASE,或pg_ctl stop|restart等)
3. 从最后一个检查点开始达到配置的时间(checkpoint_timeout)
4. 自上一个检查点以来生成配置数量的WAL (max_wal_size)

前两点与本文无关——这些都是人工触发的罕见事件,主要与维护操作相关。这篇文章是关于如何配置另外两种情况,影响定期检查点

这些时间/大小限制使用两个配置选项设置:

checkpoint_timeout = 5min
max_wal_size = 1GB (新选项:PostgreSQL 9.5开始引入)

使用这些(默认)值,PostgreSQL将每5分钟触发一次CHECKPOINT,或者在写入大约1/ 2GB的WAL之后,以先发生的为准。

注意:max_wal_size是总WAL大小的软限制,它有两个主要后果。首先,数据库将尝试不超过限制,但允许这样做,因此在分区上保留足够的可用空间并对其进行监视。其次,它是对WAL总量的限制,而不是“每个检查点”——由于扩展检查点(稍后解释),配额涵盖1-2个检查点(PG 11之前为2-3个)。因此,当max_wal_size = 1GB时,数据库将在写入500 - 1000 MB的WAL后启动一个CHECKPOINT,具体取决于checkpoint_completion_target。

默认值相当保守(即低),就像样例配置文件中的许多其他默认值一样,即使在像Raspberry Pi这样的小型系统上也可以工作。您可能需要大幅增加限制以获得最多可用硬件。

但是如何确定适合您的系统/应用程序的值呢?如前所述,我们的目标是不要太频繁也不要太不频繁地执行检查点。我们的调优“最佳实践”方法包括两个步骤:

1. 选择一个“合理的”checkpoint_timeout值
2. 将max_wal_size设置为很少达到的高值

checkpoint_timeout的“合理”值是多少? 显然,自最后一个检查点以来我们积累的数据越多,我们在恢复期间需要做的工作就越多,所以这与恢复时间目标(RTO)有关,即我们希望恢复完成的速度有多快(在崩溃之后)。

注意:如果您担心在崩溃后恢复需要很长时间,因为系统需要满足一些高可用性/ SLA需求,那么您可能应该考虑设置副本,而不是仅仅依赖检查点。系统可能需要长时间的重新启动、更换硬件部件等等。较短的检查点无法解决这些问题,而故障转移到副本是一种经过验证的解决方案。

这有点棘手,因为checkpoint_timeout是生成WAL所需时间的限制,而不是恢复时间的限制。然而,虽然WAL通常是由多个进程生成的(后端运行DML),但恢复是由单个进程执行的——因此仅限于单个CPU,可能会在I/O上停滞,等等。这不仅影响本地恢复,还影响流复制,其中副本可能无法跟上主节点。恢复可能有冷缓存(例如,在重新启动之后),由于昂贵的I/O,使得单进程特别地慢。

注意: PostgreSQL 15引入了recovery_prefetch选项,它允许在恢复期间异步预取数据,解决了这个“单进程恢复”的弱点,并且可能使恢复比生成数据的原始工作负载更快。

此外,实例可能带有非常不同的工作负载------例如,在同一时间段内,处理写偏繁重的工作负载的实例将比只读实例生成更多的WAL。

应该很清楚,更长的checkpoint_timeout意味着更多的WAL,因此更长的恢复时间,但是仍然不清楚什么是“合理的”超时值。不幸的是,没有简单的公式可以告诉你什么是“最优”值。

然而,默认值(5分钟)显然太低了,生产系统通常使用30分钟到1小时之间的值。PostgreSQL 9.6甚至将最长时间增加到1天(所以显然有资深黑客认为这对某些系统可能是一个好主意)。较低的值还可能导致写放大,这是由于整个页面的写(这是一个严重的问题,但为了简短起见,我不打算在这里讨论它)。

checkpoint_timeout参数是“收益递减”的一个很好的例子——增加该值的好处很快就会消失。例如,将超时从5分钟加倍到10分钟可以产生明显的改进。从10分钟再增加一倍到20分钟可能会再次改善,但改善可能会小得多(而成本 - WAL量和恢复时间 - 仍然翻倍)。然后在20到40分钟,成本仍然翻倍,但相对的好处还是不那么显著。该由你来决定如何取舍。

假设我们决定使用30分钟,根据我们的经验,这是一个合理的值——不太低,也不太高。

checkpoint_timeout = 30min

从现在起,这就是我们的目标,我们希望平均每半小时设立一个检查点。但是,如果我们这样做,数据库可能仍然会更频繁地触发检查点,因为达到了检查点之间的WAL数量限制——max_wal_size。我们不希望那样——我们的目标是检查点仅由checkpoint_timeout触发。

这意味着我们需要估计数据库在30分钟内产生多少WAL,以便我们可以将其用于max_wal_size。有几种方法可以计算这个估算值:

  • 使用pg_current_wal_lsn()查看实际的WAL位置(基本上是文件中的偏移量),并计算30分钟后测量的位置之间的差异。

注意:在pg_current_xlog_location()之前,这个函数一直被称为pg_current_xlog_location()。

  • 启用log_checkpoints=on,然后从服务器日志中提取信息(每个完成的检查点都有详细的统计信息,包括WAL的数量)。
  • 使用pg_stat_bgwriter,它包含有关检查点数量的信息(您可以将其与当前max_wal_size值的知识结合使用)。

例如,让我们使用第一种方法。在我运行pgbench的测试机器上,我看到如下:

test=# select pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
C7/72C140D8
(1 row)

... after 5 minutes ...

test=# select pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
C7/E8A494CF
(1 row)

test=# SELECT pg_wal_lsn_diff('C7/E8A494CF', 'C7/72C140D8');
pg_wal_lsn_diff
-----------------
1977832439
(1 row)

这表明,在5分钟内,数据库生成了约1.8GB的WAL,因此对于checkpoint_timeout = 30min,大约是6 * 1.8GB = 11GB的WAL。但是,正如前面提到的,max_wal_size是1 - 2个检查点的配额,我们需要使用22GB。

注意:max_wal_size覆盖的检查点数量取决于检查点目标值,这将在后面关于扩展检查点的部分中讨论。可以肯定的是,更高的价值很重要。此外,数量过去是2 - 3个检查点,直到PG 11

其他方法收集数据的方式不同,但总体思路是相同的,结果应该具有可比性。

只作预估

这些测量本质上是不准确的,因为检查点的频率会影响我们需要写的全页写次数,而全页写可能是WAL写放大的主要来源

检查点之后对每个数据页(数据文件中8kB的数据块)的第一次更改触发全页写,即将完整的8kB页写入WAL。如果你改变一个页面两次(例如,因为从不同的事务做两个更新),这可能会导致一个或两个完整的页面写,这取决于它们之间是否发生了检查点。这意味着减少检查点的频率可以消除一些整页的写操作,从而减少WAL的写入量。因此,当降低检查点的频率后产生的WAL数量明显低于更频繁的检查点产生的WAL总数时,不要感到惊讶。

例如,假设检查点每10分钟发生一次,每个检查点写入10GB的WAL。如果您希望检查点每30分钟发生一次,您可以设置

max_wal_size = 60GB

覆盖2个检查点,每个30GB,这样就可以了。但是,如果您测量每个检查点生成的WAL的数量,您可能会发现平均只有~15GB的WAL(例如),而不是预期的30GB。这可能是由于更少的整页写入,最终这是一件好事——更少的整页写入、更低的WAL放大、更少的要归档和/或复制的WAL,等等。

这意味着较少的检查点可以显著减少每个备份策略需要保留的WAL量。通常需要保留足够的备份和WAL来执行上个月的PITR,也就是说,您需要保留过去30天左右的WAL。如果降低检查点的频率可以减少整页写入的次数,那么WAL归档的大小也会减少。

扩展检查点

当我建议您只需要调优checkpoint_timeout和max_wal_size时,我并没有说出全部真相。这两个参数当然是两个重要的参数,但还有第三个参数,称为checkpoint_completion_target。

您可能不需要调优它—默认值相当合理,如果您调优了前两个参数,就可以了。但是最好理解什么是“扩展检查点”,以便在需要时对其进行调优。

在CHECKPOINT期间,数据库需要执行以下三个基本步骤:

1. 识别共享缓冲区中所有脏的(修改的)块
2. 将所有这些缓冲区写入磁盘(或者更确切地说写入文件系统缓存)
3. Fsync()将所有修改过的文件同步到磁盘

只有当所有这些步骤完成时,检查点才能被认为是完成的——最终,是最后的“fsync”步骤使事情完全持久。

你可以“尽可能快地”完成这些步骤,例如,一次性写完所有脏缓冲区,然后在所有受影响的文件上调用fsync(),事实上,这正是PostgreSQL在8.2版本之前所做的。但是,由于填充文件系统缓存、使设备饱和以及影响用户会话,这会导致严重的I/O停滞。假设您在共享缓冲区中有8GB的修改数据,您将所有这些数据一次性写入操作系统并调用fsync。内核将尽其所能尽快满足您的请求,但这意味着其他进程将不得不等待更长时间的I/O。这是从后台启动的,但对用户会话的影响是巨大的。

为了解决这个问题,PostgreSQL 8.3引入了“扩展检查点”的概念——不是一次写入所有数据,而是在很长一段时间内进行写操作。这让操作系统有时间在后台清理脏数据,使最终的fsync()更便宜,并减少对用户会话的影响

写操作是根据到下一个检查点的进度来进行限制的——数据库知道在需要另一个检查点之前我们还剩下多少时间/ WAL,因此它可以计算出在给定的时间点应该写多少脏缓冲区(到操作系统)。

然而,数据库必须在最后一刻才发出写操作——这意味着最后一批写操作仍然在文件系统缓存中,使得最后的fsync()调用(在开始下一个检查点之前发出)再次变得昂贵。可能不会像以前那么昂贵(因为脏数据的数量会更少),但如果我们能防止这种情况……

checkpoint_completion_target = 0.9(在PG 14之前一直是0.5) 表示所有写操作应该在下一个检查点完成的距离。例如,假设检查点每30分钟触发一次,数据库将限制写操作,以便最后一次写操作在30 * 0.9 = 27分钟后完成。这意味着操作系统有另外3分钟的时间将数据刷新到磁盘,这样在检查点结束时发出的fsync调用就既便宜又快速。

在PostgreSQL 9.6之前,我们需要担心内核从页缓存中清除脏数据的速度有多快,这是由vm.dirty_expire_centisecs(默认设置为30秒)和vm.dirty_background_bytes(开始写数据时的脏数据量)共同决定的。在计算我们应该在检查点结束时为操作系统留出多少时间来写数据时,需要考虑到这一点。

然而,PostgreSQL 9.6引入了一个选项checkpoint_flush_after,使得这些内核参数的调优基本没有必要——该选项指示数据库在写入少量数据(默认为256kB)之后,在CHECKPOINT期间定期进行fsync。这大大减少了fsync调用对其他进程的影响(没有严重的I/O延迟)。这也意味着我们不需要太担心检查点结束时剩下的时间,因为未刷新的数据量很小。

总结

现在您应该了解了检查点的目的,以及调优检查点的基本知识。总结一下:

1) 大多数检查点应该由time (checkpoint_timeout)触发。
2) 内部是吞吐量(不频繁的检查点)和恢复所需时间(频繁的检查点)之间的折衷。
3) 大多数生产系统使用的超时时间在30-60分钟之间,请在这个范围内选择一个值,除非您有数据支持不同的选择
4) 在确定超时后,通过估计WAL的数量来选择max_wal_size
5) 将checkpoint_completion_target设置为0.9(如果是旧版本,则默认为0.5)
6) 在没有checkpoint_flush_after的旧版本(9.6之前)上,您可能需要调优内核(vm)。Dirty_expire_centisecs和vm.dirty_background_bytes)

参考原文:

https://www.enterprisedb.com/blog/basics-tuning-checkpoints

我是【Sean】,  欢迎大家长按关注并加星公众号:数据库杂记。有好资源相送,同时为你提供及时更新。已关注的朋友,发送0、1到7,都有好资源相送。

往期导读: 
1. PostgreSQL中配置单双向SSL连接详解
2. 提升PSQL使用技巧:PostgreSQL中PSQL使用技巧汇集(1)
3. 提升PSQL使用技巧:PostgreSQL中PSQL使用技巧汇集(2)
4. PostgreSQL SQL的基础使用及技巧
5. PostgreSQL开发技术基础:过程与函数
6. PostgreSQL中vacuum 物理文件truncate发生的条件
7. PostgreSQL中表的年龄与Vacuum的实验探索:Vacuum有大用
8. PostgreSQL利用分区表来弥补AutoVacuum的不足
9. 也聊聊PostgreSQL中的空间膨胀与AutoVacuum
10. 正确理解SAP BTP中hyperscaler PG中的IOPS (AWS篇)

文章转载自数据库杂记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论