译者 | 王强
策划 | 万佳
在这篇博文中,我们会介绍如何在零停机时间的前提下,使用 Bucardo 将 Postgres 数据库迁移到一个新实例上。我们将介绍如何避免常见的陷阱,比如数据丢失、性能下降和数据完整性故障等。我们已成功使用这一流程将我们的 Postgres 数据库从 9.5 版迁移到 Amazon RDS 上的 12.5 版,但该流程不只适用于 RDS,也不依赖 AWS 独有的任何内容。这种迁移策略应该能适用于任何自托管或托管的 Postgres。
分 析
在本文中,我们将讨论将多个 Web 应用程序(如微服务)从一个数据库迁移到另一个的过程。现代软件架构由多个应用程序(或微服务)组成,而每个应用程序都有多个运行实例以增强扩展性。为了将你的应用程序移动到新的数据库,你必须首先确保两个数据库中的数据是同步的,并在任何给定时间点保持同步,否则你的客户端迟早会丢失数据,甚至陷入无效状态。
一个简单的解决方案是停止旧数据库的写入操作,获取快照,将其恢复到新的数据库,然后在新数据库中恢复操作。这种方案需要的停机时间太久,不适合生产环境。我们提到这一点只是为了做参考,因为这是确保你不会丢失任何数据的最简单方法,但用它的话,你可能会失去一些客户。
更现实的方法是在两个数据库之间设置一个近乎实时的双向复制,这样在理想情况下,应用程序可以同时向两者读取和写入,而不会注意到任何差异。你可以用这种方法一次一个实例地逐步移动你的应用程序,过程中不会停机,且不会影响用户。
由于我们希望应用程序能写入两个数据库,我们需要进行多主复制(multi-master replication)。在谷歌上搜索“Postgres 中的多主复制”可以找到大量解决方案,每种方案都有自己需要注意的优缺点。
我们决定继续使用 Bucardo,因为它开源、速度快,并且提供了简单的监控和冲突解决机制。
Bucardo 的工作机制
Bucardo 充当两个 Postgres 实例之间的中间人。你可以让 Bucardo 在你喜欢的任何机器上运行,只要它可以访问源数据库和目标数据库即可。安装并设置多主复制后,Bucardo 将为你选择复制的所有表添加一些额外的触发器。
你运行 Bucardo 的实例在本地使用一个单独的 Postgresql 数据库以保存同步状态,这样你就可以随意暂停和重启同步过程。当发生更改时,触发器会将所有受影响的主键添加到 Bucardo 实例的 Postgres 中的“delta”表,另一个触发器将“启动(kick)”同步。每次同步被启动时,Bucardo 将对比所有主表中每个表的受影响行并选择一个获胜者,然后将更改同步到其余数据库。选择获胜者并不简单,此时可能会发生冲突。
小心漂移
一些在线指南建议,使用 Bucardo 的正确方法是获取源数据库的快照,将其恢复到新的数据库,然后启动一个多主 Bucardo 同步。不要那样做!
如果这样做,你将丢失与当前数据库大小和写入流量成正比的数据。这是因为获取快照并恢复它需要大量时间。在这段时间里,源数据库将因为数据写入而开始漂移,并且这种漂移也必须同步以确保两个主数据库包含相同的数据。这里的问题是人们相信 Bucardo 会做某种回填,但事实证明它在这项任务上不可靠,并且可能无法同步大的漂移。你自然可以使用跨数据库对比数据的工具,确保消除偏差;但如果数据集很大,这样做会浪费大量时间,而恰恰我们追求的就是零停机时间。
此外,如果复制延迟足够大,正在进行的同步可能会被误报为漂移。
如何同步漂移
你可以启动 Bucardo 同步,并使用autokick=0标志告诉它在本地数据库中缓存所有漂移。不幸的是,虽然这个选项很关键,但它没有文档支持!这一步很关键,据我们所知唯一明确的参考资料出现在 David E. Wheeler 的这篇优秀博文中。
注意 autokick=0。这个标志确保了在记录增量时,它们不会被复制到任何地方,直到我们让 Bucardo 这样做为止。
使用这个标志,你就可以在本地缓存 Bucardo 实例中的增量,为你腾出了足够的时间来准备新数据库。这是非常关键的,尤其是对于大漂移更是如此。
如何引导新数据库
这里有两个选项。你可以从第一个数据库中获取全包快照并将其恢复到新实例,或者你可以从一个新的空数据库开始,然后分别传输用户、模式和数据(按这个顺序)。我们推荐后一种方法。原因是在对两个解决方案进行基准测试对比后,第二个的结果更干净。我们可以从头开始关闭旧用户帐户和临时表并细化用户权限。
如果你使用的是 AWS RDS,推荐的这个方案也会更快。获取快照可能需要几分钟时间,具体取决于你的数据库大小。
此外,如果你像我们一样从未加密的服务器迁移到使用静态加密的服务器,你需要获取快照、加密快照,然后将其还原到新的 RDS 实例。这样做用的时间更久,而最小化迁移时间是我们的一个关键目标。
选择性同步
在开始 Bucardo 同步前,你需要正确配置它。你需要指定两个数据库、它们的类型(主 / 副本),还有指定数据库的哪些部分应包含在同步中。你可以从一个模式(schema)中批量添加所有表,数据库有很多表的时候这个办法非常有用。
Bucardo 无法在没有主键(PK)的情况下同步表,这很正常,因为那种情况下它无法区分唯一条目。我们不得不在流程中排除一些表,这些表充当各种表迁移的缓存并且不包含 PK。一些未使用的表也被排除在外,因此我们没有将未使用的数据传输到新数据库。在 Bucardo 中很容易完成上述操作:添加所有表后,你可以移除要排除的表。
迁移用户
Bucardo 不会迁移 Postgres 用户,你需要手动转移你的用户帐户。我们为此编写了一个脚本。这个脚本会到新数据库,使用从配置服务器检索到的密码创建新用户,然后设置他们的权限。尽管你可能不会将数据存储为代码,但将用户保存为代码是一种很好的做法,这样在发生灾难时就能够恢复它们了。
迁移模式和数据
你可以使用 Postgres 及其pg_dump/pg_restore工具来传输你的模式和数据。这个步骤很简单,但有一个要点。请记住,此时我们已经启动并运行了 Bucardo 来记录漂移,因此在目标服务器上恢复数据将被解释为同步回源数据库的更改。这就是为什么我们需要启用 session_replication_role=replica标志,使用一个副本会话将数据恢复到目标 Postgres 数据库。在我们启动你的持续同步之前,我们需要禁用它。
冲 突
高可用性是零停机迁移的先决条件,它通常要求每个应用程序有多个正在运行的实例。一般来说,每个实例都应该在重新启动之前排空,因此无法在完全相同的时间点将所有实例切换到新数据库。所以总会有一个关键的——或短或长的——时间窗口,在这个窗口中同一个应用程序将同时写入两个数据库,并且在这段时间内可能会发生冲突。
冲突很少见,因为它们需要在两个数据库中进行两次写入,然后 Bucardo 才能复制这两个记录。复制时间接近于零,你可能根本不会遇到任何冲突,但这种迁移发生在关键的生产环境中,因此不能忽略它们。
想象一下,两个客户试图在同一天预订同一所房子。如果他们同时尝试这样做并且每个用户都指向不同的数据库,则可能会发生冲突。Bucardo 有一个 冲突解决机制,提供了两个基本选项:要么让 Bucardo 自动处理冲突(默认选项),要么中止同步并手动解决它们。这是迁移过程中最关键的部分,我们进一步分析一下。
如果你的表有一个自动递增的 ID 作为主键,Postgres 会自动从相应的序列中选择下一个 ID。Bucardo 也会同步序列。假设在上面的示例中,你有一个带有自动递增 ID 作为 PK 的 bookings 表,并且最新的记录 ID 是 42。这里会发生并发插入,并且在两个数据库中创建两条不同的记录,它们都以 43 作为 PK,但数据不同。如果你让 Bucardo 处理冲突,它会只保留最新的一个并删除另一个。最后你会丢失一个对你的客户来说似乎是成功的预订。你的数据库仍处于有效状态,但你会丢失数据,还没法恢复。这是一个死胡同!
在讨论解决方案之前,让我们考虑另一种情况。假设你的表使用 UUID 作为 PK。回放上面的场景,并发预订将在两个数据库中创建两个不同的记录,并具有两个不同的 PK。这次没有发生冲突。Bucardo 将成功同步两个数据库中的两条记录,但从业务角度来看你的数据仍然无效,因为你不能两次预订同一所房子。因此这里很明显,从业务角度来看数据库有效性并不能保证你的数据有效。你需要小心对待冲突的处理方式,以免你的客户遇到问题。
Bucardo 支持自定义解析策略。你可以根据业务需求制定自己的策略,但这很快就会变得过于复杂和耗时。另一种方法是创建你自己的工具来检测和解决迁移期间的数据违规问题。这并非易事:它必须根据数据的复杂程度来做设计,并且可能需要大量开发工作。
我们的解决方案是在开始迁移之前满足两个条件,来彻底避免冲突。
首先,我们努力最小化数据库之间的转换时间,以最小化冲突概率。为了做到这一点,我们会修改应用的重配置脚本以指向新的数据库,一次一个实例,但所有的不同应用会并行操作。
第二步最关键,就在我们开始将应用切换到新数据库之前,我们撤销了旧数据库中应用用户的写入权限。通过这种方式,我们可以彻底避免冲突,但代价是一定比例的数据库写入失败时间。这当然需要你的应用程序能够优雅地处理失败的数据库写入。你的应用程序执行此操作时应该能独立于任何数据库迁移活动,因为这对于生产环境来说至关重要。
下面就是最终的迁移计划:
实 现
本节将展示我们遵循的步骤,以及每个步骤对应的脚本。我们已将代码上传到这个 GitHub 存储库,下文会对代码做具体拆解分析。
准备
- 启动一个新实例(在我们的例子中是 EC2)。该指令假设你运行的是 Debian 操作系统。
- 运行 install.sh 来安装 Bucardo
- 编辑 vars.sh 以设置你的数据库和 postgres 角色密码
- 在 shell 中导出上述变量:
$sourcevars.sh
- (可选)如果你之前在源数据库中使用过 Bucardo,你可能需要运行 uninstall_bucardo.sh 来清除旧触发器。在运行之前,请查看我们根据我们的数据库生成的 uninstall.template。你需要在那里列出你所有的表。
- 你需要手动运行
$ bucardo install
才能完成本地 Bucardo 安装。
迁移
仔细看看 configure.sh 脚本。在这里,你需要编辑脚本以匹配你的迁移方案。你需要为 Bucardo 对象定义描述性名称并指定排除的表或略过此选项。在你了解脚本的作用后可以继续运行它。该脚本执行以下操作:
- 设置
.pgpass
文件和一条 Bucardo 别名命令,以避免在此过程中要求你输入密码的交互式提示中断流程 - 配置 Bucardo 数据库、herds、数据库组和同步。如果你需要进一步了解 Bucardo 对象类型,他们的文档页面中有一个 列表。
- 在新的 Postgresql 主机中初始化一个空数据库并运行此脚本创建用户。你需要编辑这个脚本来指定你的角色。密码由我们之前获取的
vars.sh
文件检索。 - 这一步只传输数据库模式,使用
pg_dump
并将其传输到新主机 - 使用本地缓存启动 Bucardo 同步
- 以压缩格式传输数据库数据。当数据传输和漂移开始堆积时,Bucardo 会将其保存在本地并在 autokick 标志更改值后重播
- 重置 autokick 标志的值以停止本地缓存,然后重新加载配置以让同步遵守新值
- 启动多主同步
现在持续同步已就位,是时候开始在新数据库中移动应用了。对我们来说,我们是更改配置服务器中的应用程序参数然后一一重新部署来完成这一步的。在这一步中,我们需要将旧数据库中的用户权限设置为只读。一旦我们应用的第一个实例连接到新数据库,我们就运行 revoke_write_access_from_old_db.sql 脚本更改旧数据库中的权限。这一步的时机非常重要。
迁移后检查
- 当你的同步运行时,你应该验证数据复制。我们使用分叉的 pgdatadiff 工具 来做到这一点。我们还进一步扩展了它,允许数据 diff 来排除表。
- 将所有应用切换到新数据库后,你可以停止 Bucardo 同步并下线它的机器。你应该再次运行 uninstall_bucardo.sh 以便从触发器清理你的新数据库。
总 结
将你的 postgresql 数据库迁移到一个新实例会面临巨大挑战。无论你选择哪种工具来实施,你要面对的挑战都是一样的:
- 传输数据
- 在两个数据库之间设置多主复制
- 从业务角度处理冲突,确保数据一致性
- 验证同步过程
- 消除停机时间以避免干扰你的客户
在本文中,我们介绍了自己是如何解决这些问题的。我们遇到的一大困难是没有这方面的在线教程,因此我们不得不随机应变,并多次迭代我们的解决方案,直到我们正确地完成任务。我们也想听听你的反馈意见,这样可以帮助我们改进流程,并帮助可能面临相同问题的其他读者。
PS:背景故事
2020 年初,我们发现我们使用了两个 Postgres9.5 实例,我们从 Blueground 的早期就一直在使用它们。2020 年 1 月,我们不得不关闭旧实例并使用新实例,因为亚马逊即将迁移到新的 SSL/TLS 证书。这次迁移中,我们丢失了不少数据,花费了几天的时间来恢复它们。问题出在我们信任 Bucardo 的自动同步机制,让它处理我们的漂移;正如前面提到的那样,它有问题并且失败了。今年我们不得不再做一次,因为 Postgres 9.5 即将 EOL 了,否则它们会被 AWS 强行升级。这次我们下定决心要注意每一个小细节。我们相信我们可以快速、可靠且无故障地达成目标,我们做到了。
为什么要升级到新实例
首先,我们需要解释为什么我们不让亚马逊在没有我们干预的情况下在线升级我们的数据库。亚马逊提供了升级流程,但与迁移到新数据库实例的方案相比,它有一些严重的缺点:
- AWSRDS 不为你提供即时回滚选项。在迁移过程中有两个实例,回滚是对我们应用的一个简单重配置,指向旧数据库。在整个过程中,这是一个非常重要的故障预防措施。
- 透明度。如果 RDS 升级数据库失败、出现延迟或性能问题,我们根本无法采取任何措施。在生产环境中,你需要有一个可靠的回滚计划,以防万一。
- 我们想要的某些功能在当前实例中不可用,例如静态加密和 RDS 见解。
- 在某些情况下,我们需要更改实例类型。
我们选择 Bucardo 是因为我们想要一个在我们的 VPC 中沙盒化的解决方案,这样生产数据永远不会泄露到互联网上。最后迁移很成功,也没有丢失数据。迁移过程的总耗时不到 2 小时,算是比较成功的!
原文链接:
https://engineering.theblueground.com/blog/zero-downtime-postgres-migration-done-right/