Table of Contents
11.1. 一般性描述
11.1.1. 从 WAL 中获取的直接好处
11.1.2. 更多好处
11.2. 实现
11.3. WAL 配置
作者: Vadim Mikheev 和 Oliver Elphick
11.1. 一般性描述
预写式日志 (WAL) 是一种实现事务日志的标准方法.有关它的详细描述可以在 大多数(如果不是全部的话)有关事务处理的书中找到. 简而言之,WAL 的中心思想是对数据文件 的修改(它们是表和索引的载体)必须是只能发生在这些修改已经 记录了日志之后 -- 也就是说,在日志记录冲刷到永久存储器之后. 如果我们遵循这个过程,那么我们就不需要在每次事务提交的时候 都把数据页冲刷到磁盘,因为我们知道在出现崩溃的情况下, 我们可以用日志来恢复数据库:任何尚未附加到数据页的记录 都将先从日志记录中重做(这叫向前滚动恢复,也叫做 REDO) 然后那些未提交的事务做的修改将被从数据页中删除 (这叫向后滚动恢复 - UNDO).
11.1.1. 从 WAL 中获取的直接好处
使用 WAL 的第一个明显的好处就是显著地 减少了磁盘写的次数.因为在日志提交的时候只有日志文件需要冲刷到磁盘; 在多用户环境里,许多事务的提交可以用日志文件的一次 fsync() 来完成.而且,日志文件是顺序写的, 因此同步日志的开销要远比同步数据页的开销要小.
另外一个好处就是数据页的完整性.实际情况是,在 WAL 之前,PostgreSQL 从来不能保证 在崩溃的情况下数据页的完整性.在 WAL之前,在写的过程中的任何崩溃都可能导致:
1.
索引记录指向一个不存在的表的行
2.
索引记录在分裂操作中丢失
3.
完全崩溃了的表和索引页的内容,因为数据页只写了一部分
索引的问题(问题 1 和 2)可能已经通过额外的 fsync() 调用修补好了,但是如果没有 WAL,那么没有很明显的 处理第三种情况的方法;WAL 在日志里保存整个 数据页的内容 -- 如果那些内容在崩溃后的恢复中需要确保数据页的完整性的话.
11.1.2. 更多好处
UNDO 操作还没有实现。 这就意味着由退出的事务做的修改将仍然占据磁盘空间, 因此我们仍然需要一个永久的 pg_clog 文件 保存事务的状态,因为我们不能回收事务标识符.一旦实现了 UNDO, 那么 pg_clog 就不再要求是永久的了; 我们就有可能在关闭的时候删除 pg_clog. (不过,这方面的紧迫性已经随着我们对 pg_clog 采取分段存储的方法而降低了 --- 我们不再需要永久保留 pg_clog 记录.)
有了 UNDO,我们还可能实现 savepoints, 这样就允许非法事务操作的部分回卷(因为误敲了命令导致的分析器错误, 插入了重复的主键/唯一键字等等)同时还能够继续或提交该事务在 发生错误之前的合法操作.目前,任何错误都将使整个事务成为非法并且 要求事务退出.
WAL 还提供了数据库在线备份和恢复 (backup and restore (BAR))的新方法. 要使用这个方法,我们可能要经常性地把数据文件保存到另外一个磁盘, 磁带或者另外一台主机并且还要备份 WAL 日志文件. 那么数据库文件拷贝和日志归档文件就可以用于象灾难恢复中那样 恢复数据.每次做完新数据库文件以后,这个老的日志文件就可以删除了. 实现这个设施可能需要记录数据文件和索引创建和删除的日志; 同时还需要开发一种方法来拷贝数据文件(操作系统拷贝命令是不合适的).
认识这些事情的好处的一个困难是它们要求在相当可观的时间段内 保存 WAL 日志(也就是说,如果需要事务的 UNDO, 那么和可能的最长的事务的时间一样长).目前的 WAL 格式的体积相当大,因为它包括多个磁盘页的镜像. 目前这还不是一个严重的问题,因为这些日志只需要保留一到两个 检查点的时间间隔;但是为了实现这些东西,以后我们可能需要某种压缩的 WAL 格式.
11.2. 实现
在版本 7.1 以后,WAL 是自动打开的. 除了要求一些额外的磁盘空间存放 WAL 日志以及一些必要的调节以外(参阅Section 11.3) ,对管理员没有什么其他要求,
WAL 日志存放在 $PGDATA/pg_xlog 目录里,它是作为一个文件段的集合存储的,每个段 16 MB 大. 每个段分割成 8 KB 的页.日志记录头在 access/xlog.h 里描述;日志内容取决于 它记录的事件的类型.段文件的名字是自然数,从 0000000000000000开始.目前这些数字不能循环使用, 不过要把所有可用的数字都用光也需要非常长的时间.
WAL 的缓冲区和控制结构在共享内存里, 并且由后端操纵;它们是用轻量的锁保护的.对共享内存的需求 由缓冲区数量决定.缺省的 WAL 缓冲区大小是 8 个 8 KB 的缓冲区,也就是 64KB.
日志位于和主数据库文件不同的另外一个磁盘上会比较好. 你可以通过把pg_xlog目录移动到另外 一个位置( postmaster 当然得关闭),然后在 $PGDATA 里原来的位置创建一个 指向新位置的符号链接来实现.
WAL 的目的是确保在数据库记录被修改之前, 先写了日志,但是这个目的有可能被那些向内核谎报成功写的 磁盘驱动器破坏,这时候,它们实际上只是缓冲了数据而并未把数据 存储到磁盘上.这种情况下的电源失效仍然可能导致不可恢复的数据崩溃; 管理员应该确保保存 PostgreSQL 的 日志文件的磁盘不会做这种虚假汇报.
11.2.1. 用 WAL 进行数据库恢复
在完成一个检查点并且日志文件冲刷了之后,检查点的位置保存在了文件 pg_control 里.因此在需要做恢复的时候, 后端首先读取 pg_control 和检查点记录; 然后它通过从检查点记录里标识的日志位置开始向前扫描. 因为数据页的所有内容都保存在检查点之后的第一个页面修改的日志里, 所以自检查点以来的所有变化都将被恢复到一个一致的状态.
用 pg_control 获取检查点位置可以加快恢复 进程的速度,但是为了处理 pg_control 可能 的损坏,我们实际上应该实现对现存的日志段的反向读取顺序 -- 从最新到最老 -- 这样才能找到最后的检查点.这些还没有实现.
11.3. WAL 配置
有几个与 WAL 相关的参数会影响数据库性能. 本节讨论它们的使用.参阅 Section 3.4 获取配置参数的细节.
检查点(Checkpoints)是事务的顺序的点, 我们保证在该点之前的所有日志信息都更新到数据文件中去了. 在检查点时,所有脏数据页都冲刷到磁盘并且向日志文件中写入一条 特殊的检查点记录.结果是,在发生崩溃的时候,恢复器就知道 应该从日志中的哪条记录(称做 redo 记录)开始做 REDO 操作, 因为在该记录前的对数据文件的任何修改都已经在磁盘上了. 在完成检查点处理之后,任何在 undo 记录之前写的日志段都不再 需要,因此可以循环使用或者删除.(到基于 WAL 的 BAR (备份和恢复)实现的时候,这些日志在 循环利用或者删除之前将先归档.)
postmaster 每隔一段时间就 排生一个特殊的进程以创建下一个检查点. 每隔 CHECKPOINT_SEGMENTS 个日志段就创建一个检查点, 或者每隔 CHECKPOINT_TIMEOUT 秒创建一个. 以先到为准.缺省设置分别是 3 个段和 300 秒. 我们也可以用 SQL 命令 CHECKPOINT 强制一个检查点.
减少 CHECKPOINT_SEGMENTS 和/或 CHECKPOINT_TIMEOUT 会令检查点更频繁一些. 这样就允许更快的崩溃后恢复(因为需要重做的工作更少).不过, 我们必须在这个目地和更频繁地冲刷脏数据页所带来的额外开销之间 取得平衡.另外,为了保证数据页的一致性,在每个检查点之后的第一次 数据页的变化会导致对整个页面内容的日志记录.因此,检查点时间间隔 短了会导致输出到日志中的数据的增加,会抵销一部分缩短间隔的目标, 并且怎么着都会产生更多的磁盘 I/O.
至少会有一个 16MB 的段文件,而且通常不会超过 2 * CHECKPOINT_SEGMENTS + 1 个文件。你可以用这些信息来估计 WAL 需要的空间。 通常,如果一个旧的日志段文件不再需要了,那么它将得到循环使用(重命名为顺序的下一个可用段)。 如果由于短期的日志输出峰值,导致了超过 2 * CHECKPOINT_SEGMENTS + 1 个段文件, 那么到系统再次回到这个限制之内的时候,多于的段文件会被删除,而不是循环使用。
有两个常用的 WAL 函数: LogInsert 和 LogFlush. LogInsert 用于向共享内存中的 WAL 缓冲区里加一条新的记录.如果没有空间存放 新记录,那么LogInsert 就不得不写出 (向内核缓存里写)一些填满了的WAL缓冲. 我们可不想这样,因为 LogInsert 用于 每次数据库低层修改(比如,记录插入),都要花在受影响的数据页 上持有一个排它锁的时间,因为该操作需要越快越好;更糟糕的是, 写 WAL 缓冲可能还会强制创建新的日志段, 它花的时间甚至更多.通常,WAL 缓冲区应该由 一个 LogFlush 请求来写和冲刷, 在大部分时候它都是发生在事务提交的时候以确保事务记录被冲刷到 永久存储器上去了.在那些日志输入量比较大的系统上, LogFlush 请求可能不够频繁,这样就不能避免 WAL 缓冲区被 LogInsert 写.在这样的系统上,我们应该通过修改 postgresql.conf 的 WAL_BUFFERS 参数值来增加 WAL 缓冲区的数量.缺省的 WAL 缓冲区数量是 8. 增加这个数值将有对应的共享内存使用量的增加.
检查点是开销相当昂贵的操作,因为它们用操作系统的 sync() 调用强制所有脏的内核缓冲 刷新到硬盘上。繁忙的服务器可能会很快就把检查点段文件填满, 导致额外的检查点。如果这样的强制检查点发生的频率比 CHECKPOINT_WARNING 秒要频繁,那么将在服务器日志里输出一条信息,建议你增加 CHECKPOINT_SEGMENTS。
COMMIT_DELAY 定义了后端在使用 LogInsert 向日志中写了一条已提交 的记录之后,在执行一次 LogFlush 之前休眠 的毫秒数.这样的延迟可以允许其它的后端把它们提交的记录追加 到日志中,这样就可以用一次日志同步把所有日志冲刷到日志中. 如果没有打开fsync或者当前少于 COMMIT_SIBLINGS 个其它后端处于活跃事务状态的时候则不会发生休眠; 这样就避免了在其它事务一时半会不会提交的情况下睡眠. 请注意在大多数平台上,休眠要求的分辩率是十毫秒, 所以任何介于 1 和 10000 微秒之间的非零 COMMIT_DELAY 的作用都是一样的. 适用这些参数的比较好的数值还不太清楚;我们鼓励你多做试验.
WAL_SYNC_METHOD 参数决定PostgreSQL 如何 请求内核强制将 WAL 更新输出到磁盘.只要满足可靠性,那么 所有选项应该都是一样的,但是哪个最快则可能和平台密切相关. 请注意如果你关闭了 FSYNC,那么这个参数 就无所谓了.
把 WAL_DEBUG 参数设置为任何非零值都会导致 每次 LogInsert 和 LogFlush WAL 调用都被记录到标准错误.目前,这个非零值是多少 没有什么区别.这个选项以后可能会被更通用的机制取代.