可靠性和预写日志

可靠性

可靠性是任何严肃的数据库系统的重要属性,而PostgreSQL尽一切可能保证可靠操作。可靠操作的一个方面是,已提交事务记录的所有数据都应存储在非易失性区域中,该区域不会受到断电、操作系统故障和硬件故障的影响(当然,非易失性区域本身的故障除外)。成功地将数据写入计算机的永久存储器(磁盘驱动器或等效设备)通常可以满足此要求。事实上,即使计算机严重损坏,如果磁盘驱动器完好无损,它们也可以移动到另一台具有类似硬件的计算机上,并且所有已提交的事务都将保持完整。

虽然定期将数据强制写入磁盘盘片似乎是一项简单的操作,但事实并非如此。由于磁盘驱动器的速度比主内存和 CPU 慢得多,因此计算机的主内存和磁盘盘片之间存在多层缓存。首先,是操作系统的缓冲区缓存,它缓存经常请求的磁盘块并合并磁盘写入。幸运的是,所有操作系统都为应用程序提供了一种强制将缓冲区缓存中的数据写入磁盘的方法,而PostgreSQL就使用了这些功能。

 

其次,磁盘驱动器控制器中可能存在缓存;这在RAID控制卡上尤为常见。其中一些缓存是直写的,这意味着写入的内容一到达就会发送到驱动器。其他缓存是回写的,这意味着数据稍后会发送到驱动器。此类缓存可能存在可靠性隐患,因为磁盘控制器缓存中的内存是易失性的,在电源故障时会丢失其内容。更好的控制卡具有电池备份单元BBU ),这意味着卡上有一块电池,可以在系统断电的情况下为缓存供电。恢复供电后,数据将写入磁盘驱动器。

最后,大多数磁盘驱动器都有缓存。有些是直写式,有些是回写式,回写式驱动器缓存和磁盘控制器缓存一样,都存在数据丢失问题。消费级 IDE 和 SATA 驱动器尤其可能具有回写式缓存,这些缓存在电源故障后无法恢复。许多固态驱动器 (SSD) 也具有易失性回写式缓存。

通常可以禁用这些缓存;但是,执行此操作的方法因操作系统和驱动器类型而异:

  • Linux上,可以使用hdparm -I查询 IDE 和 SATA 驱动器;如果写入缓存旁边*,则表示写入缓存已启用。可以使用hdparm -W 0关闭写入缓存。可以使用sdparm查询 SCSI 驱动器。使用sdparm --get=WCE检查写入缓存是否已启用,使用sdparm --clear=WCE禁用它。

  • FreeBSD上,可以使用atacontrol查询 IDE 驱动器,并使用/boot/loader.conf中的hw.ata.wc=0关闭写入缓存;可以使用camcontrol determine查询 SCSI 驱动器,并使用sdparm查询和更改写入缓存(如果可用)。

  • Solaris上,磁盘写入缓存由format -e控制。(Solaris ZFS文件系统在启用磁盘写入缓存的情况下是安全的,因为它会发出自己的磁盘缓存刷新命令。)

  • Windows上,如果wal_sync_methodopen_datasync(默认值),则可以通过取消选中“我的电脑\打开\磁盘驱动器\属性\硬件\属性\策略\启用磁盘上的写缓存”来禁用写缓存。或者,将wal_sync_method设置为fsyncfsync_writethrough,以阻止写缓存。

  • OS X上,可以通过将wal_sync_method设置为fsync_writethrough来防止写入缓存

最近的 SATA 驱动器(遵循ATAPI-6或更高版本的驱动器)提供了驱动器缓存刷新命令(FLUSH CACHE EXT),而 SCSI 驱动器长期以来一直支持类似的命令SYNCHRONIZE CACHE。这些命令不能直接被PostgreSQL访问,但一些文件系统(例如ZFSext4)可以使用它们将数据刷新到启用写回的驱动器上的盘片上。不幸的是,此类文件系统在与电池备份单元(BBU)磁盘控制器结合使用时表现不佳。在这样的设置中,synchronize 命令会强制将控制器缓存中的所有数据写入磁盘,从而消除了 BBU 的许多好处。您可以运行pg_test_fsync程序来查看是否受到影响。如果受到影响,可以通过关闭文件系统中的写屏障或重新配置磁盘控制器(如果可以的话)来重新获得 BBU 的性能优势。如果关闭了写屏障,请确保电池仍然正常工作;电池故障可能会导致数据丢失。希望文件系统和磁盘控制器设计人员最终能够解决这个不理想的行为。

当操作系统向存储硬件发送写入请求时,它几乎无法确保数据已到达真正非易失性存储区域。相反,管理员有责任确保所有存储组件都确保数据和文件系统元数据的完整性。避免使用无电池供电的写入缓存的磁盘控制器。在驱动器级别,如果驱动器无法保证在关机前写入数据,请禁用写回缓存。如果您使用 SSD,请注意其中许多默认情况下不遵守缓存刷新命令。您可以使用diskchecker.pl测试可靠的 I/O 子系统行为。

另一个数据丢失风险是由磁盘盘片写入操作本身造成的。磁盘盘片被分成多个扇区,通常每个扇区 512 字节。每个物理读取或写入操作都会处理整个扇区。当写入请求到达驱动器时,它可能是 512 字节的倍数(PostgreSQL通常一次写入 8192 字节,即 16 个扇区),并且写入过程可能随时因断电而失败,这意味着一些 512 字节扇区已写入,而其他扇区则未写入。为了防止此类故障,PostgreSQL会定期将完整页面映像写入永久 WAL 存储,然后再修改磁盘上的实际页面。通过这样做,在崩溃恢复期间,PostgreSQL可以从 WAL 中恢复部分写入的页面。如果您有阻止部分页面写入的文件系统软件(例如 ZFS),您可以通过关闭 full_page_writes 参数来关闭此页面映像电池备份单元 (BBU) 磁盘控制器不会阻止部分页面写入,除非它们保证将数据作为完整(8kB)页面写入 BBU。

PostgreSQL还可以防止由于硬件错误或介质故障而导致存储设备上某些类型的数据损坏,例如读取/写入垃圾数据。

  • WAL 文件中的每个记录都受 CRC-32(32 位)检查保护,这使我们能够判断记录内容是否正确。当我们写入每个 WAL 记录时会设置 CRC 值,并在崩溃恢复、存档恢复和复制期间进行检查。

  • 尽管 WAL 记录中记录的整页图像将受到保护,但数据页目前默认没有进行校验和;有关启用数据页校验和的详细信息,请参阅initdb 。

  • 内部数据结构(例如pg_clogpg_subtranspg_multixactpg_serialpg_notifypg_statpg_snapshots)不直接进行校验和计算,页面也不受整页写入保护。但是,如果这些数据结构是持久性的,则会写入 WAL 记录,以便在崩溃恢复时准确重建最近的更改,并且这些 WAL 记录受到如上所述的保护。

  • pg_twophase中的各个状态文件受 CRC-32 保护。

  • 用于排序、具体化和中间结果的较大 SQL 查询中的临时数据文件目前未进行校验,也不会为这些文件的更改写入 WAL 记录。

PostgreSQL不能防止可纠正的内存错误,并且假定您将使用采用行业标准错误纠正码 (ECC) 或更好保护的 RAM 进行操作。

预写日志(WAL

预写日志WAL ) 是确保数据完整性的标准方法。在大多数(如果不是全部)有关事务处理的书籍中都可以找到详细描述。简而言之,WAL的核心概念是,对数据文件(表和索引所在的文件)的更改必须仅在记录这些更改之后写入,也就是说,在描述更改的日志记录刷新到永久存储之后。如果我们遵循此过程,则不需要在每次提交事务时将数据页刷新到磁盘,因为我们知道,如果发生崩溃,我们将能够使用日志恢复数据库:任何尚未应用于数据页的更改都可以根据日志记录重做。(这是前滚恢复,也称为 REDO。)

异步提交

异步提交是一种允许事务更快完成的选项,但代价是如果数据库崩溃,最近的事务可能会丢失。在许多应用程序中,这是一个可以接受的权衡。

如上一节所述,事务提交通常是同步的:服务器等待事务的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 的三倍,因为 WAL 写入器旨在在繁忙时段一次写入整个页面。

异步提交提供的行为不同于设置fsync = off。fsync是一个服务器范围的设置,它将改变所有事务的行为。 它禁用PostgreSQL中尝试同步对数据库不同部分的写入的所有逻辑,因此系统崩溃(即硬件或操作系统崩溃,而不是PostgreSQL本身的故障)可能导致数据库状态的任意严重损坏。 在许多情况下,异步提交提供了通过关闭fsync可以获得的大部分性能改进,但没有数据损坏的风险。

commit_delay听起来也非常类似于异步提交,但它实际上是一种同步提交方法(事实上,在异步提交期间,commit_delay会被忽略) 。commit_delay会在事务将WAL刷新到磁盘之前造成延迟,希望由一个这样的事务执行的单个刷新也可以服务于大约同时提交的其他事务。该设置可以被认为是一种增加事务可以加入即将参与单个刷新的组的时间窗口的方法,以在多个事务之间分摊刷新的成本。

WAL配置

有几个与WAL相关的配置参数会影响数据库性能。本节介绍它们的用法。有关设置服务器配置参数的一般信息,请参阅第 19 章。

检查点是事务序列中的点,在这些点上可以保证堆和索引数据文件已更新了该检查点之前写入的所有信息。在检查点时,所有脏数据页都会刷新到磁盘,并将特殊检查点记录写入日志文件。(更改记录先前已刷新到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控制,它是检查点间隔的一部分。I/O 速率会进行调整,以便检查点在给定的checkpoint_timeout秒数过去后或在超过max_wal_size之前(以较早者为准)完成。使用默认值 0.5,PostgreSQL预计在下一个检查点开始前的一半时间内完成每个检查点。在正常运行期间非常接近最大 I/O 吞吐量的系统上,您可能需要增加checkpoint_completion_target以减少来自检查点的 I/O 负载。这样做的缺点是延长检查点会影响恢复时间,因为需要保留更多的 WAL 段以备在恢复中使用。尽管checkpoint_completion_target可以设置为 1.0,但最好将其保持在 1.0 以下(最多 0.9),因为检查点除了写入脏缓冲区外还包括其他一些活动。设置为 1.0 很可能会导致检查点无法按时完成,这将导致所需 WAL 段数量发生意外变化,从而导致性能损失。

在 Linux 和 POSIX 平台上,checkpoint_flush_after允许强制操作系统在可配置的字节数之后将检查点写入的页面刷新到磁盘。否则,这些页面可能会保留在操作系统的页面缓存中,从而导致在检查点结束时发出fsync时出现停顿。此设置通常有助于减少事务延迟,但也可能对性能产生不利影响;特别是对于大于shared_buffers但小于操作系统页面缓存的工作负载。

pg_xlog目录中的 WAL 段文件数量取决于min_wal_sizemax_wal_size以及在先前的检查点周期中生成的 WAL 数量。当不再需要旧的日志段文件时,它们将被移除或回收(即重命名为编号序列中的未来段)。如果由于日志输出速率的短期峰值而超出max_wal_size,则不需要的段文件将被移除,直到系统回到此限制以下。低于该限制时,系统将回收足够的 WAL 文件以满足下一个检查点之前的估计需求,并移除其余文件。该估计基于先前检查点周期中使用的 WAL 文件数量的移动平均值。如果实际使用量超过估计值,移动平均值会立即增加,因此它在某种程度上适应峰值使用量而不是平均使用量。min_wal_size对回收以供未来使用的 WAL 文件数量设置了最小值;即使系统处于空闲状态并且 WAL 使用量估计表明只需要很少的 WAL,也始终会回收那么多 WAL 以供未来使用。

独立于max_wal_sizewal_keep_segments + 1 个最新的 WAL 文件始终被保留。另外,如果使用 WAL 归档,旧段在归档之前不能被删除或回收。如果 WAL 归档无法跟上 WAL 生成的速度,或者archive_command反复失败,旧 WAL 文件将在pg_xlog中累积,直到情况得到解决。使用复制槽的缓慢或失败的备用服务器将具有相同的效果(参见第 26.2.6 节)。

在归档恢复或备用模式下,服务器会定期执行重启点,这类似于正常操作中的检查点:服务器强制将其所有状态写入磁盘,更新pg_control文件以指示无需再次扫描已处理的 WAL 数据,然后回收pg_xlog目录中的任何旧日志段文件。重启点不能比主服务器上的检查点更频繁地执行,因为重启点只能在检查点记录处执行。如果自上次重启点以来至少已经过去了checkpoint_timeout秒,或者 WAL 大小即将超过max_wal_size ,则在达到检查点记录时会触发重启点。但是,由于对何时可以执行重启点的限制,在恢复期间经常会超过max_wal_size,最多超过一个检查点周期的 WAL 值。(无论如何, max_wal_size从来都不是硬限制,因此您应该始终留出足够的空间以避免耗尽磁盘空间。)

有两个常用的内部WAL函数:XLogInsertRecordXLogFlushXLogInsertRecord用于将新记录放入共享内存中的WAL缓冲区。如果没有空间容纳新记录,XLogInsertRecord则必须写入(移动到内核缓存)几个已填满的WAL缓冲区。这是不可取的,因为XLogInsertRecord每次数据库低级修改(例如,插入行)时都会使用,此时受影响的数据页上会持有独占锁,因此操作需要尽可能快。更糟糕的是,写入WAL缓冲区还可能强制创建新的日志段,这会花费更多时间。通常,WAL缓冲区应该通过请求进行写入和刷新XLogFlush,该请求大多在事务提交时发出,以确保将事务记录刷新到永久存储中。在日志输出较高的系统上,XLogFlush请求可能出现的频率不够高,无法避免必须进行写入。在这样的系统上,应该通过修改wal_buffers参数来增加WALXLogInsertRecord缓冲区的数量。当设置了full_page_writes并且系统非常繁忙时,将wal_buffers设置得更高将有助于平滑每个检查点之后紧接着的期间内的响应时间。

commit_delay参数定义组提交领导者进程在 在XLogFlush 内获取锁之后将休眠多少微秒,而组提交追随者则在领导者后面排队。这个延迟允许其他服务器进程将它们的提交记录添加到 WAL 缓冲区,以便所有记录都将被领导者的最终同步操作刷新。如果没有启用fsync,或者当前处于活动事务中的其他会话少于commit_siblings个,则不会发生休眠;这避免了在其他会话不太可能很快提交时休眠。注意在某些平台上,休眠请求的分辨率是 10 毫秒,因此任何在 1 到 10000 微秒之间的非零commit_delay设置都会产生相同的效果。还要注意在某些平台上,休眠操作可能比参数请求的时间稍长。

由于commit_delay的目的是允许每个刷新操作的成本在并发提交的事务之间分摊(可能以事务延迟为代价),因此有必要在明智地选择设置之前量化该成本。成本越高,commit_delay 在增加事务吞吐量方面的效果就越好直到一定程度。pg_test_fsync程序可用于测量单个 WAL 刷新操作所需的平均时间(以微秒为单位)。该程序报告的单个 8kB 写入操作后刷新所需平均时间的一半通常是commit_delay最有效的设置,因此建议将此值作为优化特定工作负载时的起点。虽然当 WAL 日志存储在高延迟旋转磁盘上时,调整commit_delay特别有用,但即使在具有非常快同步时间的存储介质上,例如固态驱动器或具有电池支持的写入缓存的 RAID 阵列,好处也可能很明显;但这绝对应该针对代表性工作负载进行测试。在这种情况下,应使用较高的commit_siblings值,而较小的commit_siblings值通常对延迟较高的介质有帮助。请注意,如果commit_delay设置过高,则很有可能增加事务延迟,从而影响总事务吞吐量。

commit_delay设置为零(默认值)时,仍有可能发生某种形式的组提交,但每个组将仅由在发生前一次刷新操作(如果有)的窗口期间达到需要刷新其提交记录的点的会话组成。在客户端数量较高的情况下,往往会出现“通道效应” ,因此即使commit_delay为零,组提交的影响也会变得显著,因此明确设置commit_delay往往帮助不大。设置commit_delay仅在以下情况下才有帮助:(1) 存在一些并发提交的事务,(2) 吞吐量在一定程度上受提交率限制;但在高旋转延迟的情况下,此设置可以有效地提高事务吞吐量,只需两个客户端(即,一个提交的客户端和一个兄弟事务)。

wal_sync_method参数决定PostgreSQL将如何要求内核强制将WAL更新输出到磁盘。所有选项在可靠性方面都应该相同,但fsync_writethrough除外,它有时会强制刷新磁盘缓存,即使其他选项不会这样做。但是,哪一个是最快的,这完全取决于平台。您可以使用 pg_test_fsync 程序测试不同选项的速度请注意,如果fsync已关闭,则此参数无关紧要。

启用wal_debug配置参数(假设PostgreSQL已编译支持该参数)将导致每个XLogInsertRecordWALXLogFlush 调用都被记录到服务器日志中。将来,此选项可能会被更通用的机制取代。

WAL 内部原理

WAL是自动启用的;管理员不需要采取任何行动,除了确保满足WAL日志的磁盘空间要求,并进行任何必要的调整

WAL日志存储在数据目录下的pg_xlog目录中,作为一组段文件,通常每个文件大小为 16 MB(但可以在构建服务器时通过改变--with-wal-segsize配置选项来更改大小)。每个段分为几页,通常每页 8 kB(此大小可以通过--with-wal-blocksize配置选项更改)。日志记录头在access/xlogrecord.h中描述;记录内容取决于正在记录的事件类型。段文件以不断增加的数字作为名称,从0000000100000000000000001开始。数字不会换行,但要用尽可用的数字库存需要非常非常长的时间。

如果日志与主数据库文件位于不同的磁盘上,则非常有利。这可以通过将pg_xlog目录移动到另一个位置(当然,在服务器关闭时)并创建从主数据目录中的原始位置到新位置的符号链接来实现

WAL的目的是确保在数据库记录被修改之前写入日志,但是磁盘驱动器可能会错误地向内核报告写入成功,而实际上它们只是缓存了数据,尚未将其存储在磁盘上,从而破坏这一目标。在这种情况下发生电源故障可能会导致无法恢复的数据损坏。管理员应尝试确保保存PostgreSQLWAL日志文件的磁盘不会产生此类错误报告。(请参阅第 30.1 节。)

在创建检查点并刷新日志后,检查点的位置将保存在文件pg_control。因此,在恢复开始时,服务器首先读取pg_control,然后读取检查点记录;然后通过从检查点记录中指示的日志位置向前扫描来执行 REDO 操作。由于在检查点之后的第一次页面修改中,数据页的所有内容都保存在日志中(假设未禁用full_page_writes),因此自检查点以来更改的所有页面都将恢复到一致状态。

为了处理pg_control损坏的情况,我们应该支持以相反的顺序(从最新到最旧)扫描现有日志段的可能性,以便找到最新的检查点。这尚未实现。pg_control足够小(小于一个磁盘页面),因此不会出现部分写入问题,并且截至撰写本文时,还没有报告仅由于无法读取pg_control本身而导致数据库故障。因此,虽然从理论上讲这是一个弱点,但pg_control在实践中似乎不是一个问题。

 

posted @ 2024-05-30 19:05  wongchaofan  阅读(6)  评论(0编辑  收藏  举报