GitLab网站的运营工作由GitLab基础设施团队负责,同时这也是GitLab目前最大的实例:拥有约300万用户和近700万个项目,是互联网上最大的单租户开源SaaS站点之一。
PostgreSQL是GitLab网站基础设施的关键组成部分,我们采用了各种策略来提升系统弹性,抵御各种因数据丢失导致的灾难性事故。当然,事故的发生本来就是小概率事件,但我们也做好了备份和复制机制,一旦发生事故,可以从这些场景中恢复。
我们通常会存在这样一种误解:认为复制是备份数据库的一种手段。但是,在这篇文章中,我们将探讨如何通过延迟复制在意外删除数据后恢复数据:在GitLab网站上,用户删除了gitlab-ce项目的标签,与标签相关联的合并请求和问题也会丢失。
有了延迟副本,我们能够在90分钟内恢复数据。我们将介绍这个过程,以及延迟复制如何帮我们实现这一目标。
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 \u0026quot;%f\u0026quot; \u0026quot;%p\u0026quot;'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如何实现延迟恢复是一件非常有趣的事情。那么让我们来看看下面的recoveryApplyDelay(XlogReaderState)。从WAL中读取的每个记录时都会调用这个方法。
static boolrecoveryApplyDelay(XLogReaderState *record){\tuint8\t\txact_info;\tTimestampTz xtime;\tlong\t\tsecs;\tint\t\t\tmicrosecs;\t/* nothing to do if no delay configured */\tif (recovery_min_apply_delay \u0026lt;= 0)\t\treturn false;\t/* no delay is applied on a database not yet consistent */\tif (!reachedConsistency)\t\treturn false;\t/*\t * Is it a COMMIT record?\t *\t * We deliberately choose not to delay aborts since they have no effect on\t * MVCC. We already allow replay of records that don't have a timestamp,\t * so there is already opportunity for issues caused by early conflicts on\t * standbys.\t */\tif (XLogRecGetRmid(record) != RM_XACT_ID)\t\treturn false;\txact_info = XLogRecGetInfo(record) \u0026amp; XLOG_XACT_OPMASK;\tif (xact_info != XLOG_XACT_COMMIT \u0026amp;\u0026amp;\t\txact_info != XLOG_XACT_COMMIT_PREPARED)\t\treturn false;\tif (!getRecordTimestamp(record, \u0026amp;xtime))\t\treturn false;\trecoveryDelayUntilTime =\t\tTimestampTzPlusMilliseconds(xtime, recovery_min_apply_delay);\t/*\t * Exit without arming the latch if it's already past time to apply this\t * record\t */\tTimestampDifference(GetCurrentTimestamp(), recoveryDelayUntilTime,\t\t\t\t\t\t\u0026amp;secs, \u0026amp;microsecs);\tif (secs \u0026lt;= 0 \u0026amp;\u0026amp; microsecs \u0026lt;= 0)\t\treturn false;\twhile (true)\t{ // Shortened: // Use WaitLatch until we reached recoveryDelayUntilTime // and then break;\t}\treturn 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才能恢复到理想的状态(在最坏的情况下)。
相比冷备份,使用延迟副本的两个好处是:
除此之外,我们还不断测试我们从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/