29.4. WAL配置

有几个WAL相关的配置参数会影响数据库性能。本节将解释它们的使用。关于服务器配置参数的设置的一般信息请参考Chapter 18

检查点是在事务序列中的点,这种点保证被更新的堆和索引数据文件的所有信息在该检查点之前已被写入。在检查点时刻,所有脏数据页被刷写到磁盘,并且一个特殊的检查点记录将被写入到日志文件(修改记录之前已经被刷写到WAL文件)。在崩溃时,崩溃恢复过程检查最新的检查点记录用来决定从日志中的哪一点(称为重做记录)开始REDO操作。在这一点之前对数据文件所做的任何修改都已经被保证位于磁盘之上。因此,完成一个检查点后位于包含重做记录的日志段之前的日志段就不再需要了,可以将其回收或删除(当WAL归档工作时,日志段在被回收或删除之前必须被归档)。

检查点对于刷写所有脏数据页到磁盘的要求可能会导致可观的I/O负载。出于这一原因,检查点活动是被有所限制的,这样I/O在检查点开始时开始并且能在下一个检查点将要开始之间完成,这使得检查点期间的性能下降被最小化。

服务器的检查点进程常常自动地执行一个检查点。检查点在每checkpoint_timeout秒开始,或者在快要超过 max_wal_size时开始。 默认的设置分别是 5 分钟和 1 GB。如果从前一个检查点以来没有WAL被写入, 则即使过了checkpoint_timeout新的检查点也会被跳过( 如果正在使用WAL归档并且你想对文件被归档频率设置一个较低的限制来约束 潜在的数据丢失,你应该调整archive_timeout 参数而不是检查点参数)。也可以使用SQL命令 CHECKPOINT来强制一个检查点。

降低checkpoint_timeout和/或max_wal_size会导致检查点更频繁地发生。这使得崩溃后恢复更快,因为需要重做的工作更少。但是,我们必须在这一点和增多的刷写脏数据页开销之间做出平衡。如果full_page_writes被设置(默认情况),则还有一个因素需要考虑。为了确保数据页一致性,在每个检查点之后对一个数据页的第一次修改将导致整个页面内容被日志记录。在这情况下,一个较小的检查点间隔会增加输出到WAL日志的容量,这让使用较小间隔的效果打了折扣并且将导致更多的磁盘I/O。

检查点的代价相对比较昂贵,首先是因为它们要求写出所有当前为脏的缓冲区,正如以上讨论的,第二个原因是它们会导致额外的WAL流量。因此比较明智的做法是将检查点参数设置得足够高,这样检查点就不会过于频繁地发生。你可以设置checkpoint_warning参数作为对于你的检查点参数的一种简单完整性检查。如果检查点的发生时间间隔比checkpoint_warning秒还要接近,一个消息将会被发送到服务器日志来推荐你增加max_wal_size。偶尔出现的这样的消息并不会导致警报,但是如果它出现得太频繁,那么就应该增加检查点控制参数。 如果你没有把max_wal_size设置得足够高, 那么在进行如大型COPY传输等批量操作的时候可能会导致出现大量类似的警告消息。

为了避免大批页面写入对I/O系统产生的冲击,一个检查点中对脏缓冲区的写出操作被散布到一段时间上。 这个时间段由checkpoint_completion_target控制(使用checkpoint_timeout配置),它用检查点间隔的一个分数表示。 I/O率将被调整,以便能按照要求完成检查点:当checkpoint_timeout给定的秒数已经过去,或者max_wal_size被超过之前会发生检查点,以先达到的为准。 在默认值0.9的情况下,LightDB可以被预期在下一个预定检查点之前完成每个检查点(大约是上一个检查点持续时间的90%)。 这将尽可能地分散I/O,使得检查点I/O负载在整个检查点间隔内是一致的。 这个的缺点是延长检查点影响恢复时间,因为需要保留更多的WAL段,为了在可能的恢复中使用。 关注恢复所需时间的用户可能希望减少checkpoint_timeout,以便检查点更频繁地出现,但仍然将I/O分散到检查点间隔。 或者,checkpoint_completion_target可以被缩减,但这将导致更密集I/O时间(检查点期间)和更少I/O时间(检查点完成后,但在下一个计划的检查点之前),因此不推荐。 虽然checkpoint_completion_target可以设置为1.0,通常建议将其设置为不高于0.9(默认值),因为检查点除了写入脏缓冲区之外还包括一些其他活动。 1.0的设置极有可能导致检查点不能按时被完成,这可能由于所需的WAL段数量意外变化导致性能损失。

在 Linux 和 POSIX 平台上,checkpoint_flush_after允许强制 OS 超过一个可配置的字节数后将检查点写入的页面刷入磁盘。否则,这些页面可能会被保留在 OS 的页面缓存中,当检查点结束发出fsync时就会导致大量刷写形成延迟。这个设置通常有助于减小事务延迟,但是它也可能对性能带来负面影响,尤其是对于超过shared_buffers但小于 OS 页面缓存的负载来说更是如此。

lt_wal目录中的 WAL 段文件数量取决于min_wal_sizemax_wal_size以及在之前的检查点周期中产生的 WAL 数量。当旧的日志段文件不再被需要时,它们将被移除或者被再利用(也就是被重命名变成数列中未来的段)。如果由于日志输出率的短期峰值导致超过max_wal_size,不需要的段文件将被移除直到系统回到这个限制以下。低于该限制时,系统会再利用足够的 WAL 文件来覆盖直到下一个检查点之前的需要。这种需要是基于之前的检查点周期中使用的 WAL 文件数量的移动平均数估算出来的。如果实际用量超过估计值,移动平均数会立即增加,因此它能在一定程度上适应峰值用量而不是平均用量。min_wal_size对回收给未来使用的 WAL 文件的量设置了一个最小值,这个参数指定数量的 WAL 将总是被回收给未来使用,即便系统很闲并且 WAL 用量估计建议只需要一点点 WAL 时也是如此。

独立于max_wal_size之外,始终保留最新的 wal_keep_size 兆字节的 WAL 文件和一个额外的 WAL 文件。还有,如果使用了 WAL 归档,旧的段在被归档之前不能被移除或者再利用。如果 WAL 归档无法跟上产生 WAL 的步伐,或者如果archive_command重复失败,旧的 WAL 文件将累积在lt_wal中,直到该情况被解决。一个使用了复制槽的较慢或者失败的后备服务器也会带来同样的效果(见Section 25.2.6)。

在归档恢复模式或后备模式,服务器周期性地执行重启点和正常操作时的检查点相似:服务器强制它所有的状态到磁盘,更新lt_control来指示已被处理的WAL数据不需要被再次扫描,并且接着回收lt_wal中的任何旧日志段文件。 重启点的执行频率不能高于主机上检查点的执行频率,因为重启点只有在检查点记录处才能被执行。 如果从最后一个重启点之后过去了至少checkpoint_timeout秒或者 WAL 尺寸快要达到max_wal_size,则会到达一个检查点,这时会触发一个重启点。 不过,因为对于何时可以执行一个重启点有限制,在恢复期间max_wal_size常常被超过,最多会超过一个检查点周期间的 WAL(不管怎样,max_wal_size从来不是一个硬限制,因此你应该总是应该留出充足的净空来避免耗尽磁盘空间)。

有两个常用的内部WAL函数:XLogInsertRecordXLogFlushXLogInsertRecord用于向共享内存中的WAL缓冲区里放置一个新记录。如果没有空间存放新记录, 那么XLogInsertRecord就不得不写出(向内核缓存里写)一些填满了的WAL缓冲区。 这并非我们所期望的,因为XLogInsertRecord用于每次数据库低层修改(比如,记录插入)时都要在受影响的数据页上持有一个排它锁,因为该操作需要越快越好。但糟糕的是, 写WAL缓冲可能还会强制创建新的日志段,这花的时间甚至更多。通常,WAL缓冲区应该由一个XLogFlush请求来写和刷出, 在大部分时候它都是发生在事务提交的时候以确保事务记录被刷写到永久存储。在那些日志输出量比较大的系统上,XLogFlush请求可能不够频繁,这样就不能避免XLogInsert进行写操作。在这样的系统上,我们应该通过修改配置参数 wal_buffers的值来增加WAL缓冲区的数量。如果设置了 full_page_writes并且系统相当繁忙, 把wal_buffers设置得更高一些将有助于在紧随每个检查点之后的时间段里得到平滑的响应时间。

commit_delay定义了一个组提交领导者进程在XLogFlush中要求一个锁之后将会休眠的微秒数,而组提交追随者都排队等候在领导者之后。这样的延迟可以允许其它服务器进程把它们提交的记录追加到WAL缓冲区中,这样所有的这些记录将会被领导者的最终同步操作刷出。如果fsync被禁用或者当前处于活跃事务中的会话数少于commit_siblings,休眠将不会发生;这样就避免了在其它事务不会很快提交的情况下进行休眠。 请注意在某些平台上,休眠要求的单位是十毫秒,所以任何介于 1 和 10000 微秒之间的非零commit_delay设置的作用都是一样的。 还要注意在某些平台上,休眠操作用的时间会比该参数所请求的要略长一点。

由于commit_delay的目的是允许每次刷写操作的开销能够在并发提交的事务之间进行分摊(可能会以事务延迟为代价),在能够明智地选择该设置之前有必要对代价进行量化。代价越高,在一定程度上commit_delay对于提高事务吞吐量的效果就越好。lt_test_fsync程序可以被用来衡量一次WAL刷写操作需要的平均微秒数。该程序报告的一次8kB写操作后的刷出所用的平均时间的一半常常是commit_delay最有效的设置,因此在优化一种特定工作负荷时,该值被推荐为起始点。当WAL日志被存储在高延迟的旋转磁盘上时,调节commit_delay特别有效,即使在具有非常快同步时间的存储介质上也能得到很显著的收益,例如固态驱动器或具有电池后备写高速缓存的RAID阵列。但是这应该在一个具有代表性的工作负荷下进行明确地测试。较高的commit_siblings值应该用在这种情况中,反之较小的commit_siblings值通常对高延迟介质有用。注意过高的commit_delay设置也很有可能增加事务延迟甚至于整个事务吞吐量都会受到影响。

commit_delay被设置为0(默认值),仍然有可能出现组提交的形式,但是组中的成员只能是那些在前一个刷写操作发生过程窗口中需要刷写它们提交记录的会话。在较高的客户端数量时很可能发生gangway effect,因此即使commit_delay为0,组提交的效果也很显著,并且显式地设置commit_delay将会没有作用。设置commit_delay只有在两种情况下有帮助:(1)有一些并发提交的事务,以及(2)吞吐量在某种程度上被提交率限制。但是在高旋转延迟的设备上,即使少到只有两个客户端,该设置也能有效提高事务吞吐量。

wal_sync_method参数决定LightDB如何请求内核强制将WAL更新到磁盘。只要满足可靠性,那么除了fsync_writethrough所有选项应该都是一样的,fsync_writethrough可以在某些时候强制磁盘高速缓存的刷写,而其他选项不能这样做。不过,哪种选项最快则可能和平台密切相关。 你可以使用lt_test_fsync程序来测试不同选项的速度。请注意如果你关闭了fsync,那么这个参数就无所谓了。

启用wal_debug配置参数(前提是LightDB编译的时候打开了这个支持) 将导致每次XLogInsertRecordXLogFlush WAL调用都被记录到服务器日志。这个选项以后可能会被更通用的机制取代。