如何利用 PostgreSQL 的延迟复制实现灾备

阅读数:2783 2019 年 2 月 18 日 08:00

如何利用PostgreSQL的延迟复制实现灾备

GitLab 网站的运营工作由 GitLab 基础设施团队负责,同时这也是 GitLab 目前最大的实例:拥有约 300 万用户和近 700 万个项目,是互联网上最大的单租户开源 SaaS 站点之一。

PostgreSQL 是 GitLab 网站基础设施的关键组成部分,我们采用了各种策略来提升系统弹性,抵御各种因数据丢失导致的灾难性事故。当然,事故的发生本来就是小概率事件,但我们也做好了备份和复制机制,一旦发生事故,可以从这些场景中恢复。

我们通常会存在这样一种误解:认为复制是备份数据库的一种手段。但是,在这篇文章中,我们将探讨如何通过延迟复制在意外删除数据后恢复数据:在 GitLab 网站上,用户删除了 gitlab-ce 项目的标签,与标签相关联的合并请求和问题也会丢失。

有了延迟副本,我们能够在 90 分钟内恢复数据。我们将介绍这个过程,以及延迟复制如何帮我们实现这一目标。

使用 PostgreSQL 进行时间点恢复

PostgreSQL 提供了一个内置功能,可以将数据库状态恢复到某个特定时间点,这个功能叫作时间点恢复(PITR),它所使用的机制与保持副本最新的机制是一样的:以整个数据库集群的一致快照(一种备份)为基础,再将变更序列应用数据库状态上,直到达到某个时间点。

为了将这个功能用于冷备份,我们会定期对数据库进行基础备份,并将其存储在归档中(在 GitLab,我们将归档保留 Google Cloud Storage 中)。此外,我们通过归档预写日志(WAL)来跟踪数据库状态的变化。有了这些,我们就可以执行 PITR,以便从灾难中恢复:从灾难发生之前的快照开始,应用 WAL 归档中的变更,直到达到灾难性事件发生之前。

什么是延迟复制?

延迟复制是指应用来自 WAL 的延迟变更。也就是说,一个在物理时间 X 提交的事务只能在时间 X + d 的副本上可见(d 为延迟时间)。

PostgreSQL 提供了两种方法用来设置数据库的物理副本:归档恢复和流式复制。归档恢复基本上与 PITR 一样,不同点在于它是以持续的方式进行的:我们不断从 WAL 归档中获取变更,并以持续的方式将它们应用于副本状态。而流式复制直接从上游数据库主机获取 WAL 流。我们更喜欢使用归档恢复,因为它更易于管理,并提供了能够跟上生产集群步伐的性能。

如何配置延迟归档恢复

归档恢复的配置主要是在 recovery.conf 中,例如:

复制代码
standby_mode = 'on'
restore_command = '/usr/bin/envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-fetch -p 4 "%f" "%p"'
recovery_min_apply_delay = '8h'
recovery_target_timeline = 'latest'

这样就配置了一个具有归档恢复功能的延迟副本。它使用 wal-e( https://github.com/wal-e/wal-e )从归档中获取 WAL 段(restore_command),并将变更的应用延迟 8 小时(recovery_min_apply_delay)。副本将遵循归档中存在的任何时间线转换,例如,由集群故障转移(recovery_target_timeline)引起的时间线转换。

可以使用 recovery_min_apply_delay 来配置流式复制。但是,有一些关于复制插槽、热备用反馈以及其他需要注意的问题。在我们的例子中,我们通过从 WAL 归档复制而不是使用流式复制来避免这些问题。

需要注意的是,recovery_min_apply_delay 是在 PostgreSQL 9.3 中引入的。但是,在以前的版本中,延迟副本通常使用恢复管理函数(pg_xlog_replay_pause()、pg_xlog_replay_resume())的组合或在延迟期间从归档中隐藏 WAL 段来实现。

PostgreSQL 是如何实现的?

研究 PostgreSQL 如何实现延迟恢复是一件非常有趣的事情。那么让我们来看看下面的 recoveryApplyDelay(XlogReaderState)。从 WAL 中读取的每个记录时都会调用这个方法。

复制代码
static bool
recoveryApplyDelay(XLogReaderState *record)
{
uint8 xact_info;
TimestampTz xtime;
long secs;
int microsecs;
/* nothing to do if no delay configured */
if (recovery_min_apply_delay <= 0)
return false;
/* no delay is applied on a database not yet consistent */
if (!reachedConsistency)
return false;
/*
* Is it a COMMIT record?
*
* We deliberately choose not to delay aborts since they have no effect on
* MVCC. We already allow replay of records that don't have a timestamp,
* so there is already opportunity for issues caused by early conflicts on
* standbys.
*/
if (XLogRecGetRmid(record) != RM_XACT_ID)
return false;
xact_info = XLogRecGetInfo(record) & XLOG_XACT_OPMASK;
if (xact_info != XLOG_XACT_COMMIT &&
xact_info != XLOG_XACT_COMMIT_PREPARED)
return false;
if (!getRecordTimestamp(record, &xtime))
return false;
recoveryDelayUntilTime =
TimestampTzPlusMilliseconds(xtime, recovery_min_apply_delay);
/*
* Exit without arming the latch if it's already past time to apply this
* record
*/
TimestampDifference(GetCurrentTimestamp(), recoveryDelayUntilTime,
&secs, &microsecs);
if (secs <= 0 && microsecs <= 0)
return false;
while (true)
{
// Shortened:
// Use WaitLatch until we reached recoveryDelayUntilTime
// and then
break;
}
return true;
}

这里的重点是,延迟是基于与事务的提交时间戳(xtime)一起记录的物理时间。我们还可以看到,延迟只被应用于提交记录,不会被应用于其他类型的记录:直接应用数据变更,但相应的提交会延迟,因此这些变更只在配置的延迟后才可见。

如何使用延迟副本来恢复数据

假设我们有一个生产数据库集群和一个具有 8 小时延迟的副本。我们如何使用它来恢复数据?让我们来看看在意外删除标签的情况下如何进行恢复。

在事件发生之后,我们马上在延迟副本上暂停归档恢复:

复制代码
SELECT pg_xlog_replay_pause();

暂停副本可以避免副本重放 DELETE 查询的危险。如果你需要更多时间进行诊断,这个操作就非常有用。

恢复方法是让延迟副本赶在 DELETE 查询发生之前。在我们的例子中,我们大致知道 DELETE 查询的物理时间。我们从 recovery.conf 中删除了 recovery_min_apply_delay,并添加了 recovery_target_time。这样可以让副本尽可能快地赶上(没有延迟),直到达到某个时间点:

复制代码
recovery_target_time = '2018-10-12 09:25:00+00'

在使用物理时间戳进行操作时,最好可以为错误留一点余地。显然,余地越大,数据丢失就越多。如果副本恢复超出实际的事件时间戳,它也会重放 DELETE 查询,我们将不得不重新开始(或者更糟:使用冷备份来执行 PITR)。

重新启动延迟的 Postgres 实例后,我们看到很多 WAL 段被重放,直到达到目标事务时间。为了了解这个阶段的进度,我们可以使用这个查询:

复制代码
SELECT
-- current location in WAL
pg_last_xlog_replay_location(),
-- current transaction timestamp (state of the replica)
pg_last_xact_replay_timestamp(),
-- current physical time
now(),
-- the amount of time still to be applied until recovery_target_time has been reached
'2018-10-12 09:25:00+00'::timestamptz - pg_last_xact_replay_timestamp() as delay;

当重放时间戳不再发生变化时,我们就知道恢复已经完成了。我们可以考虑设置 recovery_target_action,以便在重放完成后关闭、提升或暂停实例(默认为暂停)。

数据库现在处于灾难性查询之前的状态。我们可以开始导出数据或以其他方式使用数据库。在我们的示例中,我们导出了有关已删除标签的信息及其与问题和合并请求的关联,并将数据导入生产数据库。在其他数据丢失更严重的情况下,可以将副本提升并继续作为主要副本使用。然而,这意味着我们会丢失在恢复到某个时间点之后写入数据库的任何数据。

使用物理时间戳进行目标恢复的替代方法是使用事务 ID。记录事务 ID 是一种很好的做法,例如,使用 log_statements ='ddl’可以记录 DDL 语句(如 DROP TABLE)。如果我们手头有一个事务 ID,可以使用 recovery_target_xid 来重放到 DELETE 查询之前的事务。

对于延迟复本,恢复正常的方法很简单:恢复 recovery.conf 的改动,并重新启动 Postgres。过了一会儿,副本将再次显示 8 小时的延迟——为未来的灾难做好准备。

归档恢复的好处

与使用冷备份相比,延迟副本的主要好处是它消除了从归档中恢复完整快照的步骤。这可能需要数小时时间,具体取决于网络和存储速度。在我们的例子中,从归档中获取完整的约 2TB 备份大约需要五个小时。除此之外,我们必须应用 24 小时的 WAL 才能恢复到理想的状态(在最坏的情况下)。

相比冷备份,使用延迟副本的两个好处是:

  1. 无需从归档中获取完整的备份;
  2. 我们有一个 8 个小时的 WAL 固定窗口,需要重放才能赶上。

除此之外,我们还不断测试我们从 WAL 归档执行 PITR 的能力,并通过监控延迟副本的滞后来快速实现 WAL 归档损坏或其他与 WAL 相关的问题。

在我们的示例中,完成恢复需要 50 分钟时间,并转换为每小时 110GB WAL 的恢复速率(当时归档仍在 AWS S3 上)。在工作开始 90 分钟后,事故得到缓解,数据得到恢复。

总结:延迟复制什么时候有用,什么时候没有用

延迟复制可以用作从意外数据丢失中恢复数据的第一手段,并且非常适用于在配置的延迟时间内可以知道引起丢失的事件的情况。

让我们明确一点:复制不是一种备份机制。

备份和复制是两种具有不同目的的机制:冷备份对于从灾难中恢复来说很有用,例如意外的 DELETE 或 DROP TABLE 事件。在这种情况下,我们利用冷存储中的备份来恢复表或整个数据库的早期状态。另一方面,DROP TABLE 几乎可以立即复制到正在运行的集群中的所有副本——因此正常的复制对于从这个场景中恢复是没有用的。相反,复制的目的主要是保护数据库可用性,防止单个数据库出现故障,以及用于分发负载。

即使存在延迟副本,在某些情况下我们确实冷备份,并把它存储在安全的地方:数据中心故障、静默损坏或其他不可见的事件,都需要依赖冷备份。如果只进行复制,我们可能没有那么多的好运。

英文原文: https://about.gitlab.com/2019/02/13/delayed-replication-for-disaster-recovery-with-postgresql/

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论