这篇文章介绍了GOV.UK Notify 团队如何使用 AWS Database Migration Service (DMS) 将他们的PostgreSQL 数据库从 GOV.UK Platform as a Service (PaaS) 迁移到他们自己的 AWS 账户,只造成了 11 秒的停机时间。文章详细描述了他们的迁移过程,包括如何设置 DMS 实例,如何同步源数据库和目标数据库,以及如何平滑地切换流量。文章还分享了他们在迁移过程中遇到的一些挑战和解决方案,以及他们从中学到的一些经验教训。
原文链接:https://gds.blog.gov.uk/2024/01/17/how-we-migrated-our-postgresql-database-with-11-seconds-downtime/
未经允许,禁止转载!
GOV.UK Notify 目前托管在 GOV.UK 提供的平台即服务(PaaS)上。由于 PaaS 即将停止服务,我们决定把所有基础设施转移到我们自己的 Amazon Web Services (AWS) 账户。本文将介绍我们如何在最短停机时间内完成 PostgreSQL 数据库的迁移。
我们目前所有数据都存储在 PaaS 提供的数据库中,包括我们发送的每条通知的数据和成千上万个服务团队用以发送通知的模板内容。我们使用的是部署在 PaaS 上的 AWS RDS PostgreSQL 数据库,这些数据库托管在 PaaS 的 AWS 账户中。我们的应用程序在 PaaS 上运行,并与该数据库进行交互,我们将这个数据库称为“源数据库”。我们的目标是在自己的 AWS 账户中建立一个新数据库,并确保所有应用程序都能与这个新数据库进行交互。这个新建的数据库被我们称作“目标数据库”。在自己的 AWS 账户中创建新的 PostgreSQL 数据库虽然简单,但迁移所有数据并让应用程序切换到新数据库的过程中,如何尽量减少停机时间则是一大挑战。
我们的源数据库大约有 400GB 的数据容量,包含大约 13 亿行数据、85 个表、185 个索引和 120 个外键,使用的是 PostgreSQL 11 版本。在工作日,我们的数据库每秒会进行大约 1,000 次插入或更新操作,读操作也大体一致。GOV.UK Notify 每天都会发送数百万条重要且及时的通知,内容涉及从洪水预警到更新用户护照申请等。我们发送的每条通知都需要与数据库进行交互,因此应该尽可能减少停机时间,降低对业务的影响。
PaaS 团队支持使用 AWS 数据库迁移服务 (DMS) 进行数据库迁移。DMS 主要负责将数据从源数据库迁移到目标数据库,可以在源 AWS 账户或目标 AWS 账户中运行。DMS 的工作机制如下:
复制每个表的数据直到特定时间点,此阶段称为“全量加载”任务。
进入增量模式,确保源数据库中的所有新事务都在目标数据库上重放,以此确保两个数据库的数据同步。
完成这些步骤后,我们的就需要停止应用程序与源数据库的交互,并开始与目标数据库进行通信。
数据库迁移的过程相对复杂,需要按照不同阶段逐步进行。
在本例中,我们在源 AWS 账户中部署了 DMS 实例。我们选择源账户,是因为 PaaS 团队已经在该账户中配置好了 DMS 实例,这使得我们能更快捷、便利地完成设置。DMS 实例还需要获得授权,以获取 PostgreSQL 的访问权限,确保其能够与源数据库和目标数据库顺畅通信。需要注意的是,DMS 实例和我们的目标数据库处于不同的虚拟私有云(VPC)环境中。为了解决这一问题,我们在 PaaS 团队的协助下建立了 VPC 对等连接。这样一来,来自 PaaS VPC 中的 DMS 实例的流量就可以直接传输到我们的 VPC,无需经过公共互联网。
我们在自己的 AWS 账户中搭建了目标 RDS 实例。考虑到 PostgreSQL 11 版本即将停止支持,我们借此机会将我们的 PostgreSQL 版本升级到了 15。接下来,我们利用 pg_dump 命令导出了源数据库的数据库结构和数据。这样,我们就得到了一个包含重建数据库架构所需 SQL 命令的文件。我们从导出的数据库架构中提取了表的定义,并将其应用于目标数据库。在此阶段,我们暂时没有应用外键约束。原因是 DMS 在执行全量加载时并不会按照外键约束的顺序复制数据。此外,我们也没有立即创建主键或索引。这是因为在全量加载阶段,创建索引会显著降低速度。每次单独插入操作都会耗费更多时间,因为它需要更新索引,而在插入数十亿行数据的过程中,这将累积成显著的延迟。先复制所有数据,然后再添加索引,将会更加高效。
当我们目标数据库的表格建立完毕后,我们便启动了数据库迁移服务(DMS)的全量加载任务。该任务负责复制在我们点击“开始全量加载”按钮时,已经存在于数据库中的所有数据。需要注意的是,这个过程不会涉及到后续产生的任何新数据或数据更新。整个全量加载任务大约耗时 6 小时。在全量加载任务完成之后,我们接着应用了剩余的源数据库结构和数据文件,主要目的是添加索引和键约束。这部分工作大概用了 3 小时。
全量加载任务一旦完成,我们的目标数据库中的数据就与开始全量加载任务时源数据库中的数据保持一致了。但是从那时起,源数据库中发生了大量的新插入、更新和删除操作,并且这样的变更还在持续进行。为了同步这些新变更,我们随后启动了 DMS 的持续复制任务(也称为变更数据捕获)。该任务负责读取全量加载任务开始后,在源数据库的事务日志中生成的所有事务,然后将它们传输到我们的目标数据库。这确保了我们的目标数据库能够与源数据库实现同步,最多仅有微小的延迟。数据复制过程只用了几个小时就追上了最新状态。在此期间,我们对 DMS 的复制过程中的延迟进行了监控,以确保它能够处理源数据库中发生的变更量,并继续保持数据同步。我们持续进行了大约 10 天的 DMS 复制过程,期间始终保持数据同步,直到我们的应用程序停止与源数据库通信并切换到目标数据库。我们已经提前通知了用户这一切换的时间点,因此数据迁移的具体时间已经确定。
几个月前,我们制定了一个计划,用于停止应用程序与源数据库的通信,并切换至目标数据库。以下是我们的操作步骤:
首先,我们将中断应用程序对源数据库的所有访问。这样做会导致服务中断,但事先已经通知到用户。
接着,我们要确保数据复制已经追上最新状态,源数据库的所有更新都已同步到目标数据库中。
最后,允许应用程序开始与目标数据库进行通信,此时服务恢复。
需要特别注意的是,我们不能让部分应用程序同时与源数据库和目标数据库进行通信。如果出现这种情况,目标数据库上的任何更改都不会同步到源数据库,这可能导致用户接收到不一致的数据。为了实现这一过程,我们编写了一个 Python 脚本,其目的是确保流程的明确性、可重复性,且相比手动操作更为高效。这个过程的快速完成将减少 Notify 用户的服务中断时间。我们的目标是将中断时间控制在 5 分钟以内。在之前的演练中,我们已经运行了这个脚本至少 40 次。我们选择在某个周六晚上进行数据迁移,因为那时我们业务使用量最低,对用户影响也更小。
我们的脚本会通过调用 pg_terminate_backend 命令来中断所有指向源数据库的应用程序连接。这一操作只需要不到一秒钟。同时,我们还会更改应用程序所用 PostgreSQL 用户的密码,这意味着如果应用程序试图重新连接源数据库,它们将遇到认证错误。
DMS 会在目标数据库中插入一些用户显示复制状态的表,并每分钟更新一次。这些表能显示目标数据库与源数据库之间的延迟程度。我们会用迁移脚本检查这些表,确保目标数据库已经完全同步至最新状态。为确保安全,应用程序停止与源数据库通信后,我们的迁移脚本会向源数据库写入一条记录,并确认这条记录是否已安全同步到目标数据库。这为我们提供了额外证明,确保所有更改都已经被复制。
为了确保我们的应用程序能够顺利连接到数据库,必须向它们提供数据库的位置、相关 PostgreSQL 用户的用户名和密码。这些信息通常通过如下格式的环境变量传递给应用程序:SQLALCHEMY_DATABASE_URI = postgresql://original-username:original-password@random-identifier.eu-west-1.rds.amazonaws.com:5432
如果我们需要将应用程序连接至另一个数据库,就必须修改 URI 中的用户名、密码和地址,并重新部署应用程序以应用这些变更。重新部署应用程序大约需要 5 分钟。如果在迁移脚本中包含应用程序的重新部署,这将导致 5 分钟停机。为了尽可能缩短停机时间,我们在迁移前进行了两项调整,这样就可以通过快速更改域名系统(DNS)来实现切换,无需重新部署应用程序。
首先,我们在源数据库和目标数据库上都创建了一个拥有相同用户名和密码的用户。这样,在迁移过程中,我们就无需更改提供给应用程序的用户名或密码。其次,我们在 AWS Route53 中为 database.notifications.service.gov.uk 设置了一个生存时间(TTL)为 1秒的 DNS 记录。它包含两个权重设置:
100% 的 DNS 解析结果指向源数据库位置。
0% 的 DNS 解析结果指向目标数据库位置。
我们把应用程序所用的 URI 更新为这个新的用户名和密码,并使用新域名作为数据库的地址。
SQLALCHEMY_DATABASE_URI = postgresql://shared-username:shared-password@database.notifications.service.gov.uk:5432
现在,当我们需要切换应用程序所连接的数据库时,我们的迁移脚本只需在 AWS 中更新 DNS 权重,使目标数据库位置接收 100% 的查询结果,并等待 1 秒钟以让 TTL 过期。之后,当我们的应用程序下一次尝试访问数据库时,它们将连接到我们的目标数据库。
在 11 月 4 日星期六晚上,我们聚集在一起。此时情况是,目标数据库已经构建完成,全量数据加载正在进行中,新的事务也在不断地进行复制。我们检查发现,目标数据库与源数据库之间的延迟仅有几秒钟。随后,我们成功执行了迁移脚本,使得应用程序停止与原始源数据库的交互,并开始与新的目标数据库进行通信。在整个迁移过程中,我们的服务中断时间大约为 11 秒。这一时间远远低于预定的 5 分钟停机预期,我们和用户都非常满意。
我们选择使用 DMS 的原因是它得到了 GOV.UK PaaS 的强力支持,并且我们也可以从 AWS 那里得到帮助。如果今后我们需要进行 PostgreSQL 到 PostgreSQL 的数据库迁移,我们将花更多时间考虑尝试其他工具,比如 pglogical。我们发现 DMS 可能会带来更多的复杂性,并且其复制过程与我们可能在其他工具中遇到的过程有所不同。这也验证了 AWS DMS 官方文档中关于 PostgreSQL 数据库迁移指导手册的正确性。
现在数据库迁移已经完成,下一步我们将迁移应用程序。我们将把它们移至 AWS 弹性容器服务 (ECS)。