当前位置: 首页 > 科技观察

数据库模式迁移数据实践

时间:2023-03-19 21:23:51 科技观察

如何进行大规模在线数据迁移工程团队经常面临一个共同的挑战:重新设计数据模型以支持清晰准确的抽象和更复杂的功能。这意味着迁移数百万个实时数据对象并在生产环境中重构数千行代码。用户期望StripeAPI的可用性和一致性。因此,在迁移时,需要格外小心。您必须确保数据值正确无误,并且Stripe的服务始终可用。本文将展示国外移动支付服务商Stripe如何安全地进行数以亿计的Subscriptions(订阅服务)对象的大规模迁移。为什么迁移困难?1、数据规模为数亿个Subscriptions对象。在生产数据库上进行涉及所有这些对象的大型迁移将是一个巨大的工作量。想象一下,迁移一个Subscription对象需要一秒钟,而以顺序方式迁移1亿个对象将需要三年多的时间。2.服务正常运行时间商业组织继续通过Stripe的服务进行交易。所有基础设施升级都是在线执行的,不依赖于计划的维护窗口。因为Subscriptions服务在迁移过程中不能中断,所以需要保证迁移过程中所有服务100%可用。3.数据正确性代码库中的大部分代码都使用订阅数据库表。如果您试图一次修改整个订阅服务中的数千行代码,您几乎肯定会忽略一些边缘情况。工程团队必须确保每项服务始终如一地获取正确的数据。在线迁移的模型很难将数百万对象从旧数据库表迁移到新表,但是很多公司需要做这样的事情。以下是大规模在线迁移常用的4步“双写模式”。具体步骤为:向旧表和新表双重写入数据,保证它们之间的数据是同步的。修改代码库中所有数据读取路径,从新表中读取数据。修改代码库中的所有数据写入路径,只向新表写入数据。删除依赖于过时数据模型的遗留数据。迁移示例:订阅什么是订阅?为什么需要数据迁移?Stripe的订阅用于帮助DigitalOcean和Squarespace等用户为其客户构建和管理定期计费。在过去的几年里,我们稳步增加了一些功能来支持更复杂的计费模型,例如多重订阅、试用、优惠券和发票。在早期,每个Customer对象最多有一个subscription。客户信息存储为单独的记录。因为从客户到订阅的映射非常简单,所以订阅信息与客户信息一起存储。classCustomerSubscriptionsubscriptionend最终,我们的用户希望客户有多个订阅。我们决定将单个订阅字段转换为订阅字段,以便存储具有多个订阅的数组。classCustomerarray:Subscriptionsubscriptionsend当添加新特性时,这个数据模型就会出现问题。对订阅的任何修改都会触发整个客户记录的更新,与订阅相关的查询必须通过扫描客户对象来实现。所以我们决定独立存储订阅。(重新设计的数据模型将订阅移动到单独的表中。)提醒一下,四步迁移方案如下:将数据双重写入旧表和新表,以确保它们之间的数据同步。修改代码库中所有数据读取路径,从新表中读取数据。修改代码库中的所有数据写入路径,只向新表写入数据。删除依赖于过时数据模型的遗留数据。下面分别介绍这四个步骤的具体做法。第一步:doublewrite创建一个新的数据库表作为迁移的开始。第一步是开始复制新数据,同时写入旧存储和新存储。之后,将缺失的数据回填到新的存储中,使两个存储的数据相同(所有新写入的数据都应该更新新旧存储)。对于Stripe,我们会将所有新创建的订阅同时写入Customers和Subscriptions表。在开始双写两个表之前,您需要评估额外的写操作对生产环境数据库性能的潜在影响。可以通过缓慢增加重复对象的百分比同时关注系统性能指标来缓解性能问题。此时,新创建的对象已经存在于两个表中,旧对象只能在旧表中找到。现有旧对象的复制将以惰性方式开始:每当对象更新时,它们都会自动复制到新表中。这样,可以逐步转移现有数据。***,将剩余的订阅数据回填到新表中。(将现有订阅数据回填到新表中)在外部服务数据库上查找所有需要迁移的数据是回填操作中成本最高的部分。通过查询数据库查找所有对象,需要在生产数据库上进行相当多的查询,这将花费大量时间。幸运的是,数据可以从在线导入到离线过程中,对生产数据库完全没有影响。我们为Hadoop集群创建数据库快照,这使我们能够使用MapReduce以离线、分布式方式快速处理数据。我们使用Scalding来管理MapReduce作业。Scalding是一个用Scala编写的非常有用的库,可以轻松编写MapReduce作业(一个简单的作业只需10行代码)。在这种情况下,使用Scalding帮助工程团队找到所有订阅数据。具体步骤如下:编写一个Scalding作业,提供所有需要复制的订阅ID的列表。通过一组并行执行的进程大规模复制订阅数据。迁移完成后,再次运行Scalding作业,确保Subscriptions表中存在所有订阅数据。第二步:更改所有读操作路径至此,新旧数据表处于同步状态。接下来要做的是对新表执行所有读取操作。(目前所有的读操作都是在Customers表上进行的,这些操作需要转移到Subscriptions表中)需要保证从新表读取数据是安全的,新旧表中的订阅数据表应该是一致的。来自GitHub的Scientist可以用来协助验证读取操作。Scientist是一个Ruby库,它允许我们在生产环境中进行实验,比较不同代码运行的结果,并对不一致的结果发出警告。使用Scientist,实时生成不一致结果的警告和指示器。当实验代码中出现错误时,应用程序的其余部分不会受到任何影响。实验过程如下:使用Scientist同时从Subscriptions和Customers表中读取数据。如果读取到不一致的数据,请提醒工程团队。GitHub的科学家可以运行读取两个表并比较数据的实验。确认所有数据一致后,就可以开始从新表中读取数据了。(实验成功,现在所有的读操作都是在Subscriptions表上进行的)第三步:更改所有的写操作路径接下来,需要更新写操作路径,将数据写入到新的Subscriptions表中。实施的目标是逐步推出这些更改,因此需要谨慎的策略。到现在为止,数据都是先写到旧表,再复制到新表:现在顺序倒过来了:数据先写到新表,再写到旧表。通过保持这两个表的一致,我们可以进行增量更新并仔细观察每个变化。重构订阅的所有编写代码可以说是迁移中最具挑战性的部分。在Stripe服务中处理订阅操作(例如更新、分期付款、续订)的逻辑涉及跨多个服务的数千行代码。成功重构的关键是增量处理:将尽可能多的代码路径分成尽可能小的单元,以便可以仔细应用每个更改。新旧表中的数据在重建的任何阶段都必须保持一致。对于每个代码路径,我们需要使用整体方法来确保我们的更改是安全的。我们不能只是用新数据替换旧数据:每个逻辑块都需要仔细考虑。如果遗漏任何案例,可能会导致数据不一致。值得庆幸的是,可以运行更多的科学家实验来提醒工程团队注意可能存在的任何不一致。新的、简化的写入数据路径如下:您可以通过在调用订阅数组时触发错误来确保没有代码继续使用过时的订阅数组:classCustomerdefsubscriptionsshard_assertion_failed("Accessingsubscriptionsarrayoncustomer")endend第4步:删除旧数据**的最好(也是最可取)的步骤是删除旧的写入代码并最终将其删除。一旦确定没有代码依赖过时数据模型的subscriptions字段,就不再需要向旧表写入数据:有了这个变化,代码不再使用旧数据源,而是使用新数据源成为唯一的数据源。现在,可以删除所有Customer对象上的订阅数组,“删除”操作逐渐以惰性方式处理。每次加载订阅时,订阅数组都会自动清空,并运行Scalding作业和迁移以查找要删除的任何剩余对象。最终的数据模型如下:结语在保证StripeAPI数据一致性的情况下进行迁移是非常复杂的。安全进行此迁移的几个关键点:我们开发了一个四阶段迁移策略,使我们能够在生产中切换数据而无需停机。使用Hadoop离线处理数据,使用MapReduce并行处理大量数据,而不是依赖于在生产数据库上执行昂贵的查询。所做的所有更改都是增量的。我们从未尝试一次更改数百行代码。所有的变化都是高度透明和可观察的。科学家的实验在生产环境中,只要一条数据不一致,就会第一时间提醒工程团队。在整个迁移过程中,我们对安全迁移充满信心。我们发现这种方法适用于我们执行的许多实时数据迁移。我们希望这些最佳实践也能对其他进行大规模迁移的团队有所帮助。