PgSQL: WAL与检查点

介绍PostgreSQL中的WAL与检查点机制。

数据库需要保证两个基本的特性:可靠性可用性。通俗来讲:

可靠性就是:出了故障,既不会丢数据,也不会弄脏数据。

可用性就是:保证足够的读写性能,出了故障后,能够快速恢复服务。

朴素的数据库实现有两个选项:在内存中修改数据页,或者将事物变更直接写入磁盘。但这产生了一个两难困境:

  • 内存支持随机读写,因此在性能上表现强悍,然而作为易失性存储,一旦故障就会丢数据。
  • 硬盘恰恰相反,随机读写表现糟糕,但在故障时数据要可靠的多。

内存可用性强可靠性差,硬盘可用性差但可靠性强,如何解决这一对矛盾,让内存与硬盘取长补短,就是生产级数据库需要考虑的问题了。

0x1 核心思想

硬盘的随机写入性能很糟糕,但顺序写入的性能却非常可观。即使是SSD也符合这一规律,因为一次写入的擦除单位是Block(通常是几M),而操作系统的写入单元是Page(通常约4k)。如果每次事物提交都要直接将脏数据页落盘,性能表现肯定不会可观。但如果采用另一种方式,将数据的变更而不是变更后的最新数据本身落盘,就可以将随机写入变为顺序写入,从而极大地提高磁盘写入效率。

于是,预写式日志(WAL,Write Ahead Log) 出现了,所谓日志,在最朴素的意义上来讲,就是一个Append-Only的数据文件,记录了操作的内容。只要保留了WAL,数据库就是可靠的,可以恢复的。从一个给定的状态,例如空数据库开始,回放所有的操作日志到当前的时间点,就可以恢复出当前数据库应有的状态。与此同时,如果日志已经落盘确保了可靠性,数据页就不需要在每次提交时落盘了。数据页的读写可以完全在内存进行,从而提供强悍的性能支持。

但可用性不仅仅包括足够的性能,当发生故障时能够快速恢复也是可用性要求的一部分。考虑最极端的情况,从数据库创建之初所有数据页就在内存里一直飘着,只有操作日志落了盘。现在数据库运行了一整年,突然崩溃了,这时候要想恢复就需要重放一整年的操作日志,也许需要几个小时,也许需要好几天。对于生产环境,这是无法接受的,检查点(Checkpoint)解决了这个问题。

检查点(Checkpoint)类似于游戏中存档的概念,远古时期的很多游戏没有存档,一旦Game Over就要重头再来。后来的游戏有了记忆和存档,当挑战Boss失败时,只要读取最近的存档,就可以避免从头开始。

数据库中的检查点代表这样一种操作,在某一个检查点时,所有脏数据页会写回到磁盘中,使得磁盘和内存中的数据保持一致。这样当故障恢复时,只需要从该检查点开始回放操作日志即可。

例如,每个整点执行一次检查点,存档一次,那么当故障时,只需要从本小时开始的检查点开始回放WAL,就可以完成恢复。同时,检查点还有一个好处是,当数据页落盘之后,在这个检查点之前的WAL日志就可以不用了。对于高负载数据库,例如每小时产生TB级别WAL的数据库,使用检查点能够极大地减少恢复的时间和磁盘的用量。

通过检查点和预写式日志,数据库可以同时保证高度的可靠性和可用性。

0x2 WAL概述

预写式日志(WAL)是保证数据完整性的一种标准方法。对其详尽的描述几乎可以在所有(如果不是全部)有关事务处理的书中找到。简单来说,WAL的中心概念是数据文件(存储着表和索引)的修改必须在这些动作被日志记录之后才被写入,即在描述这些改变的日志记录被刷到持久存储以后。如果我们遵循这种过程,我们不需要在每个事务提交时刷写数据页面到磁盘,因为我们知道在发生崩溃时可以使用日志来恢复数据库:任何还没有被应用到数据页面的改变可以根据其日志记录重做(这是前滚恢复,也被称为REDO)。

使用WAL可以显著降低磁盘的写次数,因为只有日志文件需要被刷出到磁盘以保证事务被提交,而被事务改变的每一个数据文件则不必被刷出。日志文件被按照顺序写入,因此同步日志的代价要远低于刷写数据页面的代价。在处理很多影响数据存储不同部分的小事务的服务器上这一点尤其明显。此外,当服务器在处理很多小的并行事务时,日志文件的一个fsync可以提交很多事务。

##异步提交

异步提交是一个允许事务能更快完成的选项,代价是在数据库崩溃时最近的事务会丢失。在很多应用中这是一个可接受的交换。

如前一节所述,事务提交通常是同步的:服务器等到事务的WAL记录被刷写到持久存储之后才向客户端返回成功指示。因此客户端可以确保那些报告已被提交的事务确会被保存,即便随后马上发生了一次服务器崩溃。但是,对于短事务来说这种延迟是其总执行时间的主要部分。选择异步提交模式意味着服务器将在事务被逻辑上提交后立刻返回成功,而此时由它生成的WAL记录还没有被真正地写到磁盘上。这将为小型事务的生产力产生显著地提升。

异步提交会带来数据丢失的风险。在向客户端报告事务完成到事务真正被提交(即能保证服务器崩溃时它也不会被丢失)之间有一个短的时间窗口。因此如果客户端将会做一些要求其事务被记住的外部动作,就不应该用异步提交。例如,一个银行肯定不会使用异步提交事务来记录一台ATM的现金分发。但是在很多情境中不需要这种强的保证,例如事件日志。

使用异步提交带来的风险是数据丢失,而不是数据损坏。如果数据库可能崩溃,它会通过重放WAL到被刷写的最后一个记录来进行恢复。数据库将因此被恢复到一个自身一致状态,但是任何还没有被刷写到磁盘的事务将不会反映在该状态中。因此其影响就是丢失了最后的少量事务。由于事务按照提交顺序被重放,所以不会出现任何不一致性 — 例如一个事务B按照前面一个事务A的效果来进行修改,则不会出现A的效果丢失而B的效果被保留的情况。

用户可以选择每一个事务的提交模式,这样可以有同步提交和异步提交的事务并行运行。这允许我们灵活地在性能和事务持久性之间进行权衡。提交模式由用户可设置的参数synchronous_commit控制,它可以使用任何一种修改配置参数的方法进行设置。一个事务真正使用的提交模式取决于当事务提交开始时synchronous_commit的值。

特定的实用命令,如DROP TABLE,被强制按照同步提交而不考虑synchronous_commit的设定。这是为了确保服务器文件系统和数据库逻辑状态之间的一致性。支持两阶段提交的命令页总是同步提交的,如PREPARE TRANSACTION

如果数据库在异步提交和事务WAL记录写入之间的风险窗口期间崩溃,在该事务期间所作的修改丢失。风险窗口的持续时间是有限制的,因为一个后台进程(“WAL写进程”)每wal_writer_delay毫秒会把未写入的WAL记录刷写到磁盘。风险窗口实际的最大持续时间是wal_writer_delay的3倍,因为WAL写进程被设计成倾向于在忙时一次写入所有页面。

一个立刻关闭等同于一次服务器崩溃,因此也将会导致未刷写的异步提交丢失。

异步提交提供的行为与配置fsync = off不同。fsync是一个服务器范围的设置,它将会影响所有事务的行为。它禁用了PostgreSQL中所有尝试同步写入到数据库不同部分的逻辑,并且因此一次系统崩溃(即,一个硬件或操作系统崩溃,不是PostgreSQL本身的失败)可能造成数据库状态的任意损坏。在很多情境中,带来大部分性能提升的异步提交可以通过关闭fsync来获得,而且不会带来数据损坏的风险。

commit_delay也看起来很像异步提交,但它实际上是一种同步提交方法(事实上,commit_delay在异步提交时被忽略)。commit_delay会使事务在刷写WAL到磁盘之前有一个延迟,它期望由一个这样的事务所执行的刷写能够也服务于其他同时提交的事务。该设置可以被看成是一种时间窗口,在其期间事务可以参与到一次单一的刷写中,这种方式用于在多个事务之间摊销刷写的开销。

0x3 查看WAL状态

0x5 流复制

0x6 Checkpoint

相关命令

  • CHECKPOINT : 强制一个事务日志检查点

一个检查点是事务日志序列中的一个点,在该点上所有数据文件 都已经被更新为反映日志中的信息。所有数据文件将被刷写到磁盘。 检查点期间发生的细节可见第 30.4 节

CHECKPOINT命令在发出时强制一个 立即的检查点,而不用等待由系统规划的常规检查点(由 第 19.5.2 节中的设置控制)。 CHECKPOINT不是用来在普通操作中 使用的命令。

如果在恢复期间执行,CHECKPOINT 命令将强制一个重启点(见第 30.4 节) 而不是写一个新检查点。

只有超级用户能够调用CHECKPOINT

WAL相关视图与函数


Checkpoint相关参数

安全的删除WAL

如果想要删除wal日志,要么让pg在CHECKPOINT的时候自己删除,或者使用pg_archivecleanup。除了以下三种情况,pg会自动清除不再需要的wal日志:

  1. archive_mond=on,但是archive_commandfailed,这样pg会一直保留wal日志,直到重试成功。
  2. wal_keep_segments需要保留一定的数据。
  3. 9.4之后,可能会因为replication slot保留;

如果都不符合上述情况,我们想要清理wal日志,可以通过执行CHECKPOINT来清理当前不需要的wal。

在一些非寻常情况下,可能需要pg_archivecleanup命令,比如由于wal归档失败导致的wal堆积引起的磁盘空间溢出。你可能使用这个命令来清理归档wal日志,但是永远不要手动删除wal段;