23.3. 在线备份以及即时恢复(PITR)

在任何时候,PostgreSQL 都在集群的数据目录的 pg_xlog/ 子目录里维护着一套预写日志(WAL)。 这些日志记录着每一次对数据库的数据文件的修改的细节。这些日志存在是为了防止崩溃:如果系统崩溃, 数据库可以通过"重放"上次检查点以来的日志记录以恢复数据库的完整性。 但是,日志的存在让它还可以用于第三种备份数据库的策略:我们可以组合文件系统备份与 WAL 文件的备份。 如果需要恢复,我们就恢复备份,然后重放备份了的WAL文件,把备份恢复到当前的时间。 这个方法对管理员来说,明显比以前的方法更复杂,但是有非常明显的优势:

和简单的文件系统备份技术一样,这个方法只能支持整个数据库集群的恢复,而不是一个子集。 同样,它还要求大量的归档存储:基础备份量可能很大,而且忙碌的系统将生成许多兆需要备份的的 WAL 流量。 但是,它仍然时在需要高可靠性的场合下的最好的备份技术。

要想从在线备份中成功恢复,你需要一套连续的 WAL 归档文件,它们最远回朔到你开始备份的时刻。 因此,要想开始备份,你应该在开始第一次基础备份之前设置并测试你的步骤。 根据我们讨论过的归档 WAL 文件的机制。

23.3.1. 设置 WAL 归档

抽象来看,一个运行着的 PostgreSQL 系统生成一个无限长的 WAL 日志序列。 系统物理上把这个序列分隔成 WAL段文件,通常一块时 16M 字节大 (在制作 PostgreSQL 的时候可以改变其大小)。 这些段文件的名字是数值命名的,这些数值反映他们在抽取出来的 WAL 序列中的位置。 在不适用 WAL 归档的时候,系统通常只是创建几个段文件然后"循环"使用它们, 方法是把不再使用的段文件的名字重命名为更高的段编号。 系统假设那些内容比前一次检查点更老的段文件是没用的了,然后就可以循环利用。

在归档 WAL 数据的时候,我们希望在每个段文件填充满之后捕获之, 并且把这些数据在段文件被循环利用之前保存在某处。根据应用以及可用的硬件的不同, 我们可以有许多不同的方法"把数据保存在某处": 我们可以把段文件拷贝到一个 NFS 装配的目录,把它们放到另外一台机器上, 或者把它们写入磁带机里(需要保证你有办法把文件恢复为原名), 或者把它们打成包,烧录到 CD 里,或者是其它的什么方法。 为了给数据库管理员提供最大可能性的灵活性,PostgreSQL 试图不对如何归档做任何假设。取而代之的是,PostgreSQL 让管理员声明一个 shell 命令执行来拷贝一个完整的段文件到它需要去的地方。 该命令可以简单得就是一个 cp,或者它可以调用一个复杂的 shell 脚本 — 所有都由管理员决定。

所使用的 shell 命令由配置参数 archive_command 声明, 它实际上总是放在 postgresql.conf 文件里的。 在这个字串里,任何 %p 都被要归档的文件的绝对路径代替,而任何 %f 只是被文件名代替。 如果你需要在命令里嵌入一个真正的 %,写 %%。 最简单的有用命令是类似下面这样的

archive_command = 'cp -i %p /mnt/server/archivedir/%f </dev/null'

它将把 WAL 段拷贝到目录 /mnt/server/archivedir。 这个只是一个例子,并非我们建议的方法,可能不能在所有系统上都正确运行。

归档命令将在运行 PostgreSQL 服务器的同一个用户的权限下执行。 因此被归档的 WAL 文件实际上包含你的数据库里的所有东西,所以你应该确保自己的归档数据不会被别人窥探; 比如,归档到一个没有组或者全局读权限的目录里。

有一点很重要:当且仅当归档命令成功时,它才返回零。在得到一个零值结果之后, PostgreSQL 将假设该 WAL 段文件已经成功归档, 因此它稍后将被删除或者被新的数据覆盖。但是,一个非零值告诉 PostgreSQL 该文件没有被归档; 因此它会周期性的重试直到成功。

归档命令通常应该设计成拒绝覆盖已经存在的归档文件。这是一个非常重要的安全特性, 可以在管理员操作失误(比如把两个不同的服务器的输出发送到同一个归档目录)的时候保持你的归档的完整性。 我们建议你首先要测试你准备使用到归档命令,以保证它实际上不会覆盖现有的文件, 并且在这种情况下它返回非零状态。 我们发现,在这方面, cp -i 在某些平台上是正确的,而在其它平台上是不正确的。 如果选定的命令本身并不能正确处理这个问题,你应该增加一个命令预先探测归档文件是否存在。 比如,类似下面的东西。

archive_command = 'test ! -f .../%f && cp %p .../%f'

在几乎所有的 Unix 变种上都工作正确。

在设计你的归档环境都时候,请考虑一下如果归档命令不停失败会发生什么情况, 因为有些方面要求操作者的干涉,或者是归档空间不够了。 比如,如果你往磁带机上写,但是没有自动换带机,那么就有可能发生这种情况; 如果磁带满了,那就除非换磁带,否则啥事也做不了。 你应该确保人和错误条件或者人和要求操作员干涉带错误都会正确报告, 这样才能迅速解决这些问题。否则 pg_xlog/ 目录会不停地填充 WAL 段文件, 直到问题解决。

归档命令的速度并不要紧,只要它能跟上你的服务器生成 WAL 数据的平均速度即可。 即使归档进程落在了后面一点,正常的操作也会继续进行。 如果归档进程慢很多,就会增加灾难发生的时候丢失的数据量。 同时也意味着 pg_xlog/ 目录包含大量未归档的日志段文件, 并且可能最后超出了磁盘空间。我们建议你监控归档进程,确保它是按照你的意识运转的。

如果你关心能够恢复到当前即时的状态,你可能需要采取几个额外的步骤以确保当前的, 部分填充的 WAL 段也拷贝到了某些地方。这条对于生成很少 WAL 流量的服务器 (或者在运行中有松弛阶段的)特别重要,因为在一个 WAL 段文件完全填充满进而可以归档之前, 可能需要很长时间。一个处理这些的可能的方法是设置一个 cron 作业, 周期性(比如每分钟一次)地标识当前 WAL 段文件然后把它们保存到某个安全的地方。 归档的 WAL 段和保存的当前段就足够保证你可以总是恢复到当前时间的一分钟之内。 这个行为目前还不是内置于 PostgreSQL 的,因为我们不想把 archive_command 的定义复杂化,因为那样就要要求它跟踪成功归档但是却又有不同时刻含义的同一个 WAL 文件。 archive_command 只是用于处理那些不再改变的 WAL 段文件; 除了错误重试之外,对于任何给出的文件名他都只被调用一次。

在写自己的归档命令的时候,你应该假设被归档的文件最多 64 个字符长并且可以包含 ASCII 字母,数字,以及点的任意组合。 我们不必要记住原始的全路径(%p),但是有必要记住文件名(%f)。

请注意尽管 WAL 归档允许你回复任何对你的 PostgreSQL 数据库的数据做的修改, 在最初的基础备份之后,它还是不会回复对配置文件的修改(也就是说,postgresql.confpg_hba.confpg_ident.conf),因为这些文件都是手工编辑的,而不是通过 SQL 操作来编辑的。 所以你可能会需要把你的配置文件放在一个日常文件系统备份过程即可处理到的地方。 参阅 Section 17.2 获取如何重定位配置文件的知识。

23.3.2. 进行一次基础备份

进行基础备份的过程相当简单:

  1. 确保 WAL 归档打开并且可以运转。

  2. 以数据库超级用户身份连接到数据库,发出命令

    SELECT pg_start_backup('label');

    这里的 label 是任意你想使用的这次备份操作的唯一标识。 (一个好习惯是使用你想把备份转储文件放置的目的地的全路径。) pg_start_backup 用你的备份的信息,在你的集群目录里,创建一个备份标签文件, 叫做 backup_label

    至于你连接到集群中的那个数据库没什么关系。你可以忽略函数返回的结果; 但是如果它报告错误,那么在继续之前处理它。

  3. 执行备份,使用任何方便的文件系统工具,比如 tar 或者 cpio。 这些操作过程中既不需要关闭数据库,也不希望关闭数据库的操作。

  4. 再次以数据库超级用户身份连接数据库,然后发出命令

    SELECT pg_stop_backup();

    它应该返回成功。

  5. 只要在备份过程中使用的 WAL 段文件作为正常数据库活动的一部分备份完毕, 你的备份工作就完成了。

有些你想用的工具在拷贝文件的时候,如果它们正拷贝的文件改变了,就会发出警告或者错误信息。 在进行一个活跃数据库的基础备份的时候,这种情况是正常的,不是错误; 因此你需要保证你可以区别这种报怨和真正的错误。 比如,有些版本的 rsync 会给 "vanished source files(源文件消失)" 发出独立的退出码,你可以写一个驱动脚本接受这样的退出码,把它当作非错误类型。 还有,有些版本的 GNU tar 人为在 tar 正在处理某文件的时候改文件改变了是一个错误。而且看来好像除了手工检查 tar 的消息之外,没有什么很方便的办法来区别这类错误。因此 GNU tar 并不是做基础备份的最好的工具。

我们不需要太关心在 pg_start_backup 和开始实际的备份之间开销的时间, 也不需要太关心备份结束和 pg_stop_backup 之间的时间; 几分钟的延迟不会搞砸事情。不过,你必须确保这些操作是按顺序执行的而不是重叠执行的。

要保证你的备份转储包括所有数据库集群目录里的文件(比如,/usr/local/pgsql/data)。 如果你在使用并未放置在这个目录里的表空间,也要小心地包含它们 (并且要确保你的备份转储归档符号连接是符号连接,否则,恢复会把你的表空间搞乱)。

不过,你可以在备份转储文件里省略集群目录里的 pg_xlog/ 子目录。 这个略微复杂些的动作是值得的,因为它减少了恢复的时候的错误。 如果 pg_xlog/ 是一个指向集群目录之外的一个符号连接,那么这件事情很容易处理, 出于性能考虑的时候经常这么做。

要使用这个备份,你需要保存所有备份开始以及之后的 WAL 段文件。 为了帮助你实现这个任务,pg_stop_backup 函数创建一个备份历史文件, 它马上存储到 WAL 归档区域。这个文件的名字是以你在使用备份的时候需要的第一个 WAL 段文件的名字命名的。 比如,如果开始 WAL 文件是 0000000100001234000055CD,那么备份历史文件将命名为类似 0000000100001234000055CD.007C9330.backup 这样的东西。 (这个文件名的第二部分表示在该 WAL 文件里面的准确位置,通常可以被忽略。) 一旦你安全地把这些日志段文件归了档,那么你就可以删除所有那些数值名字在这个文件前面的归档的 WAL 段。 文件系统备份不再需要它们了。请注意,只有写完整的 WAL 段才会被备份,所以, 运行 pg_stop_backup 和完成备份保证文件系统备份一致性所需要的所有 WAL 段文件之间有一些时延。

备份历史文件只是一个小的文本文件。它包含你给予 pg_start_backup 的标签字串, 以及备份的起始时间和终止时间。如果你使用这个标签来表示转储文件放在哪里, 则在需要的时候,归档的历史文件就足够告诉你转储文件存放在哪里了。

因为你必须保留直到你最后一次基础备份的所有归档的 WAL 文件, 那么两次基础备份之间的间隔通常是根据你想在归档 WAL 文件上花多少存储空间来定的。 你还应该考虑你准备在恢复上花多少时间,如果需要恢复的话 — 系统将需要重放所有那些段, 而如果最后一次基础备份以来,时间已经很长了,那么那些动作可能会花掉好些时间。

还有一件事值得一提,那就是 pg_start_backup 函数在数据库集群目录里创建了一个叫 backup_label 的文件,它被 pg_stop_backup 删除。 这个文件当然也会作为你的备份转储文件的一部分归档。这个备份标签文件包含你给予 pg_start_backup 的标签字串, 以及 pg_start_backup 运行的时刻,以及起始 WAL 文件的名字。 如果有混淆,那么我们可以看看备份转储文件里面然后判断转储文件来自那个备份会话。

我们还可以在 postmaster 停止的时候制作一个备份转储。 在这种条件下,很明显你不能使用 pg_start_backup 或者 pg_stop_backup, 并且因此你必须靠自己的手段来跟踪备份转储文件都是那些,以及相关的 WAL 文件最远走到哪里。 通常使用上面的在线备份步骤更好些。

23.3.3. 从在线备份中恢复

好,最糟糕的事情发生了,现在你需要从备份中恢复。下面是步骤:

  1. 停止 postmaster,如果它还在运行的话。

  2. 如果你还有足够的空间,把整个集群数据目录和所有表空间拷贝到一个临时位置, 以防万一你之后还需要它们。请注意这个预防措施要求你在系统里又足够的剩余空间来现有库的保持两份拷贝。 如果你没有足够的空间,那么你至少需要把集群数据目录的 pg_xlog 子目录的内容拷贝到安全的地方, 因为它们可能包含系统宕掉的时候还没有归档的日志。

  3. 然后清理掉所有在该集群数据目录里的现存文件, 以及所有你使用的表空间里根目录下的现存文件。

  4. 从你的备份转储中恢复数据库文件。要小心用正确的所有者(数据库系统用户,而不是 root!)和权限恢复它们。 如果你使用了表空间,你可能需要核实在 pg_tblspc/ 里的符号连接都得到正确恢复。

  5. 删除任何目前还在 pg_xlog/ 里的文件;这些文件来自备份转储,因此它们可能比目前的老。 如果你就根本没有归档 pg_xlog/,那么重建之,要注意也要重建子目录 pg_xlog/archive_status/

  6. 如果你有在步骤 2 里面保存的 WAL 段文件,那么把它们拷贝到 pg_xlog/。 (最好是拷贝它们,而不是把它们移动回来,这样即使发生了糟糕的事情,你需要重启的时候, 你也依然拥有未修改的文件。)

  7. 在集群数据目录里创建一个恢复命令文件 recovery.conf(参阅 Recovery Settings)。 你可能还需要临时修改 pg_hba.conf 以避免普通用户连接,直到你确信恢复已经正常了为止。

  8. 启动 postmaster。postmaster 将进入恢复模式并且继续读取它需要的归档的 WAL 文件。 在恢复过程完成后,postmaster 将把 recovery.conf 改名为 recovery.done (以避免不小心因后面的崩溃再次进入恢复模式)然后开始正常的数据库操作。

  9. 检查数据库的内容以确保你已经恢复到你期望的位置。 如果还没有,回到步骤 1。如果全部正常,则恢复 pg_hba.conf 成正常状态,允许你的用户登录。

所有这些操作的关键部分时设置一个恢复命令文件, 这个文件描述你希望如何恢复以及恢复应该走到哪里。 你可以使用 recovery.conf.sample(通常安装在安装目录的 share/ 子目录里)作为原型。 你必须在 recovery.conf 里面声明的一个东西是 restore_command, 它告诉系统如何拿回归档的 WAL 文件段。类似 archive_command, 这个是一个脚本命令字串。它可以包含 %f,这个变量会被需要的日志文件名替换, 以及 %p,它会被要拷贝去的日志文件的绝对路径代替。 如果需要在命令里替换真正的 %,写 %%。 最简单的有用命令是类似下面的东西

restore_command = 'cp /mnt/server/archivedir/%f %p'

这个命令将把以前归档的 WAL 段从目录 /mnt/server/archivedir 拷贝过来。 你当然可以使用某些更复杂的东西,甚至是一个要求操作者装配合适的磁带的 shell 脚本。

重要的一点是:该命令在失败的时候返回非零值。如果日志文件没有出现在规档中,那么该系统询问该命令; 在问到的时候,它必须返回非零。这个不是错误条件。还要注意 %p 路径的基础名将和 %f 不一样; 不要认为它们是可以互换的。

在归档中找不到的 WAL 段将被认为在 pg_xlog/ 里;这样就允许使用最近没有归档的段。 但是在归档中的段将比 pg_xlog/ 中的优先。在检索归档的文件的时候,系统将不会覆盖现有的 pg_xlog/ 内容。

通常,恢复将处理所有可用的 WAL 段,因此把数据库恢复到当前时间(或者是在所给出的可用 WAL 段数目的情况下, 我们能走到的最近的地方)。但是如果你想恢复到某些以前的时刻点(比如,就在菜鸟 DBA 删除你的主要事务表之前), 那么只需要在 recovery.conf 里声明要求的停止点。你可以通过日期/时间来声明, 也可以通过特定事务 ID 的结束来声明这个停止点,我们叫做"恢复目标"。 在我们写到这些的时候,只有日期/时间选项比较有用, 因为我们没有工具来帮助你精确地标识应该使用哪个事务 ID。

注意: 请注意停止点必须在备份的终止时间之后(也就是,pg_stop_backup 的时间)。 你无法使用一个基础备份恢复到备份正在进行中的某个时刻。 (要想恢复到该时刻,你必须回到你以前的基础备份,然后从那个位置向前滚动。)

23.3.3.1. 恢复设置

这些设置只能在 recovery.conf 里面使用,并且只是在恢复的过程中起作用。 在任何之后的恢复中,你必须重新设置他们。恢复过程开始后,它们的值无法改变。

restore_command (string)

执行检索归档 WAL 文件段序列的 shell 命令。这个参数是必须的。 字串中的任何 %f 都被从归档中检索出来的文件名替换, 而任何 %p 都被替换为拷贝过去的服务器上的绝对路径。 需要在命令里嵌入真正的 % 字符时,写 %%

有一点很重要,那就是这个命令只有在成功的时候才返回零。 系统向这条命令询问没有在归档里出现的文件名; 在这种情况下,它必须返回非零。比如:

restore_command = 'cp /mnt/server/archivedir/%f "%p"'
restore_command = 'copy /mnt/server/archivedir/%f "%p"'  # Windows

recovery_target_time (timestamp)

这个参数声明恢复执行到达的时间戳。最多可以声明一个 recovery_target_timerecovery_target_xid。缺省是恢复到 WAL 日志的结尾。 精确的停止点也受 recovery_target_inclusive 影响。

recovery_target_xid (string)

这个参数声明恢复将到达的事务 ID。要注意的是,尽管事务 ID 在事务开始的时候是认为顺序的, 但是事务可以以不同的数值顺序完成。将要恢复的事务是那些在声明的这个事务之前(可以选择包括它提交的时候的)提交的。 最多可以声明一个 recovery_target_xidrecovery_target_time。 缺省是恢复到 WAL 日志的结尾。精确的停止点也受 recovery_target_inclusive 影响。

recovery_target_inclusive (boolean)

声明我们是否在恢复目标之后(true),还是正好在其之前(false)停止。 适用于 recovery_target_timerecovery_target_xid, 不管声明的是哪个。它分别表示具有准确的提交时间或者 ID 的那些(个)事务,是否将包含在恢复之中。 缺省是 true

recovery_target_timeline (string)

声明恢复到一个特定的时间线。缺省是恢复到进行基础备份时的当时的当前时间线上。 只是在复杂的重新恢复的情况下,你才需要设置这个参数,也就是在你需要恢复到一个本身是在即时恢复之后到达的状态下, 才需要这么做。 参阅 Section 23.3.4 进行讨论。

23.3.4. 时间线

能够把数据库恢复到以前的某个时间点的能力导致了一些类似科幻小说里的时间跟踪和并行宇宙这样的复杂情况。 在数据库的最初的历史里,可能你在周二下午 5:15 删除掉了一个非常关键的表。 然后有条不紊地拿出备份,恢复到周二晚上 5:14 即时备份。在这个数据库宇宙的历史里, 你从来没有删除过那个表。但是假如你后来认识到这么干并非绝好的主意,并且想回到最初的历史中的稍后的点。 你没法这么干,因为在数据库运行的时候,它覆盖了一些 WAL 段文件的序列,这些序列就是在你希望回去的区间里的。 因此你的确需要区分在你从那些原始数据库历史生成的 WAL 中完成即时恢复之后生成的 WAL 序列。

为了处理这些问题,PostgreSQL 有个叫时间线的概念。 每次你即时恢复到一个比 WAL 序列的结尾要早的时刻,那么就创建一个新的时间线, 以表示在该次恢复之后生成的 WAL 记录。(不过,如果恢复动作一尺处理到 WAL 的结尾, 我们就不会开始一个新的时间线:我们只是扩展现有个那个。)时间线 ID 号是 WAL 段文件名的一部分, 因此新的时间线并不会覆盖以前的时间线生成的 WAL 数据。实际上我们可以归档许多不同的时间线。 虽然这些看起来像没用的特性,但它却可能常常是救命稻草。考虑一下你并不很确信应该恢复到那个时刻的情况, 这个时候你不得不做好几次试验性即时恢复然后从中找到旧历史中最好的分支。 如果没有时间线,那么这个过程可能很快就会导致无法管理的混乱。 有了时间线,你可以恢复到任意以前的状态, 包括恢复到你后来放弃的时间线分支的状态。

每当创建一个新的时间线的时候,PostgreSQL 都创建一个"时间线历史"文件, 它显示自己从那个时间线分出来,以及何时分出来的。这些历史文件是在从包含多个时间线的规党中进行恢复时, 允许系统选取正确的 WAL 段文件的必要文件。因此,它们像 WAL 段文件一样归档到 WAL 归档里。 历史文件只是很小的文本文件(不想段文件很大),所以独立地保存他们代价很小,也值得做。 如果你喜欢,你可以在历史文件里加入注释,录自己为什么设置一个时间线以及如何设置的等信息。 这样的注释会在你有厚厚一堆不同的时间线需要选择和分析的时候特别有价值。

恢复的缺省的行为时沿着与备份基础备份的同一个时间线恢复。 如果你像恢复到某些子时间线(也就是,你想回到某些本身就是在开始恢复企图之后发生的状态), 你需要在 recovery.conf 里声明目标时间线 ID。你无法恢复到比基础备份更早的时间线分支。

23.3.5. 注意

在我们写到这些的时候,在线备份技术还有几个局限。它们可能在将来的版本中修补:

还要注意,缺省的 WAL 格式占地相当大,因为它包含许多磁盘页快照。 这些磁盘页快照是设计来至迟崩溃恢复的,因为我们可能需要修补部分写入的磁盘页。 根据你的系统硬件和软件的不同,这种部分写入的危险可能是微乎其微的。 这种情况下,你可以通过使用 full_page_writes 参数,关闭磁盘页面快照, 从而大大减少归档日志的总尺寸。(在你这么做之前,阅读 Chapter 26 里面的注意和警告。) 关闭页面快照并不阻止日志使用 PITR 操作。 一个将来需要开发的功能是在full_page_wirtes打开的时候,通过删除不许要的磁盘页拷贝来压缩归档的 WAL 数据。同时,管理员可以通过尽量合理地增加检查点的时间间隔来减少包含在 WAL 里的页面快照。