首发于笔者的微信公众号:技术心流FollowFlows

 

各工程团队常面临一项共同挑战:重新设计数据模型以支持清晰准确的抽象和更复杂的功能。这意味着,在生产环境中,需要迁移数以百万计的活跃数据对象,并且重构上千行代码。

用户期望 Stripe API 保障可用性和一致性。所以在进行迁移时,需要格外谨慎,必须保证数据的数值正确无误,并且 Stripe 的服务始终保持可用。

本文将展示 Stripe 如何安全地对数以亿计的 Subscriptions (译者注:Subscriptions指服务的订购)对象进行大规模迁移。

为什么迁移是困难的

  • 数据规模

    数以亿计的 Subscriptions 对象。在生产环境数据库上进行涉及到所有这些对象的大规模迁移会有巨大的工作量。

    想象一下,迁移一个 Subscription 对象需要花费一秒钟,若以顺序方式迁移一亿个对象将花费超过三年的时间。

  • 服务在线时间

    商业机构不断的通过 Stripe 的服务进行交易。所有的基础设施升级都是在线进行,而不依赖于有计划的维护时段。因为不能在迁移过程中中断Subscriptions 服务,在这个迁移过程中必须要保证所有服务100%处于可用状态。

  • 数据正确性

    代码库中的很多代码都在使用 Subscriptions 数据库表。如果试图一次性修改整个 Subscriptions 服务中数以千计的代码行,那几乎肯定会忽视一些边界情况 。工程团队必须确保每项服务都能够持续获取正确无误的数据。

在线迁移的模式

将数百万个对象从旧数据库表迁移到新表是很有难度的,但许多公司需要去做这样的事情。

以下是在进行大型在线迁移中常用的4步”双写模式“,具体步骤是:

  1. 向旧表和新表双写数据以保证它们之间的数据是同步的。

  2. 修改代码库中所有的数据读取路径以从新表读取数据。

  3. 修改代码库中所有的数据写入路径以将数据只写入新表。

  4. 删除依赖过时数据模型的旧数据。

迁移示例:Subscriptions

什么是Subscriptions?为什么需要进行迁移?

Stripe 的 Subscriptions 用于帮助 DigitalOcean 和 Squarespace 这类用户构建并管理他们客户的订购计费。在过去几年中,我们稳步增加了一些功能来支持更复杂的计费模式,例如复合订阅、试用、优惠券和发票。

在早期,每个 Customer 对象最多只有一个 subscription 。 customers 信息存储为单独的记录。因为 customers到 subscriptions 之间的映射关系非常简单,所以subscriptions 信息与 customers 信息存储在一起。

最终,我们的用户想要具有多个 subscriptions 的 customers 。我们决定将单一的 subscription 字段转换为 subscriptions 字段,以便存储具有多个 subscription 的数组。

当添加新功能时,这个数据模型便出现问题了。任何对 subscriptions 的修改都会引发整条 Customer 记录的更新,以及 subscriptions 相关的查询都要通过扫描 customer 对象实现。所以我们决定将 subscriptions 独立存储。

重新设计的数据模型将 subscriptions 转移到独立的数据表中

再次提醒,四步迁移方案如下:

  1. 向旧表和新表双写数据以保证它们之间的数据是同步的。

  2. 修改代码库中所有的数据读取路径以从新表读取数据。

  3. 修改代码库中所有的数据写入路径以将数据只写入新表。

  4. 删除依赖过时数据模型的旧数据

下面介绍这四个步骤的具体实践。

第一步:双写

创建一张新的数据库表作为迁移的开始。第一步是开始复制新数据,同时写入新旧两处存储中。之后,再将缺失的数据回填至新存储,已使两处存储具有相同的数据。

所有新写入的数据都应更新新旧两处存储

在 Stripe 的案例中,我们将所有新创建的 subscriptions 同时写入 Customers 表和 Subscriptions 表。在开始双写两张表之前,需要评估额外的写入操作对生产环境数据库性能的潜在影响。可以通过缓慢提高重复对象的百分比来缓解性能问题,同时持续关注系统运行指标。

进行到此时,新创建的对象已同时存在于两张表中,而旧对象只能在旧表中找到。接下来将以懒惰方式( lazy fashion )开始复制已存在的旧对象:每当对象更新时,将它们自动复制到新表中。这种方式可逐步转移已存在的数据。

最后,将剩余的 subscriptions 数据回填至新表。

回填已存在 subscriptions 数据至新表

在正在对外提供服务的数据库上找到所有需要迁移的数据是回填操作中代价最大的部分。通过查询数据库查找所有对象的方式将需要在生产环境数据库上执行相当多的查询操作,这将耗费很多时间。幸运的是,可以将数据从线上导入对生产环境数据库完全无影响的离线流程中。我们创建适用于我们 Hadoop 集群的数据库快照,这让我们可以使用 MapReduce (https://en.wikipedia.org/wiki/MapReduce)  以离线,分布式的方式快速处理数据。

我们使用 Scalding (https://github.com/twitter/scalding)  来管理 MapReduce  Job。 Scalding 是用 Scala 编写的非常实用的库,可以很容易地编写MapReduce Job(10行代码即可实现一个简单的Job)。 在这种情况下,使用 Scalding 帮助工程团队找出所有subscriptions 数据。具体步骤如下:

  • 编写一份 Scalding Job,提供所有需要复制的 subscription ID 的列表。

  • 通过一组进程并行执行来大规模的复制 subscriptions 数据。

  • 迁移完成后,需再次运行 Scalding Job,以确保所有 subscriptions 数据都已存在于 Subscriptions 表中。

第二步:改变所有读操作路径

到目前为止,新旧数据表已是同步状态。下一步要做的是在新表上进行所有的读操作。

目前,所有的读操作在 Customers 表上进行,需要将这些操作转移到 Subscriptions 表上。

需要确保从新表读数据是安全的,subscription 在新旧表中的数据应该是一致的。可以使用 GitHub 出品的 Scientist (https://github.com/github/scientist)  来辅助验证读操作。Scientist 是一个 Ruby 库, 它可以让我们在生产环境运行实验,比对不同代码的运行结果并对不一致的结果发出警告 。通过 Scientist ,可实时生成针对不一致结果的警告和指标。当实验代码中发生错误,其余的应用程序是不会受到任何影响的。

实验如下进行:

  • 使用 Scientist 从 Subscriptions 表和 Customers 表同时读取数据。

  • 如果读取到的数据不一致,则向工程团队发出警告。

GitHub 出品的 Scientist 可运行读取两张表并对数据做对比的实验。

在确认所有数据是一致的后,就可以开始从新表读取数据了。

实验成功,现在所有的读操作都在 Subscriptions 表上进行。

第三步:改变所有写操作路径

接下来,需要更新写操作路径,将数据写入新的 Subscriptions 表。 实施的目标是逐步推进这些改变,所以需要采取谨慎的策略。

直到现在,数据一直写入旧表,然后被复制到新表:

现在要颠倒这个顺序:先将数据写入新表,然后将其写入旧表中。 通过保持这两张表的一致性,我们可以进行增量更新并仔细观察每个更改。

重构 subscriptions 的所有写操作代码可以说是迁移中最具挑战性的部分。 Stripe 服务中处理 subscriptions 操作的逻辑(例如更新,分期付款、续费)涉及多个服务的数千行代码。

成功重构的关键是增量处理:将尽可能多的代码路径分隔成可能的最小单元,以便可以仔细应用每个更改。 新旧两张表的数据在重构的任何一个阶段都需要保持一致。

对于每个代码路径,我们需要使用整体方法来确保我们的更改是安全的。 我们不能仅仅只使用新数据替代旧数据:每一个逻辑块都需要仔细斟酌。 如果错过了任何情况,可能就会造成数据不一致。 值得庆幸的是,可以运行更多的 Scientist 实验来提醒工程团队可能存在的任何不一致。

新的,简化的写数据路径如下所示:

可通过在调用 subscriptions 数组时触发报错的方法,确保没有代码继续使用过时的subscriptions 数组:

第四步:删除旧数据

最后的步骤是移除旧的写操作代码并最终删除它们。

一旦确定没有任何代码依赖过时数据模型的 subscriptions 字段,就不再需要将数据写入旧表:

随着这一变化,代码不再使用旧数据源,新数据源成为唯一数据源。

现在,可以删除所有 Customer 对象上的 subscriptions 数组,并且逐渐以懒惰的方式处理“删除”操作。 每次 subscription 被加载后,都会自动清空这个  subscriptions 数组,然后运行 Scalding 作业并迁移,以查找任何剩余的要删除的对象。 最终的数据模型如下:

结论

在保证 Stripe API 数据一致性的同时进行迁移是非常复杂的工作。安全进行这项迁移的几个要点如下:

  • 我们制定了一个四阶段迁移策略,可以让我们在生产环境中不停服进行数据切换。

  • 使用Hadoop离线处理数据,使用MapReduce以并行方式处理大量数据,而不是依赖在生产环境数据库上执行的代价高昂的查询。

  • 所做的所有更改都是渐进式的。 我们从未试图一次更改几百行代码。

  • 所有的变化都是高度透明和可观察的。 Scientist 的实验只要有一条数据在生产环境中是不一致的,就立即提醒工程团队。 在整个迁移过程中,我们都对安全的迁移怀有信心。

我们发现这种方法在我们执行过的许多在线数据迁移中都很有效。我们希望这些实践做法对于其他团队进行大规模迁移也是有帮助的。

如何进行大规模在线数据迁移(来自Stripe公司的经验)的更多相关文章

  1. HDFS数据迁移解决方案之DistCp工具的巧妙使用

    前言 在当今每日信息量巨大的社会中,源源不断的数据需要被安全的存储.等到数据的规模越来越大的时候,也许瓶颈就来了,没有存储空间了.这时候怎么办,你也许会说,加机器解决,显然这是一个很简单直接但是又显得 ...

  2. 数据库分库分表(sharding)系列(五) 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案

    作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...

  3. 数据迁移的应用场景与解决方案Hamal

    本文来自网易云社区 作者:马进 跑男热播,作为兄弟团忠实粉丝,笔者也是一到周五就如打鸡血乐不思蜀. 看着银幕中一众演员搞怪搞笑的浮夸演技,也时常感慨,这样一部看似简单真情流露的真人秀,必然饱含了许许多 ...

  4. Redis实践系列丨Codis数据迁移原理与优化

    Codis介绍 Codis 是一种Redis集群的实现方案,与Redis社区的Redis cluster类似,基于slot的分片机制构建一个更大的Redis节点集群,对于连接到codis的Redis客 ...

  5. DB 分库分表(5):一种支持自由规划无须数据迁移和修改路由代码的 Sharding 扩容方案

    作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...

  6. Apsara Clouder云计算技能认证:云数据库管理与数据迁移

    一.课程介绍 二.云数据库的简介及使用场景 1.云数据库简介 1.1特点: 用户按存储容量和带宽的需求付费 可移植性 按需扩展 高可用性(HA) 1.2阿里云云数据库 RDS 稳定可靠,可弹性伸缩的在 ...

  7. EF Code First:实体映射,数据迁移,重构(1)

    一.前言 经过EF的<第一篇>,我们已经把数据访问层基本搭建起来了,但并没有涉及实体关系.实体关系对于一个数据库系统来说至关重要,而且EF的各个实体之间的联系,实体之间的协作,联合查询等也 ...

  8. SQL SERVER 2000/2005/2008数据库数据迁移到Oracle 10G细述

    最近参与的一个系统涉及到把SQL Server 2k的数据迁移到Oracle 10G这一非功能需求.特将涉及到相关步骤列举如下供大家参考: 环境及现有资源: 1.OS: Windows 7 Enter ...

  9. kettle 数据迁移 (转)

    最近在公司搞一个项目重构迁移问题,旧项目一直在线上跑,重构的项目则还没上线.重构之后数据库表结构,字段,类型等都有变化,而且重构的数据库由oracl改为mysql.这样就设计到数据迁移问题,别人推荐下 ...

随机推荐

  1. 加减 script函数初识

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. hdu1070

    #include <stdio.h>#include <string.h>struct milk{ char brand[128]; int price; int volume ...

  3. 修复Ubuntu的引导

    重装win7后如何恢复ubuntu引导 在重装系统之后,开机启动界面的ubuntu引导不见了,直接进入新安装的window系统中.下面是如何恢复ubuntu引导的方法: 1)准备一张ubuntu系统安 ...

  4. 【Qt文档阅读】Window and Dialog Widgets

    Window and Dialog Widgets 没有嵌入到父控件中的控件(widget)称之为窗口(window).通常窗口带有边框和标题栏. Windows通常集成到桌面环境中,并且在某种程度上 ...

  5. [转]简短介绍 C# 6 的新特性

    原文地址:http://www.oschina.net/translate/briefly-exploring-csharp-new-features 几周前我在不同的地方读到了有关C#6的一些新特性 ...

  6. EasyUI 在mvc中的引入与使用

    使用步骤: 一.先引入: 1.easyui下载,可以去官方网站去下载最新版EasyUI官方下载地址:http://www.jeasyui.com/download/index.php 2.下载后解压, ...

  7. nfs使用教程

    服务端 配置说明 文件名 /etc/exports 格式 <输出目录> [客户端 (访问权限,用户映射,其他) ] [客户端 (访问权限,用户映射,其他) ] 格式说明 输出目录:NFS系 ...

  8. 【leetcode 105. 从前序与中序遍历序列构造二叉树】解题报告

    前往 中序,后序遍历构造二叉树, 中序,前序遍历构造二叉树 TreeNode* build(vector<int>& preorder, int l1, int r1, vecto ...

  9. node -- 安装及快速开始

    下载并安装 node下载地址:https://nodejs.org/en/download/ 安装就绪后,打开命令行,操作如下: shift+右键/Win+r->cmd 检测是否安装成功: no ...

  10. C++ List的用法(转载)

    Lists将元素按顺序储存在链表中. 与 向量(vectors)相比, 它允许快速的插入和删除,但是随机访问却比较慢. assign() 给list赋值  back() 返回最后一个元素  begin ...