PG中调整checkpoint相关参数的基本原则【原创/翻译】

原创翻译文章,转载须注明出处。访问我的Github(地址:https://guobo507.github.io)查看最新文章列表。

原文链接:https://www.2ndquadrant.com/en/blog/basics-of-tuning-checkpoints/。译者水平有限,不当之处请多包涵。

在具有一定规模的写入(non-trivial number of writes)的系统上,合适的检查点参数设置对于获得良好的系统性能至关重要。然而,检查点是我们经常在社区邮件列表,以及在为客户进行性能调整审查时,发现设置混乱和配置有问题的区域之一(另一个区域是不久前Citus公司的Joe Nelson曾经讨论过的autovacuum)。因此,我将引导您理解和完成检查点的设置,包括检查点的作用以及如何在PostgreSQL中进行合适的调整。

What is the point of checkpoints?

PostgreSQL是依赖预写日志(WAL)的数据库之一,所有的更改都要首先写入WAL日志(关于数据库更改的流式信息),然后才写入到数据文件中。这样可以提供数据的持久性,因为在发生崩溃的情况下,数据库可以使用WAL日志执行恢复,即从WAL日志中读取数据库的更改,然后将其重新应用到数据库中。

虽然这可能使实际的写入量增加一倍,但是可以提高数据库整体的性能。用户只需要等待WAL日志刷新到磁盘,而数据文件页仅在内存中被修改,然后稍后在后台刷新到磁盘。这样做很值得,因为对WAL日志本质上是顺序写入的,而对数据文件的写入通常是随机的(因为缓存在内存中的数据页是随机的)。

假设系统崩溃了,数据库需要执行恢复。最简单的方法是从头开始,并从头开始重放所有的WAL日志。最后,我们就能获得完整的(正确的)数据库。这样做有一个明显的缺点,需要保留并在恢复时重放所有的WAL日志。我们经常处理规模不大的数据库(例如几百GB),但是每天会产生几TB的WAL日志(变更多)。因此,想象一下当数据库运行了一年以后,需要多少磁盘空间来保存所有WAL日志,以及在恢复过程中重放所有WAL日志需要多少时间。

但是,如果数据库可以保证给定WAL位置(日志中的偏移量)的所有更改,以及直到该位置的所有数据文件的更改都已经刷新到磁盘。那么,数据库在恢复时就可以确定需要恢复的起始位置,并仅仅重放WAL日志的其余部分即可,从而将大大减少恢复的时间。而且数据库还可以删除该“已知良好(known good)”位置之前的WAL文件。

这正是检查点的目的:确保在执行数据库恢复时不再需要某个时间点之前的WAL日志,从而减少了磁盘空间需求和恢复的时间。

注意:如果您碰巧是一名游戏玩家,则您可能更加熟悉检查点的概念。如您的角色在游戏中通过了某个点,并且如果您没能击败下一个BOSS或掉进了熔岩中;则您只需要从您已经通过的最后一关重新开始即可,而不是从游戏的开头重新开始。让我们看看在PostgreSQL中是如何实现这一点的。

我们再讨论另一个比较极端的例子:非常频繁地执行检查点操作(例如,每秒进行一次)。这杨的结果是将只保留非常少量的WAL日志,恢复速度也非常快(只需重放很少量的WAL日志)。但这也会把对数据文件的异步写入转换为类似同步写入(频率高),从而严重影响用户体验(例如,增加COMMIT的延迟,降低系统的吞吐量)。

因此,在实践中,您可能希望检查点不经常发生而不影响用户,但是也要足够频繁,以合理限制恢复时间和对磁盘空间的需求。

Triggering checkpoints

可以触发检查点的原因大约有三四种:

  • 直接执行CHECKPOINT命令;
  • 执行需要的检查点的命令(例如:pg_start_backupCREATE DATABASE,或pg_ctl stop|restart和其他一些命令);
  • 自上一个检查点以来已达到配置的时间;
  • 自上一个检查点以来生成了已配置的最大WAL文件数量(也称为WAL用尽(running out of WAL)或填充WAL(filling WAL))。

前两条在数据库日常运行中是无关紧要的,很少见的,因为都是必要时才会手动触发的事件。这篇文章是关于如何配置另外两个数据库自动执行的检查点事件的,这些事件会影响定期执行的检查点。

另外两条基于时间(time)/大小(size)的限制通过如下两个配置参数来实现:

  • checkpoint_timeout = 5min
  • max_wal_size = 1GB (before PostgreSQL 9.5 this was checkpoint_segments)

使用上面的(默认)值,PostgreSQL将在每5分钟,或者在磁盘上的WAL日志文件总量增长到大约1GB之后触发CHECKPOINT操作。

注意max_wal_size是总WAL日志文件的大小,它是一个软限制(soft limit),有两个意义。首先,数据库将尝试不超过该限制,但超过它是可以的,因此请在分区文件系统上配置足够的可用空间并对其进行监控。其次,这并不是针对“每个检查点”的限制,由于检查点是分散的(稍后说明),WAL日志的配额会在2-3个检查点之间分配。因此,数据库通常在写入了300–500MB的WAL文件之后自动启动CHECKPOINT操作,具体还要依赖于您的checkpoint_completion_target设置。

与模版配置文件中的大多数其他参数的默认值一样,默认值也很低,其大小设置可以在Raspberry Pi等小型系统上良好的工作。

但是,如何来确定您的系统中的一个合适的参数值呢?我们的目标是不要太频繁也不要很少执行检查点操作。关于这两个参数的设置的“最佳实践”包括下面的两个步骤:

  • 设置一个合理的checkpoint_timeout值;
  • max_wal_size设置得足够高以至于很少能达到。

实际上很难说一个“合理”的checkpoint_timeout值是多少,因为它取决于恢复时间目标(RTO),即可接受的最大恢复持续时间是多少。

这有点棘手,因为checkpoint_timeout它限制了生成WAL日志所花费的时间,而不是直接影响到恢复的时间。由于多种原因,我们无法确切地说出恢复需要话费多长时间。WAL日志通常是由多个进程(运行DML)生成的,而恢复时则是由单个进程执行的(此限制主要是固有的,不太可能很快改变)。这不仅影响本地恢复,还会影响到具有流复制的备用数据库的故障转移恢复。当然,本地恢复通常是在重新启动后立即进行的,此时文件系统缓存很冷(cold)。

通常,默认值(5分钟)比较低,设置30分钟到1小时之间的值是比较常见的。PostgreSQL 9.6甚至将最长期限增加到1天(因此,一些黑客认为这对于某些用例是个好主意)。较低的值也可能由于全页写入而导致写入放大(在此不做讨论)。

假设我们决定使用30分钟:

checkpoint_timeout = 30min

现在我们需要评估在30分钟内数据库将产生多少WAL日志文件,以便我们可以将其用于max_wal_size设置。有几种方法可以确定生成多少WAL日志文件:

  • 使用pg_current_xlog_insert_location()方法查看实际的WAL位置变化(基本上是文件中的偏移)来计算每30分钟测量的位置之间的差异。
  • 启用log_checkpoints = on,然后从服务器日志中提取信息(每个完成的检查点都会有详细的统计信息,包括WAL的数量)。
  • 使用来自pg_stat_bgwriter视图的数据,该数据还包括有关检查点数量的信息(您可以将其与当前·max_wal_size·值的知识相结合)。

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

postgres=# SELECT pg_current_xlog_insert_location();
pg_current_xlog_insert_location 
---------------------------------
3D/B4020A58
(1 row)

... After 5 minutes(五分钟后) ...

postgres=# SELECT pg_current_xlog_insert_location();
pg_current_xlog_insert_location 
---------------------------------
3E/2203E0F8
(1 row)

postgres=# SELECT pg_xlog_location_diff('3E/2203E0F8', '3D/B4020A58');
pg_xlog_location_diff 
-----------------------
            1845614240
(1 row)

这表明在5分钟内,数据库生成了约1.8GB的WAL日志,因此checkpoint_timeout = 30min大约会生成10GB的WAL日志文件。但是,如前所述,max_wal_size配额是2–3个检查点的总和,因此max_wal_size = 30GB(3 x 10GB)似乎是比较合理的。

其他方法使用不同的数据源进行考量和计算,但是最终想法是大致相同的。

Spread checkpoints

我建议您调整checkpoint_timeoutmax_wal_size,但是我并没有说明全部的真相。还有另一个参数,称为checkpoint_completion_target,但是要调整它,您需要了解“扩展检查点(spread checkpoints)”的含义。

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

  1. 识别共享缓冲区中的所有脏页(已修改的页);
  2. 将所有这些缓冲区写入磁盘(或更确切地说,写入文件系统缓存);
  3. 调用fsync()将所有修改后的文件刷新到磁盘(数据文件)。

仅当上述所有步骤完成之后,检查点才能被视为完成。您可以“尽可能快地”执行这些步骤,即一次性写入所有的脏缓冲区,然后调用fsync()来刷新到磁盘文件,实际上,这是PostgreSQL 8.2之前的版本所采取的策略。但这会导致由于填充文件系统缓存、使设备饱和而导致I/O的停顿,影响到用户会话。

为了解决这个问题,PostgreSQL 8.3引入了“传播检查点”的概念。它不是一次写入所有的脏数据,它将一次检查点的写入分散在很长一段时间内。这使操作系统有时间在后台刷新脏数据,从而使最终调用fsync()的代价要低得多。

我们将写本次检查点的写操作分散到直到下一个检查点发生时的过程来限制写操作(从而降低I/O的大小)。因为数据库知道到下一次检查点操作还有多少时间/WAL,并且它可以计算已经写出了多少缓冲区。但是,数据库必须直到当前检查点的最后才发出写命令,这意味着最后一批写出操作仍将保留在文件系统缓存中,从而使最终fsync()调用(在开始下一个检查点之前发出)再次变得昂贵。

The writes are throttled based on progress towards the next checkpoint – the database knows how much time/WAL do we have left until another checkpoint will be needed, and it can compute how many buffers should be already written out. The database however must not issue writes until the very end – that would mean the last batch of writes would still be in filesystem cache, making the final fsync() calls (issued right before starting the next checkpoint) expensive again.

因此,数据库需要为操作系统内核预留足够的时间,以便脏数据在后台刷新到磁盘上。页面缓存(Linux文件系统缓存)的到期通常是由时间驱动的,尤其是由以下内核参数驱动:

vm.dirty_expire_centisecs = 3000

这表示页面数据将在30秒后过期(默认情况下)。

注意:关于内核参数vm.dirty_background_bytes的调整非常重要。在具有大量内存的系统上,此参数的默认值过高,从而使内核可以在文件系统缓存中累积大量脏数据。内核通常会一次全部清除它们,这反而会降低了传播检查点的好处。

现在,返回到checkpoint_completion_target = 0.5。此配置参数说明距离下一个检查点尚有多远时,本次检查点操作应该完成所有写入的操作。例如,假设检查点仅由checkpoint_timeout = 5min触发,则数据库将限制写入时I/O大小,以使最后一次写入在2.5分钟后完成。然后,操作系统还有2.5分钟的时间将数据刷新到磁盘上,因此5分钟后发出的fsync()调用既便宜又快速。

当然,考虑到文件系统缓存页的超时时间仅仅为30秒,为系统预留2.5分钟似乎有点太长了。您可以考虑增加checkpoint_completion_target,例如设置为0.85,这将使系统大约需要45秒,比它需要的30秒多一点。不过,我们不建议这样做,因为在密集写入的情况下,max_wal_size可能比checkpoint_timeout设置的5分钟更早触发检查点,从而实际为操作系统预留的时间少于了30秒。

但是,处理写密集型工作负载的系统不太可能以更高的checkpoint_timeouts值运行,从而使默认的checkpoint_completion_target值也相对较低。例如,将超时设置为30分钟,它将强制数据库在前15分钟内(以两倍的写入速率)进行所有写操作,然后在其余15分钟内保持空闲状态。

您可以尝试大致使用下面的公式来设置checkpoint_completion_target

(checkpoint_timeout - 2min) / checkpoint_timeout

例如,checkpoint_timeout设置为30分钟时,checkpoint_completion_target约为0.93。通常建议该参数不要超过0.9(超过0.9的设置是可以的,并且您不太可能观察到这两个值之间的显着差异)。当您使用非常高的checkpoint_timeout值(在PostgreSQL 9.6上现在最长可达1天)时,这可能会改变。

Summary

现在您应该知道检查点的用途以及如何调整它们的一些基础考量了。再来总结一下:

  • 大多数检查点应基于时间的,即由checkpoint_timeout触发的:

    • 性能(不频繁的检查点)和较小的恢复所需的时间(频繁的检查点)之间的折衷;
    • 15至30分钟之间的值是最常见的,但是长达1小时也不是一件坏事。
  • 确定checkpoint_timeout的设置后,可以通过估算WAL日志文件的量选择max_wal_size设置;

  • 设置checkpoint_completion_target,以便操作系统内核有足够(但不是特别多)的时间将数据真正刷新到磁盘上;

  • 适当调整vm.dirty_background_bytes以防止操作系统内核在页面缓存(文件系统缓存,同样位于内存中)中累积大量脏数据页。

或者,您可以简单地使用我们的PostgreSQL Performance Tuning服务,该服务不仅覆盖检查点的配置优化,还覆盖PostgreSQL的配置的其他重要部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章