对于一个正在持续运行的SaaS平台来说,数据库迁移远不是把旧数据库备份下来,再恢复到新服务器这么简单。假设平台已经积累数亿条用户、订单、账单和日志数据,每分钟还有新的注册、订单、付款和数据修改产生,此时直接停止业务进行迁移,停机时间可能从几十分钟延长到数小时,甚至更久。
真正意义上的不停机迁移,并不是要求数据库迁移过程中完全没有任何业务变化,而是让旧数据库继续承载生产业务,同时把历史数据和迁移期间产生的新数据持续同步到目标数据库。等目标数据库基本追平以后,再用一个很短的切换窗口完成业务切换。
迁移前首先要搞清楚哪些数据真正属于核心业务
数据库迁移之前,不能只统计数据库有多少张表、多少GB数据,还要把应用与数据库之间的依赖关系梳理清楚。
例如一个电商SaaS平台可能同时存在用户、商品、库存、订单、支付、优惠券和物流等数据。用户资料迁移失败,可能只是个人信息暂时无法显示;但库存和订单之间的数据关系如果出现错误,就可能直接影响交易。
因此迁移前应该建立完整的数据依赖清单,明确哪些表属于核心交易数据,哪些属于统计数据,哪些属于缓存数据,哪些可以在迁移后重新生成。
同时还要检查数据库版本、字符集、排序规则、时区、存储引擎、索引、触发器、存储过程以及权限配置。很多迁移事故并不是数据没有复制过去,而是目标数据库虽然拥有数据,却与原来的运行环境存在兼容性差异。
全量迁移完成并不意味着数据库已经迁移完成
假设旧数据库中有5000万条订单记录,迁移程序花费两个小时把这些数据全部复制到新数据库。迁移结束时,旧数据库仍然在接受订单。
例如全量复制在上午10点完成,但是10点到10点30分之间又产生了几十万条INSERT、UPDATE和DELETE操作。如果这些变化没有同步到新数据库,那么新数据库实际上已经落后于生产系统。
因此生产环境中的数据库迁移通常需要分成两个阶段。
第一阶段是全量数据复制,把迁移开始之前已经存在的历史数据复制到目标数据库。
第二阶段是增量同步,持续捕获旧数据库在迁移过程中产生的数据变化。
以MySQL为例,可以利用binlog复制或者CDC技术持续捕获数据库变化。这样,即使全量复制需要几个小时,旧数据库仍然可以正常处理业务,而目标数据库则持续追赶最新的数据状态。
真正需要关注的是目标数据库距离源数据库还有多远
数据库迁移期间,工程师最需要监控的指标之一就是复制延迟。
假设旧数据库已经产生了最新的一批binlog变化,而目标数据库只同步到几分钟前的位置,此时就不能进行最终切换。
迁移系统至少应该能够持续观察全量迁移进度、增量同步延迟、数据库连接数、CPU、内存、磁盘空间、IOPS、复制错误以及应用请求成功率。
对于大型SaaS平台,还应该把迁移状态纳入现有监控系统。Prometheus、Grafana或者其他监控平台可以将迁移延迟、错误数量和数据库资源使用情况集中展示,并设置告警。
相比简单地显示“迁移完成”,一个能够准确告诉工程师“目标数据库已经追平到哪个位置”的系统更加重要。
双写并不是数据库迁移的万能方案
很多数据库迁移文章会直接建议应用同时写入旧数据库和新数据库,但双写实际上会显著增加系统复杂度。
例如用户创建一笔订单,应用先写旧数据库,再写新数据库。如果旧数据库写成功,而新数据库因为网络故障写入失败,就产生了数据不一致。
如果新数据库写成功,旧数据库失败,又会出现另一种不一致。
更复杂的情况是应用在两个数据库之间执行到一半发生崩溃,或者请求因为网络超时被客户端重新发送,导致同一笔业务被重复写入。
因此,除非系统本身已经具备成熟的双写、幂等、重试、冲突处理和一致性机制,否则不应该把“双写”简单理解成数据库不停机迁移的标准答案。
在很多迁移项目中,更容易控制的方式是让旧数据库继续作为生产写入源,再通过数据库复制或者CDC把变化持续同步到目标数据库。
分布式事务也不能自动解决迁移问题
Seata等分布式事务框架适用于特定的分布式业务事务场景,但数据库迁移本身并不等于分布式事务。
如果只是把一个MySQL数据库迁移到另一个MySQL数据库,并不需要因为迁移就自动引入分布式事务框架。
真正需要解决的是数据复制、变化捕获、顺序、幂等、校验以及最终切换。
如果业务本身同时操作多个数据库,例如订单系统、库存系统和支付系统已经形成跨数据库事务,那么才需要根据原有业务架构评估分布式事务或者可靠消息、事务消息、补偿机制等方案。
数据库迁移不能把所有问题都交给一个事务框架解决。
SaaS平台可以采用租户级灰度迁移
多租户SaaS平台还有一个非常实用的策略,就是不要一次性把所有租户切换到新数据库。
假设平台有一万家企业客户,可以先选择内部测试租户,然后选择几十家低风险租户进行迁移。如果运行稳定,再逐渐扩大到几百、几千个租户,最后才切换全部租户。
应用层可以根据tenant_id决定当前租户使用旧数据库还是新数据库。
这种方式的好处是风险被限制在一个较小范围内。假设新数据库存在某个特殊查询错误,也不会立即影响整个SaaS平台。
对于拥有大量租户、不同租户业务规模差异明显的平台,租户级灰度往往比简单地安排一个凌晨停机窗口更加具有实际价值。
数据校验不能只靠随机抽查
数据库迁移以后,必须验证数据是否完整。
最基础的检查包括表数量、表结构、字段类型、字符集、索引、约束和数据库对象。对于关键业务表,还需要进一步检查记录数量、主键范围、时间范围以及不同租户的数据数量。
例如某个SaaS平台有5000万条订单数据,就不能只随机抽查几百条,然后宣布“数据100%一致”。
可以根据租户、日期或者主键范围进行分批校验。对于特别重要的订单、支付和账单数据,还可以进行更加严格的逐记录比对。
此外还应该验证数据库迁移以后应用实际使用的查询。因为数据完整并不意味着业务完整,某个视图、索引、触发器或者字符集配置出现变化,都可能让应用出现异常。
最终切换应该尽可能短
假设数据库全量复制和增量同步已经持续了六个小时。此时目标数据库已经基本追平,数据校验也已经通过,才进入最终切换阶段。
首先暂停新的写入请求,或者让应用进入短暂的维护状态。随后等待最后一批增量数据同步完成。
确认目标数据库已经追平以后,再修改应用的数据库连接配置,让新的业务请求进入目标数据库。
最后重新开放业务,并立即观察订单创建、登录、支付、查询、后台管理以及API接口等关键功能。
这里需要特别说明,“数据库迁移用了六小时”和“业务停机六小时”是两个完全不同的概念。前者可能是在后台持续进行,真正影响用户的可能只是最后的数据追平和连接切换阶段。
切换完成以后不要马上删除旧数据库
迁移完成以后,旧数据库应该保留一段时间,而不是马上销毁。
因为新数据库开始承载生产业务以后,可能才会暴露一些原来测试环境没有发现的问题。
但旧数据库不能被简单地当成一个随时可以切回去的备份。
例如切换完成以后,新数据库已经产生了100万条新订单。如果此时直接把应用切回旧数据库,而旧数据库没有同步这些新订单,就可能造成严重的数据丢失。
因此真正的回滚方案必须提前考虑切换以后产生的数据如何处理。必要时需要设计反向同步、数据补偿、恢复备份或者业务层人工审核机制。
成熟的数据库迁移方案,不应该只有一句“出现异常立即回滚”,而应该明确什么情况下可以回滚、回滚以后数据从哪里来,以及切换以后新增的数据如何保证不丢失。
读写分离和分库分表不是迁移的必选项
读写分离、ProxySQL以及分库分表都可以成为大型SaaS架构的一部分,但它们并不是数据库不停机迁移的必要条件。
如果一家公司的数据库只是从旧服务器迁移到新的数据库集群,没有必要因为一次迁移就同时引入复杂的分库分表架构。
同样,增加一个数据库代理也不会自动解决数据同步问题。
迁移方案应该尽量控制变量。数据库迁移和数据库架构重构最好不要同时进行,除非业务规模已经要求必须同时完成。
对于几百万甚至上千万条数据的数据库,首先应该把备份、复制、校验和切换流程做稳定。只有当单数据库的容量、吞吐量或者业务隔离要求真正达到瓶颈以后,再考虑分库分表。
迁移前一定要进行完整演练
生产环境数据库迁移最忌讳“第一次操作就是正式迁移”。
应该先建立接近生产环境的测试环境,使用与生产数据库结构一致的表、索引和应用版本进行完整演练。
演练不能只测试数据导入,还应该测试全量复制、增量同步、数据校验、应用切换、数据库连接池、定时任务、消息队列、报表系统以及故障恢复。
如果正式迁移预计需要六个小时,测试环境就应该完整跑一次六小时级别的流程,观察磁盘空间、复制延迟、网络吞吐量和系统资源变化。
最终还应该形成明确的操作手册,把每一个切换步骤、负责人、检查项目和异常处理方式记录下来。
SaaS数据库不停机迁移的核心,不是找到一个所谓“零停机工具”,而是把整个过程拆成可以验证的阶段:先完成历史数据迁移,再持续同步生产变化,然后进行数据校验,最后利用很短的切换窗口改变业务的数据入口。
对于大型SaaS平台,最重要的也不是追求迁移速度,而是始终清楚哪个数据库拥有最新数据、目标数据库落后多少、哪些数据已经完成校验,以及切换以后发生故障应该怎样恢复。
当全量数据、增量变化、应用依赖、租户流量和回滚路径都能够被明确控制时,数据库迁移就不再是一场必须依靠运气完成的“大搬家”,而会成为一个可以演练、监控、验证和逐步执行的生产工程。
SaaS 数据库迁移过程中如何保证业务不停机
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP