作者 | Peter Johnston
译者 | 平川
策划 | Tina
本文最初发布于 Retool 官方博客。
Retool 的云托管产品基于一个在微软 Azure 云中运行的 4TB 的 Postgres 数据库。去年秋天,我们把这个数据库从 Postgres 9.6 版升级到 13 版,而且停机时间极短。
我们是如何做到的呢?坦率地说,这不是一条从 A 到 B 的直线。在这篇文章中,我将为你讲述这个故事,并分享一些可以帮助你进行类似升级的技巧。
动    机
你可能不了解 Retool,我们是一个快速构建内部工具的平台。你可以使用一个可拖放编辑器来构建 UI,并轻松将它们连接到自己的数据源,包括数据库、API 和第三方工具。你可以把 Retool 作为一个云托管产品(它基于我们在这篇文章中谈到的数据库),或者你可以自己托管它。从 4TB 的数据库可以看出,Retool 的许多客户正在云上构建许多应用。
去年秋天,我们决定升级我们的主 Postgres 数据库。我们有一个令人信服的理由——Postgres 9.6 将在 2021 年 11 月 11 日到达生命周期的终点,这意味着将不再有针对这个版本的 Bug 修复或安全更新。我们不想让客户的数据有任何风险,所以我们不能继续使用这个版本。事情就这么简单。
技术设计
升级涉及一些高级决策:
  • 应该升级到什么版本的 Postgres?
  • 应该用什么策略进行升级?
  • 应该如何测试升级?
在开始之前,我们回顾下我们的一些限制条件和目标,就只有下面这几个。
  • 在 2021 年 11 月 11 日之前完成升级。
  • 尽量减少停机时间,特别是在全球范围内周一至周五的工作时间里。这是截止日期之外最重要的考量因素,因为 Retool 对我们的许多客户而言至关重要。
  • 当在 4TB 的数据库上操作时,停机时间是特别需要考虑的一个因素。在这种规模下,原本简单的事情也会变得比较困难。
  • 我们希望维护窗口最多为一小时左右。
  • 在我们不得不再次升级之前,最大限度地增加这次升级为我们赢得的时间。
PostgreSQL 13
我们决定升级到 Postgres 13,因为它符合上述所有标准,特别是最后一个标准:在下一次升级之前为我们赢得最多的时间。
当我们开始准备升级时,Postgres 13 是 Postgres 的最新版本,其支持窗口到 2025 年 11 月。我们预计,在这个支持窗口结束时,我们将把数据库分片,并逐步完成下一次重大的版本升级。
Postgres 13 还提供了一些以前版本中没有的功能。这里是完整列表。以下是最让我们兴奋的几个:
  • 重大的性能改进,包括在并行查询执行方面。
  • 能够安全地添加默认值非空的列,这消除了一个常见的问题。在早期的 Postgres 版本中,添加默认值非空的列会导致 Postgres 执行表重写,并阻塞并发读和写,这可能会导致停机。
  • 并行索引清理。(Retool 有几个写流量很大的表,我们非常关心清理问题。)
升级策略
很好,我们已经选定了一个目标版本。现在,我们要如何实现这个目标呢?
一般来说,升级 Postgres 数据库版本最简单的方法是进行 pg_dump 和 pg_restore。关闭应用程序,等待所有的连接终止,然后关闭数据库。在数据库处于冻结状态时,把它的内容转储到磁盘上,然后将其恢复到一个运行 Postgres 目标版本的新数据库服务器上。恢复完成后,把应用程序指向新的数据库,恢复服务。
这种升级方案很吸引人,因为它既简单,又能保证数据在新旧数据库之间完全同步。但我们马上就排除了这个选项,因为我们想尽量减少停机时间——4TB 的数据库完成转储和恢复,停机时间需要几天,而不是几小时或几分钟。
我们选择了一种基于逻辑复制的策略。通过这种方法,可以并行运行两个数据库的副本:要升级的主数据库和运行在 Postgres 目标版本上的从数据库。主数据库将持久性存储的变化(通过解码其预写日志)发布到从数据库上,使得从数据库可以快速复制主数据库的状态。这有效地消除了在 Postgres 目标版本上等待数据库恢复的时间,而且,目标数据库始终是最新的。
值得注意的是,这种方法需要的停机时间比“转储和恢复”策略少得多。我们不需要重建整个数据库,只需要停止应用,等旧的 v9.6 主数据库完成所有事务,再等 v13 主数据库复制完最新数据,就可以将应用指向主数据库。这可以在几分钟内完成,而不是几天。
测试策略
我们维护一个 Retool 云实例的过渡环境。我们的测试策略是在这个过渡环境上运行多次测试,并在这个过程中编制一份详细的操作手册并反复修改完善。
测试运行和操作手册都对我们很有帮助。在下一节中你将看到,我们在维护窗口期间执行了许多手动步骤。在最后切换时,这些步骤基本上都顺利完成,因为我们在前几周进行了多次彩排,这帮助我们创建了一个非常详细的操作手册。
我们的主要疏忽是没有在过渡环境中用一个有代表性的工作负荷进行测试。过渡数据库比生产数据库小,即使理论上讲,我们可以借助逻辑复制策略处理更大的生产工作负载,但我们遗漏了一些细节,导致 Retool 的云服务出现中断。我们将在下面的章节中进行详细介绍,但这里我们希望传达的最重要的信息是:用有代表性的工作负载进行测试非常重要。
实施计划:技术细节
实现逻辑复制
我们最终使用了 Warp。值得注意的是,Azure 的单服务器 Postgres 产品不支持 Postgres 扩展 pglogical,而我们的研究使我们相信,这是 Postgres 在 10 版本之前实现逻辑复制的最佳选项。
我们初期走过的一个弯路是尝试 Azure 的数据库迁移服务(DMS)。DMS 会首先对源数据库做个快照,然后将其恢复到目标数据库服务器。一旦初始转储和恢复完成,DMS 就会启动逻辑解码,这是 Postgres 的一项功能,将数据库的持久性变化发送给外部订阅者。
然而,在我们 4TB 的生产数据库上,最初的转储和恢复一直未能完成:DMS 遇到了一个错误,但没有向我们报告。与此同时,尽管没有取得任何进展,DMS 还是在我们的 9.6 主数据库中保留了事务。这些长期运行的事务反过来又阻碍了 Postgres 的自动清理功能,因为 vacuum 进程不能清理长期运行的事务开始后产生的死亡元组。随着死亡元组的堆积,9.6 主服务器的性能开始受到影响。这导致了我们上面提到的故障。(我们后来增加了监控,跟踪 Postgres 中未清理元组的数量,使我们能够主动发现危险的情况)。
Warp 的功能与 DMS 类似,但提供更多的配置选项。特别是 Warp 支持并行处理,可以加速初始转储和恢复。
Warp 希望所有的表都有一个单列主键,所以我们不得不做一些小动作来骗过它。我们把复合主键转换成唯一约束,并增加标量主键。除此之外,Warp 非常简单易用。
跳过大表复制
我们进一步优化了我们的方法,让 Warp 跳过两个特别大的表,这两个表左右了转储和恢复的运行时间。我们这样做是因为 pg_dump 不能在单个表上进行并行操作,所以最大的表将决定最短迁移时间。
为了处理在 Warp 中跳过的两个特别大的表,我们写了一个 Python 脚本,将数据从旧数据库服务器批量转移到新服务器。其中比较大的一个表有 2TB,这是应用程序中一个仅限追加的事件审计表,它很容易迁移:我们等切换后再迁移其中的数据,因为即使该表是空的,Retool 产品的功能也不受影响。我们还选择将非常老的审计事件转移到一个备份存储解决方案中,以减少表的大小。
另一个表是一个仅限追加的大小几百 GB 的日志表,记录了对所有 Retool 应用的所有编辑(即 page_saves),这个表比较棘手。这张表是所有 Retool 应用的真相来源,所以在维护完成的那一刻需要保证最新。为了解决这个问题,我们在维护窗口前的几天里迁移了大部分内容,并在窗口期间迁移了剩余的内容。虽然这个方法很有效,但我们注意到,它确实增加了风险,因为我们现在有更多的工作要在有限的维护窗口内完成。
编制操作手册
以下是我们在维护窗口期间执行的一个大概的步骤:
  • 停止 Retool 服务,等待所有未完成的数据库事务提交。
  • 等待 Postgres 13 从数据库赶上逻辑解码的进度。
  • 同时,把剩余的 page_saves 行复制过来。
  • 在所有数据都迁入 Postgres 13 服务器后,启用主键约束(Warp 要求禁用这些约束)。
  • 启用触发器(Warp 要求禁用触发器)。
  • 重置所有的序列值,这样一旦应用程序重新上线,就可以使用序列分配整数主键了。
  • 重新上线 Retool 服务,指向新的数据库,执行健康检查。
启用外键约束
从上面的操作手册中可以看到,我们必须做的一个步骤是禁用然后重启外键约束检查。复杂之处在于,默认情况下,Postgres 在启用外键约束时,会运行一次全表扫描,以验证当前所有的行是否满足新的约束。对于大型数据库来说,这是一个问题:Postgres 根本不可能在一小时的维护窗口内扫描数 TB 的数据。
为了解决这个问题,我们最终选择在几个大表中不启用外键约束。我们认为这可能没什么问题,因为 Retool 的产品逻辑会执行它自己的一致性检查,而且也不会从被引用的表中删除,这意味着我们不太可能留下一个悬空的引用。尽管如此,这也是一种风险;如果我们的推理不正确,那么我们最终会有一堆无效的数据需要清理。
后来,在维护窗口结束后的清理工作中,我们恢复了当时去掉的外键约束。我们发现,Postgres 为我们的问题提供了一个干净的解决方案:ALTER TABLE 的 NOT VALID 选项。添加一个带有 NOT VALID 选项的约束,Postgres 就会只对新数据执行约束验证,对现有数据则不执行,这样就可以绕过昂贵的全表扫描。之后,只需要运行 ALTER TABLE ... VALIDATE CONSTRAINT,它就会运行全表扫描并从约束中删除 NOT VALID 选项。当我们这样做的时候,发现表中并没有无效的数据,这让我们松了一口气。要是我们在维护窗口之前就知道这个选项就好了。
小    结
我们把维护窗口安排在 10 月 23 日(星期六)晚些时候,在 Retool 云流量最低的时间段。通过上述配置,我们能够在大约 15 分钟内新建一个 13 版本的数据库服务器,并通过逻辑解码订阅我们 9.6 版本主服务器的变化。
最后,在 Warp 的帮助下,通过逻辑复制策略,以及在测试环境中的彩排,我们编制了一份可靠的操作手册,使我们能够将 4TB 的数据库从 Postgres 9.6 迁移到 13。在这个过程中,我们认识到了在真实的工作负载上进行测试的重要性,创造性地跳过了不那么关键的大表,并且了解到(有点晚),Postgres 允许我们选择只对新数据执行外键约束验证,而不是所有数据。希望你能从我们的经验中学到一些东西。
查看英文原文:
https://retool.com/blog/how-we-upgraded-postgresql-database/?
点击底部阅读原文访问 InfoQ 官网,获取更多精彩内容!
今日好文推荐
点个在看少个 bug👇
继续阅读
阅读原文