# Checkpoints Tuning Basics [译]

原文地址：https://blog.2ndquadrant.com/basics-of-tuning-checkpoints/

在进行不重复的写操作的系统上，调优检查点对于获得良好性能至关重要。然而，检查点是我们经常在社区邮件列表和我们客户的性能调整评估过程中发现混淆和配置问题的领域之一。 （另外一个是autovacuum，前几天由Citus的Joe Nelson讨论过。）那么让我来引导你通过检查点 - 他们做什么以及如何在PostgreSQL中调整它们。

## 什么是检查点？

PostgreSQL是依赖于预写日志（WAL）的数据库之一 - 所有的改变都会先写入一个日志（一系列更改），然后再写入数据文件。这提供了持久性，因为在发生崩溃的情况下，数据库可能会使用WAL来执行恢复 - 从WAL读取更改并将其重新应用于数据文件。

虽然这可能会使写入量增加一倍，但实际上可能会提高性能。用户只需等待WAL（刷新到磁盘），而数据文件只在内存中修改，然后在后台刷新。这很好，因为虽然WAL写入本质上是连续的，但写入数据文件通常是随机的。

假设系统崩溃，数据库需要执行恢复。最简单的方法是从头开始，从头开始重放整个WAL。最后，我们应该得到一个完整和正确的数据库。缺点当然是需要保留和重放整个WAL。我们经常处理的数据库不是非常大（比如几百GB），但是每天产生几个TB的WAL。所以想象在运行一年的数据库上，需要多少磁盘空间来保持所有的WAL，以及在恢复过程中需要多少时间来重放。

但是如果数据库保证在给定的WAL位置（wal_lsn）处的所有数据变更都已经落盘。那么就可以在恢复期间确定这个位置，并仅重播WAL的剩余部分，从而显着减少恢复时间。而且它也可以移除“已知的安全点”之前的WAL。

这正是检查点的目的 - 确保WAL在某个时间点之前不再需要进行恢复，从而减少磁盘空间要求和恢复时间。

*注意：如果你碰巧是一个玩家，你可能对检查点的概念很熟悉 - 你的角色通过了游戏中的某个点，如果你没有击败下一个老板或掉入一个湖泊热熔岩，你从最后一点，而不是从一开始就开始。让我们看看如何在PostgreSQL中实现这一点;-)*

还有另外一个极端 ：非常频繁的检查点（例如每隔一秒钟）。这样可以只保留微量的WAL，恢复极快（不得不重播只有微小的WAL量）。但它也会从异步写入数据页面退化为同步写入，严重影响用户（例如增加COMMIT延迟，减少吞吐量）。

所以在实践中通常希望检查点不要频繁到影响用户，但又足够频繁以限制恢复和磁盘空间需求的时间。





## 触发检查点

有三到四个原因可以触发检查点：

* 直接执行CHECKPOINT命令
* 执行需要检查点的命令（例如，pg_start_backup，CREATE DATABASE或pg_ctl stop | restart等）
* 达到自上次检查点以来的配置时间量
* 从上一个检查点（又名“用完WAL”或“填充WAL”）生成配置的WAL量

前两点在这里相当不重要 - 那些是罕见的，手动触发的事件。这篇博文是关于如何配置其他两个事件，影响常规定期检查点。

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



* `checkpoint_timeout = 5min`
* `max_wal_size = 1GB`（在PostgreSQL 9.5之前是`checkpoint_segments`）

使用这些（默认）值，PostgreSQL将每5分钟触发一次CHECKPOINT，或者在WAL增长到磁盘上大约1GB之后。

注意：max_wal_size是总WAL大小的软限制，这有两个结果。首先，数据库将尽量不超过它，但是被允许，因此在分区上保留足够的空闲空间并监视它。其次，这不是一个“每个检查点”的限制 - 由于扩展检查点（稍后解释），WAL配额被分为2-3个检查点。因此，根据checkpoint_completion_target，使用max_wal_size，数据库将在写入300-500 MB的WAL之后启动CHECKPOINT。

默认值相当低，就像示例配置文件中的大多数其他默认值一样，即使在像Raspberry Pi这样的小型系统上也可以工作。

但是，如何确定您的系统的良好价值？我们的目标不是过分频繁地或不经常地检查点，而我们的调整“最佳实践”包括两个步骤：

* 选择一个“合理”的`checkpoint_timeout`值
* 设置`max_wal_size`足够高，很少到达

很难说checkpoint_timeout的“合理”值是多少，因为它取决于恢复时间目标（RTO），即什么是可接受的最大恢复持续时间。

但是，这有点棘手，因为checkpoint_timeout是生成WAL需要多长时间的限制，而不是直接在恢复时间上。而遗憾的是，无法确切说明需要多长时间才能恢复。 WAL通常由多个进程（运行DML）生成，而恢复则由单个进程执行（这种限制大多是固有的，不可能很快消失）。这不仅会影响本地恢复，还会影响到流式复制到备用数据库。当然，本地恢复通常在重启后立即发生，当文件系统缓存很冷时。

但一般来说，默认值（5分钟）是相当低的，30分钟到1小时之间的值是相当普遍的。 PostgreSQL 9.6甚至将最大值增加到1天（所以有些黑客认为这是一个好的主意）。由于整页写入，低值也可能导致写入放大（我不打算在这里讨论）。

假设我们已经决定使用30分钟。

```
checkpoint_timeout = 30min
```

现在我们需要估计数据库在30分钟内产生多少WAL，以便我们可以使用它来获得max_wal_size。 有几种方法可以确定生成多少WAL：

* 使用`pg_current_xlog_insert_location()` （10之后是`pg_current_wal_insert_lsn`）查看实际的WAL位置（基本上在文件中偏移），并计算每30分钟测量的位置之间的差异。
* 启用`log_checkpoints = on`，然后从服务器日志中提取信息（每个完成的检查点都会有详细的统计信息，包括WAL的数量）。
* 使用来自pg_stat_bgwriter的数据，其中也包含关于检查点数量的信息（可以结合当前max_wal_size值的知识）。

例如，我们使用第一种方法。 在运行pgbench的测试机上，我看到：

```bash
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）似乎是正确的。

其他方法使用不同的数据来源，但想法是相同的。

## 传播检查点

我建议你需要调整`checkpoint_timeout`和`max_wal_size`，但我并没有说出全部的真相。还有另一个参数叫做`checkpoint_completion_target`。但要调整它，你需要了解“传播检查点”是什么意思。

但在CHECKPOINT期间，数据库需要执行以下三个基本步骤：

* 识别共享缓冲区中的所有脏（已修改）块
* 将所有这些缓冲区写入磁盘（或者写入文件系统缓存）
* `fsync`将所有修改后的文件保存到磁盘

只有当所有这些步骤完成后，检查点才算完成。您可以尽可能快地完成这些步骤，即一次性写入所有脏缓冲区，然后在文件上调用fsync，实际上这就是直到PostgreSQL 8.2一直做的事情。但是由于填充文件系统缓存、喂饱设备会导致I/O延迟，并影响用户会话。

为了解决这个问题，PostgreSQL 8.3引入了“扩展检查点”的概念，而不是一次写入所有的数据，写入的时间很长。这使得操作系统有时间在后台刷新脏数据，使得最终的fsync开销更小。

这些写入是基于进入下一个检查点的进度而被限制的， 数据库知道下一个检查点还剩多少时间/WAL空间，并计算出应当写出多少个缓冲区。然而数据库一直拖到最后时刻才写入。这意味着最后一批写入仍然在文件系统缓存中，使得最终的fsync（）调用（在开始下一个检查点之前发出）再次开销巨大。

所以数据库需要留出足够的时间，以便脏数据在后台刷新到磁盘 - 而页缓存（Linux文件系统缓存）的到期通常是由时间驱动的，特别是通过这个内核参数：

`vm.dirty_expire_centisecs = 3000`

这表示数据在30秒后过期（默认情况下）。

注意：当涉及到内核参数时，调整`vm.dirty_background_bytes`很重要。在具有大量内存的系统上，默认值太高，从而使内核在文件系统缓存中积累了大量脏数据。内核通常决定一次刷新它们，减少扩展检查点的好处。

现在回到`checkpoint_completion_target = 0.5`。这个配置参数表示如果所有的写操作都完成的话，到下一个检查点有多远。例如，假设检查点仅由checkpoint_timeout = 5min触发，数据库将会限制写入操作，以便在2.5分钟后完成最后一次写入操作。然后操作系统又有2.5分钟将数据刷新到磁盘，这样5分钟后发出的fsync呼叫廉价又快捷。

当然，离开系统2.5分钟可能看起来过多，考虑到到期超时只有30秒。您可能会增加`checkpoint_completion_target`例如到0.85这将使系统大约45秒，比它需要的30秒多一点。这并不是推荐的，因为在密集写入的情况下，检查点可能比5分钟之后更早地被max_wal_size触发，使操作系统少于30秒。

但是，处理写入密集型工作负载的系统不太可能运行更高的checkpoint_timeouts值，使默认的completion_target值肯定太低。例如，如果将超时设置为30分钟，则会强制数据库在前15分钟内完成所有写入（以写入速率的两倍），然后闲置15分钟。

相反，您可以尝试使用此公式粗略设置checkpoint_completion_target

`（checkpoint_timeout - 2min）/ checkpoint_timeout`

其中30分钟约为0.93。有时候建议不要超过0.9--这可能是好的，你不可能观察到这两个值之间的任何显着差异。 （当使用非常高的checkpoint_timeout值时，这可能会改变，PostgreSQL 9.6现在可以达到1天）。



## 总结

所以现在你应该知道检查点的目的是什么，也是调整它们的基础。总结一下：

* 大多数检查点应该是基于时间的，即由checkpoint_timeout触发
  * 性能（不频繁的检查点）和恢复所需的时间（频繁的检查点）
  * 15-30分钟之间的数值是最常见的，上升到1h也不会有什么不好的，9.6甚至能设置为1天。
* 在决定超时之后，通过估计WAL的数量来选择`max_wal_size`
* 设置`checkpoint_completion_target`，以便内核有足够的时间将数据刷新到磁盘（但不是非常多）
* 还要调整`vm.dirty_background_bytes`来防止内核在页面缓存中积累大量脏数据