我们如何从MongoDB从MongoDB移动到Postgres,没有停机时间,将成本降低30%?

2021-05-07 03:22:11

凭证在2015年出生于2015年作为一个周末Hackathon项目,由我们的小型软件房屋运行,RSPlective。最初,它由MongoDB数据库备份。真相被告知,这种选择是随机的 - 它是我们在我们项目中使用的最常见的数据库。我们已经有了一些经验,所以Mongo在那个阶段是一个非常自然的组成部分。但是,如凭证,我们添加了第二个数据库 - PostgreSQL - 这似乎更适合即将到来的功能。然后我们一段时间我们保留了Mongo的一部分数据,并在Postgres的另一部分,直到我们决定将其全部移动到Postgres。

当我们开始时,我们已经收集了大约五年的数据,围绕三大洲的多个数据库实例传播,每个数据库实例都专用于不同的凭证群集。数百万个可以随时更新的优惠券代码。围绕一个不断变化的数据。并使事情变得更糟,因此必须为即将到来的突破变化做好大量的代码。如果在时间表上呈现,我们花了三个月重写和测试新代码,并在接下来的三个月迁移所有数据。

那么为什么要经历所有这些麻烦?我们有两种有效的理由要这样做。

首先,您可以轻松地想象,维护两个不同的数据库类型,创造了一倍的CodeBase,范式和概念的级联效果,在添加新功能时必须记住。它也是初始设置的问题的源头,后跟随机弹出的问题(通常是星期五下午)。如果其中一个看起来冗余,那么所有这些问题总结并导致工程团队的紧张和挫折。

其次,撰写 - 用于我们使用的MongoDB的SaaS平台,与替代方案相比,非常昂贵。它成为我们每月费用的显着比例。此外,我们对我们得到的支持的质量并不满足。有时,响应延迟可能会长达几天。在某些情况下,唯一提供的解决方案是重新启动数据库,没有良好的解释为什么奇怪的事情在第一位置发生或他们是否计划在将来解决这个问题。

为了成功,当流量高时,我们将其拆分成几个任务 - 每个任务 - 每个任务对应于不同的实体。他们中的大多数都很轻松地迁移相对较小的数据块已经更新的数据。这些任务中的每一个都有自己的故事,但本文将讲述最后一项任务的故事。它是关于两个核心实体 - 凭证和广告系列 - 它用作凭证API中的主要对象。正如你想象的那样,这些是最长的Mongo。您可以说,我们的系统的核心是在必须被替换的数据库周围构建的。

我们使用AWS的数据库迁移服务来帮助我们迁移。主要动机是减少准备迁移工具的设置的时间,依赖于已经由数百种开发人员测试的SaaS解决方案。

我们决定为每个Mongo系列创建新的临时表,以某种方式安全地将它们与下一步中的生产表合并。 PostgreSQL数据库具有一个很好的功能,帮助我们称为表继承。它使我们有可能以分层顺序将两个表连接在一起,以获取具有多个独立子表的父表。

首先,我们不得不找出我们应该选择的数据库。这种迁移与以前的迁移不同,所以如果Postgres是最佳选择,我们仍然在努力解决问题。丢弃几个选项后,我们仔细研究了AWS环境的DocumentDB和PostgreSQL。 DocumentDB是用于存储JSON文档的2019年1月发布的非关系数据库。其使命将在某种程度上与MongoDB API兼容,它完全转移到AWS团队,以合理的价格转化为可扩展性和可用性。

Postgres具有统一潜在技术的明显优势。因此,与重用现有数据库相比,购买DocumentDB将是额外的费用。通过设计,DocumentDB也不支持所有MongoDB命令,即使具有更大的可扩展性或可用性。然而,支持的特征对于大多数情况来说应该足够好。

因此,只有我们没有太大的疑问,我们只能预期迁移的代码更改。我们的估计是我们在使用一些不支持的Mongo功能的代码中拥有大约5-10个位置。做一些额外的(通常小)模式的迁移,如额外的旗帜来平整状态,通常是这些问题的解决方案。在下一步中,这些标志将让我们简化更复杂的查询。但是,这种变化需要首先在整个应用中添加许多小调整。

最后收集所有成本预测后,如果我们设法重用当前数据库实例,则可以轻松增强它们,与其他选项相比,我们仍然更好。因此,毕竟,选择并不是那么艰难,当决定做出了唯一的问题是我们将如何迁移大量数据。

为了回答这个问题,我们没有潜入关于可用迁移工具的巨大研究。我们所做的就是检查AWS数据库迁移服务的可能性,看看我们可以使用这种想法。我们希望减少准备迁移工具的设置的时间,依靠数百名开发人员测试的SaaS解决方案。

DMS是一个很好的工具,可用于在不同类型的数据库之间迁移数据 - 包括Mongo到Postgres Transfer。打开它后,您将获得带有用于迁移数据的即用的软件的底层EC2实例。为了能够保持数据同步,即使在长期运行迁移期间,您也可以以多Az模式推出它,只需单击一下即可。使用DMS,您还可以在框中获得体面的迁移监控过程。数据库迁移服务提供了多种抽象,其中三个似乎是必不可少的。所以端点,复制实例和数据库迁移任务。

从上次开始,任务是执行一次和/或作为正在进行的复制的特定副本粘贴作业的描述。它将过滤和转换规则与一些其他选项保持过滤。

复制实例是不言自明的,但要清除,这些是我们可以选择用于特定迁移任务的EC2实例列表。 AWS管理员创建具有所需RAM和CPU资源的实例后,将触发付款计数器。然而,总而言之,我们案件中使用DMS的总成本是Margnable。但是,如果你真的想知道,那么是的......我们花了这个工具的总共花了200美元。但与预测的节省相比,它就像在海洋中的一滴水。

最后但并非最不重要的是,端点是如何连接到特定数据库的描述。除了一些明显的参数之外,它还包括将使用的方向的意图,这意味着如果这是迁移过程中的源代码或者是目标。运行Replication实例后,可以使用它来测试定义的端点的连接。当您在源/目标端点的同一VPC中创建DMS Replication实例,那么您将前进一步,因为您不必在Internet上将您的数据库公开进行迁移时间。所有这些都在我们的情况下使DMS非常易于使用。

首先,它的GUI并不完美。有两种模式 - 图形和JSON - 首先不支持JSON模式的所有功能。所以通过文档是必要的,了解所有可能的过滤器和转换。但是,在我们设法使用JSON模式设置迁移过程之后,它变得更好地使用DMS。诀窍是使用简单的Bash脚本,大JSONs生成,具有精确的任务描述,我们简单地粘贴到DMS网站的文本区域整体。

我们不喜欢DMS的另一件事是,当创建一个新的迁移作业时,默认情况下会选中在开始时删除目标数据库中的目标表。确保有人需要这种行为,但为什么最危险的选项是默认的?我们计划以非常可控的方式按项目迁移数据项目,因此我们设想要创建数百个迁移任务。通过这种持续的风险,简单的错误将所有生产数据删除所有的生产数据推动我们走向更安全,但更难实现迁移过程的变体。我们的解决方案是以两个步骤迁移数据 - 首先到临时表可以意外地由DMS擦除,然后到目的地生产表。

在我们确定DMS是这项工作的可接受的工具之前,我们还必须克服许多技术障碍。最初,我们计划做两轮迁移,一个是一个用于竞选和第二次职业券。我们想象每轮每个项目都只有两个步骤为每个项目(工作空间) - 运行DMS,正在进行的复制,并在项目中切换布尔标志' s配置。正如您所猜的,它变得更加复杂。让我们轻快进入故事。

首先,我们需要精确澄清迁移前每个项目的设置是什么。从DMS任务的角度来看,每个项目基本上都是一个广告系列列表,以及一系列独立或属于广告系列的凭证。可行的吗?当然,但如果您将数百个项目乘以,则清楚地清楚地无法用手键入所有这些设置。

在如此重要的任务之前,清除一些剩余的逻辑中的代码总是很好,以简化迁移一点点。在我们的情况下,这是例如检查所有广告系列和凭证字段是否仍在使用。丢弃旧代码是过程中最简单的部分,可以缩短其整体时间。不幸的是,我们没有任何工作要做。

这使我们可以拥有非常简单的核心迁移脚本,这绝对是目标之一。为了进一步节省时间,我们在描述每个实体类型的有效模型时作出了一些假设。正如您可能所知,MongoDB是一个文档数据库,这意味着它是艺术模式。如果使用它,就像我们这样做一样,要存储具有架构的数据,那么您不仅要将其保持良好状态的责任转移到应用程序级别,但您仍然需要预期数据的某些部分将脏迁移到来时。是的,正如我们稍后会看到的那样,Mongo中的数据可以在许多方面(或弹性,具体取决于观点)。考虑到这些假设,我们在迁移每个项目的迁移之前进行适当的理智检查,以找到污染的条目。这允许我们以孤立地快速修复损坏的条目,并按住核心迁移。短缺免责声明 - 目的地数据库中可能会检查这些假设的一部分,因此在我们的情况下,有两个阶段的理智检查是有意义的 - 在蒙古和第二部分中的初始阶段。

我们在数据仍处于临时表中,我们执行了这些理智检查,DMS复制了数据。更详细信息将来,但我可以提到我们为每个MongoDB集合创建短期目的地表,并且当加载数据时,我们将这些临时子表逐一合并到父母中。

要更好地了解我们所做的何种类型的检查,让我们探索我们常常从两种简单类型的错误中找到实体的第一部代码:

{{码}} db ["凭证 - 租户 - 项目"]。count({$或:[{type:{$ alivings:false}},{deleted_at:{$ sipers:true}}] })

+ DB ["竞选 - 租户 - 项目"]。计数({$或:[{campaign_type:{$ alivings:false}},{deleted_at:{$ sipers:true}}})}})}

我们想找到的第一件事是,如果是完全完成凭证和广告系列类型的旧迁移之一。我们决定使用最终SQL表单中具有非空约束的这些类型,因此所有条目必须在迁移之前配备某些值。还可以在最终数据库中检查和修复此问题,但更容易削减根源中的问题并为所有人忘记它。

我们选中的第二件事是如果存在具有旧方法的任何条目,可让变量保留删除时间 - deleted_at。目前我们有一个替代的骆驼案命名删除了,它稍后发生,因为为了简单的迁移脚本,最好先清理旧数据。因此,如果从上面查询返回的总数是非零,我们列出了所有错误的条目,并在证明的情况下修复或删除它们。

在Postgres数据库中降落的数据后,我们执行第二轮Sanity检查。让我们跳过几步,并迅速描述在此阶段检查的内容。首先,我们使用以下查询搜索内部损坏的条目:

您可以看到持有凭证的表上执行的检查部分。这是最有趣的作品,所以此次查询的其余部分都在此处切割。在此之后,在广告系列和#39;数据上进行了相同类型的查询。每当一些有趣的发现来,我们将它们固定在蒙古,因此如果迁移必须因某种原因中止,则保留了这种行动的效​​果。

让我们探索上面的代码的细节。第一行仍然很难掌握。在描述核心迁移脚本时,我们将回复它。接下来的两行比较了“折扣”字段,具有SQL样的“NULL”和JSON - 类似的“NULL”值。这两种类型的空白是我们面临的第一个问题。旧的和新代码都暗示了在获取响应中的可能性,但我们希望确保在通过迁移脚本推动后完全完成数据。缺少数据预期SQL的空值,但JSON NULL有点令人惊讶。我们没有一个代码可以在该点设置空值,所以也许我们在前段时间的代码或这些空值是一些旧手动操作的结果。无论如何,阅读和解析损坏的数据,如此运行良好,但是在存储在PostgreSQL字段中的NULL上应用的JSONB连接'||'导致令人讨厌的错误。所以对这个小怪癖对谨慎谨慎态度很好。

关于出版物的计数的下一行显示了我们如何检查任何像性相同的字段是否未以浮动形式作为字符串值存储在Mongo中。例如,而不是预期的1,我们得到了“1.0000”。也许我们甚至不会注意到,如果我们没有大量的(演员...为INT)SQL转换已经在代码中。这种组合在迁移了几个旧的测试项目后引起了意外错误,因此您可以称之为我们发现的第二次怪癖。很少有这样的案例,所以我们手动修复了所有这些。这两个问题 - “1.0000”和JSON的空值 - 在迁移脚本中稍后通过我们解决,但我们在此处留下了所有这些检查,请仔细检查一切是否正常。

最后一张检查(jsonb_array_length(发布 - >'条目')> 0和publish :: text'%$ date%')“本来是为了找到出版物的优惠券输入以无效方式存储。此时,我们的代码已忽略存储在那里的数据,除了检查这些条目的总计数的一个查询。因此,我们决定使用所有这些出版物条目迁移凭证,并在我们在一个数据库中的所有数据稍后修复此问题。但首先,我们必须修复第一次手动测试后捕获的令人讨厌的错误。在极少数情况下,日期将在MongoDB中存储为isoDate,该isodate将被DMS转移为JSON对象,其中包含数值时间戳值的一个$日期字段。即使我们不再真正使用此数据了,我们的ORM系统仍在解析它,显然无法以此格式读取日期。与以前一样,没有许多情况下,因此手动修复最有效。

除了检查内部凭证和竞选数据,我们还在这一步检查了各种关系。当凭证和广告系列在一个数据库中以及另一个数据库中的其他部分时,这是不可能的。这是因为这是一个很长一段时间的脚本,大多数可能会给我们很多误报。因此,我们核实了每个广告系列内的凭证计数。在使用纯MongoDB脚本迁移之前,此检查是可能的,但在SQL版本中将其写入更容易。 SQL也保证计数凭证不受并行操作的影响,例如添加或删除凭证,因此将其与广告系列的“凭证_Count”进行比较,总是给出可靠的结果。我们还检查了存储在凭证数据中的赎回和出版物的总数。只有在所有数据内部在一个数据库内完成,那种检查只是可行的,即使那么花费大量的时间。

对于每个项目,有两个蒙古收藏率迁移。我们决定将每个集合转移到临时表,以限制其中一个DMS作业将丢弃充满活跃的活动或优惠券的目标表的风险。在准备迁移时,选择是否迁移到最终或临时表至关重要。我们去了似乎最安全的选项,如果例如我们在设置DMS任务时犯了错误。这不太可能发生,但我们想使用最安全的路径,看看我们是否能够忍受将掌握我们的负担。

此外,正如您稍后会看到我们需要额外的字段以完成迁移,并且在最终表中将它们添加到通过API返回此数据的风险,或将其存储在系统事件数据中。我们需要为这些风险准备代码,但我们仍然可能错过了一些东西。因此,延长模型的子表给我们绝对保证了这不会发生这种情况。很快您就会看到我们选择的一些问题,如果我们选择将数据直接迁移到生产表。很难说其他路径如何结束,但本文将呈现硬币的一侧。从时间的角度来看,我可以安全地说,我们解决的道路是正确的,因为它给我们带来了最佳表现和预期的结果。

我们决定为每个Mongo系列创建新的临时表,以某种方式安全地将它们与下一步中的生产表合并。 PostgreSQL数据库有一个很好的功能,帮助我们在这里称为表继承。它允许我们以分层顺序将两个表连接在一起,以获取具有多个独立子表的父表。每个子表单独存储数据和索引,但是当您从父表读取时,您将从所有接合表中聚合的结果,就像只有一个逻辑表一样。在此类继承关系中加入两个表的明显条件是子表必须拥有父表的所有列,但它们可以在与任何编程语言中的继承工作相同的感觉中具有更多。在通过父表读取数据时,无法访问这些列,但它们可以在许多方面都很有用。在我们的迁移过程中,我们使用这些附加列来存储每个对象的MongoDB原始数据和_ID。

让我再次压力每个子表每个子表都包含一个单独的索引的单独数据。对于独特的索引来说尤为重要,因为可以获得SE

......