服务器 频道

预写式日志(Write-Ahead Logging (WAL))

【IT168 服务器学院】    预写式日志 (WAL) 是一种实现事务日志的标准方法。有关它的详细描述可以在大多数(如果不是全部的话)有关事务处理的书中找到。 简而言之,WAL 的中心思想是对数据文件的修改(它们是表和索引的载体)必须是只能发生在这些修改已经记录了日志之后, 也就是说,在描述这些变化的日志记录冲刷到永久存储器之后。 如果我们遵循这个过程,那么我们就不需要在每次事务提交的时候都把数据页冲刷到磁盘,因为我们知道在出现崩溃的情况下, 我们可以用日志来恢复数据库:任何尚未附加到数据页的记录都将先从日志记录中重做(这叫向前滚动恢复,也叫做 REDO)。

    WAL 的好处

    使用 WAL 的第一个主要的好处就是显著地减少了磁盘写的次数。 因为在日志提交的时候只有日志文件需要冲刷到磁盘;而不是事务修改的所有数据文件。 在多用户环境里,许多事务的提交可以用日志文件的一次 fsync() 来完成。而且,日志文件是顺序写的, 因此同步日志的开销要远比同步数据页的开销要小。 这一点对于许多小事务修改数据存储的许多不同的位置更是如此。

    另外一个好处就是数据页的完整性。实际情况是,在 WAL 之前,PostgreSQL 从来不能保证在崩溃的情况下数据页的完整性。 在 WAL 之前,在写的过程中的任何崩溃都可能导致:

 

    索引记录指向一个不存在的表的行

    索引记录在分裂操作中丢失

    完全崩溃了的表和索引页的内容,因为数据页只写了一部分

    索引的问题(问题 1 和 2)可能已经通过额外的 fsync 调用修补好了,但是如果没有 WAL,那么没有很明显的处理第三种情况的方法; WAL 在日志里保存整个数据页的内容 -- 如果那些内容在崩溃后的恢复中需要确保数据页的完整性的话。

    最后,WAL 还提供了数据库在线备份和恢复(backup and restore (BAR))的可能。 通过归档的 WAL 文件,我们可以支持恢复到手头的 WAL 文件包含的任意时刻: 我们只需要简单地安装以前的数据库的物理备份,然后重放 WAL 到自己希望的时间。 另外,物理备份还不必是数据库状态的一个即时快照 — 如果它是花了一段时间制作的话, 因为 WAL 日志的重放将修复任何内部的不一致。

    有几个与 WAL 相关的参数会影响数据库性能。 本节讨论它们的使用。

    检查点(Checkpoints) 是事务序列中的点, 我们保证在该点之前的所有日志信息都更新到数据文件中去了。 在检查点时,所有脏数据页都冲刷到磁盘并且向日志文件中写入一条特殊的检查点记录。 结果是,在发生崩溃的时候,恢复器就知道应该从日志中的哪个点(称做 redo 记录)开始做 REDO 操作, 因为在该记录前的对数据文件的任何修改都已经在磁盘上了。 在完成检查点处理之后,任何在 redo 记录之前写的日志段都不再需要, 因此可以循环使用或者删除。(在进行 WAL 归档的时候, 这些日志在循环利用或者删除之前必须先归档。)

    服务器的后端写进程将每隔这段时间就自动执行一个检查点。 每隔 checkpoint_segments 个日志段就创建一个检查点, 或者每隔 checkpoint_timeout 秒创建一个。 以先到为准。缺省设置分别是 3 个段和 300 秒。 我们也可以用 SQL 命令 CHECKPOINT 强制一个检查点。

    减少 checkpoint_segments 和/或 checkpoint_timeout 会令检查点更频繁一些。 这样就允许更快的崩溃后恢复(因为需要重做的工作更少)。不过, 我们必须在这个目的和更频繁地冲刷脏数据页所带来的额外开销之间取得平衡。 另外,为了保证数据页的一致性,在每个检查点之后的第一次数据页的变化会导致对整个页面内容的日志记录。 因此,检查点时间间隔短了会导致输出到 WAL 日志中的数据的增加,会抵销一部分缩短间隔的目标, 并且怎么着都会产生更多的磁盘 I/O。

    检查点开销相当高,首先是因为它需要写出所有当前脏的缓冲区,其实是因为他们导致上面讨论的额外的后继 WAL 流量。 因此把检查点参数设置得足够高,让检查点发生的频率降低是明智的。要对你的检查点参数的一个简单自检,可以设置 checkpoint_warning 参数。如果检查点发生的间隔接近 checkpoint_warning 秒, 那么将向服务器日志输出一条消息,建议你增加 checkpoint_segments 的数值。 偶尔出现的这样的警告并不会导致警告,但是如果它出现得太频繁,那么就应该增加检查点控制参数。

    至少会有一个 WAL 段文件,而且通常不会超过 2 * checkpoint_segments + 1 个文件。每个段文件通常 16MB 大(当然你可以在制作服务器的时候修改它)。你可以用这些信息来估计 WAL 需要的空间。 通常,如果一个旧的日志段文件不再需要了,那么它将得到循环使用(重命名为顺序的下一个可用段)。 如果由于短期的日志输出峰值,导致了超过 2 * checkpoint_segments + 1 个段文件, 那么到系统再次回到这个限制之内的时候,多于的段文件会被删除,而不是循环使用。

    有两个常用的 WAL 函数: LogInsert 和 LogFlush。 LogInsert 用于向共享内存中的 WAL 缓冲区里加一条新的记录。如果没有空间存放新记录, 那么LogInsert 就不得不写出(向内核缓存里写)一些填满了的WAL缓冲。 我们可不想这样,因为 LogInsert 用于每次数据库低层修改(比如,记录插入), 都要花在受影响的数据页上持有一个排它锁的时间,因为该操作需要越快越好;更糟糕的是, 写 WAL 缓冲可能还会强制创建新的日志段, 它花的时间甚至更多。通常,WAL 缓冲区应该由一个 LogFlush 请求来写和冲刷, 在大部分时候它都是发生在事务提交的时候以确保事务记录被冲刷到永久存储器上去了。在那些日志输入量比较大的系统上, LogFlush 请求可能不够频繁,这样就不能避免 LogInsert 进行写操作。在这样的系统上,我们应该通过修改配置参数 wal_buffers 的值来增加 WAL 缓冲区的数量。缺省的 WAL 缓冲区数量是 8。增加这个数值将造成对应的共享内存使用量的增加。 (要注意的是,目前我们没有什么证据表明把 wal_buffers 的设置增大超过缺省是值得的。)

    commit_delay 定义了后端在使用 LogInsert 向日志中写了一条已提交的记录之后, 在执行一次 LogFlush 之前休眠的毫秒数。 这样的延迟可以允许其它的后端把它们提交的记录追加到日志中,这样就可以用一次日志同步把所有日志冲刷到日志中。 如果没有打开fsync或者当前少于 commit_siblings 个其它后端处于活跃事务状态的时候则不会发生休眠; 这样就避免了在其它事务一时半会不会提交的情况下睡眠。 请注意在大多数平台上,休眠要求的分辩率是十毫秒, 所以任何介于 1 和 10000 微秒之间的非零 commit_delay 的作用都是一样的。 适用这些参数的比较好的数值还不太清楚;我们鼓励你多做试验。

    wal_sync_method 参数决定PostgreSQL 如何请求内核强制将 WAL 更新输出到磁盘。只要满足可靠性,那么所有选项应该都是一样的,但是哪个最快则可能和平台密切相关。 请注意如果你关闭了 fsync,那么这个参数就无所谓了。

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

 

    在版本 7.1 以后,WAL 是自动打开的。 除了要求一些磁盘空间存放 WAL日志以及一些必要的调节以外

    WAL 日志存放在数据目录的 pg_xlog 目录里,它是作为一个文件段的集合存储的,通常每个段 16 MB 大。 每个段分割成多个页,通常 8K 大。日志记录头在 access/xlog.h 里描述;日志内容取决于它记录的事件的类型。 段文件的名字是递增自然数,从 000000010000000000000000 开始。目前这些数字不能循环使用, 不过要把所有可用的数字都用光也需要非常长的时间。

    WAL 的缓冲区和控制结构在共享内存里, 并且由后端操纵;它们是用轻量的锁保护的。对共享内存的需求由缓冲区数量决定。 缺省的 WAL 缓冲区大小是 8 个 8 KB 的缓冲区,也就是 64KB。

    日志位于和主数据库文件不同的另外一个磁盘上会比较好。 你可以通过把pg_xlog目录移动到另外一个位置( postmaster 当然得关闭), 然后在 $PGDATA 里原来的位置创建一个指向新位置的符号链接来实现。

    WAL 的目的是确保在数据库记录被修改之前, 先写了日志,但是这个目的有可能被那些向内核谎报成功写的磁盘驱动器破坏, 这时候,它们实际上只是缓冲了数据而并未把数据存储到磁盘上。 这种情况下的电源失效仍然可能导致不可恢复的数据崩溃; 管理员应该确保保存 PostgreSQL 的日志文件的磁盘不会做这种虚假汇报。

    在完成一个检查点并且日志文件冲刷了之后,检查点的位置保存在了文件 pg_control 里。因此在需要做恢复的时候, 后端首先读取 pg_control 和检查点记录; 然后它通过从检查点记录里标识的日志位置开始向前扫描执行 REDO 操作。 因为数据页的所有内容都保存在检查点之后的第一个页面修改的日志里, 所以自检查点以来的所有变化都将被恢复到一个一致的状态。

    但是为了处理 pg_control 可能的损坏, 我们实际上应该实现对现存的日志段的反向读取顺序 -- 从最新到最老 -- 这样才能找到最后的检查点。这些还没有实现。 pg_control 很小(比一个磁盘页小),因此它出现只写了一部分的问题的概率几乎为零, 到目前为止,我们还没有看到只是说不能读取 pg_control 自身的错误。 因此,尽管这在理论上是一个薄弱环节,但是 pg_control 看起来似乎并不是实际会发生的问题。

0
相关文章