0 前言
总会遇到更换DB问题:
整个迁移过程,不能长时间停服,也不能丢数据。如何不停机安全迁移数据更换DB呢?
设计迁移方案,要做到,每步都可逆。要保证,每执行一个步骤后,一旦出现问题,能快速回滚到上一步。看订单库案例。
先把
旧库还在线,所以不断有订单数据写旧库,不仅要往新库复制数据,还要保证新旧库数据【实时同步】,就要用
实现新旧两个数据库实时同步。
Binlog实时同步数据。若源库不是MySQL就麻烦,但也可参考复制状态机理论实现。这步不需回滚,只增加一个新库和一个同步程序,对旧库和程序没任何改变。即使新上线的同步程序影响旧库,只要停掉同步程序。
graph LR
订单服务[订单服务] <-->|读写| 旧库[(旧库)]
旧库 -->|数据同步| 实时同步程序[实时同步程序]
实时同步程序 -->|数据写入| 新库[(新库)]改造订单服务,业务逻辑部分不变,DAO层改造:
订单服务仍只读写旧库,不读写新库。让这【新订单服务】稳定运行至少1~2w,期间除验证新订单服务稳定性,还要验证新、旧两个订单库中的数据是否一致。该过程,若新版订单服务异常,可立即下线新订单服务,回滚到旧订单服务。
稳定一段时间后
这个双写的业务逻辑,一定先写旧库,再写新库,并以写旧库结果为准。
即原则上不能让新库影响现有业务可用性和数据准确性。上面这过程若异常,可关闭双写,回滚到只读写旧库。
切换到双写后新、旧库数据可能存在不一致:
graph LR
订单服务[订单服务] <-->|读写| 旧库[(旧库)]
订单服务 -->|写入| 新库[(新库)]
旧库 -->|数据比对| 比对/补偿程序[比对/补偿程序]
比对/补偿程序 -->|补偿写入| 新库开启双写后,还要至少稳定运行几w
确保不能有旧库写成功,新库写失败的case。对比程序也没发现新旧两库数据有不一致,这时就可认为,新旧两库数据一直保持同步。
接着
期间若异常,可再切回旧库。全部读请求都切换到新库后,读写请求就已经都切换到新库,实际切换完成。
再稳定一段时间后
停掉对比程序,把订单服务的写状态改为【只写新库】,旧库可下线。整个迁移过程中,只有这步不可逆。但这步主要操作就是摘掉不再使用的旧库,对在用的新库无改变,异常可能性已很小。
就完成在线更换DB的全部流程。双写版本的订单服务也就完成了它的历史使命,可在下一次升级订单服务版本时,下线双写功能。
对比两个在随时变化的DB中的数据。没有类似复制状态机这样理论上严谨实际操作还很简单的方法。但还可根据业务数据实际情况,针对实现对比和补偿,经一段时间,把新旧两个DB差异,逐渐收敛一致。
订单这类时效性强的数据,方便对比和补偿。因为订单一旦完成几乎不会再变,对比和补偿程序,就能依据订单完成时间,每次只对比这时间窗口内完成的订单。补偿逻辑简单:发现不一致,直接用旧库订单数据覆盖新库订单数据。
切换双写期间,少量不一致订单数据,等订单完成后,会被补偿程序修正。后续只要不是双写时,新库频繁写失败,就能保证两库数据完全一致。
如商品信息随时可能变化。若数据上有更新时间,对比程序可利用这更新时间,每次在旧库取一个更新时间窗口内的数据,去新库找相同主键的数据对比,发现数据不一致,还要对比更新时间。若新库数据更新时间晚于旧库数据,可能对比期间数据发生变化,这种情况暂不补偿,放到下个时间窗口继续对比。时间窗口的结束时间,不要选取当前时间,而要比当前时间早点儿,如1min前,避免对比正写入的数据。
若数据连时间戳也没,只能去旧库读取Binlog,获取数据变化,去新库对比和补偿。
这些方法严格推敲都不是百分百严谨,都不能保证在任何情况下,经过对比和补偿后,新库数据和旧库完全一样。但大多数情况下,这些实践方法还是可以有效地收敛新旧两个库的数据差异,酌情采用。
设计在线切换数据库的技术方案,首先要保证安全性,确保每一个步骤一旦失败,都可以快速回滚。此外,还要确保迁移过程中不丢数据,这主要是依靠实时同步程序和对比补偿程序来实现。
切换过程按顺序:
Q:整个切换方案仅一步不可逆:双写切换为单写新库。若不计成本,咋修正迁移方案,让这步也快速回滚?
A:不可逆主要因为,一旦切新库单写,旧库数据就和新库不再一致,无法再切回旧库。所以,关键是,切到新库单写后,需保证旧库数据和新库保持同步。双写得新增一种过渡状态:从双写以旧库为准,过渡到双写以新库为准。再把比对和补偿程序反过来,用新库数据补偿旧库数据。就能做到,一旦出问题,再切回旧库。
也有其它实践:
Q:这种数据同步架构下,若下游某同步程序或DB异常,需要把 Binlog 回退到某个时间点然后重新同步,咋办?
A:若下游只有一个同步程序,直接按时间重置Canal实例的位点。但若MQ下游有多个消费者,就不能重置Canal里的位点,否则影响其它消费者。在MQ的消费订阅上按时间重置位点,这只影响出问题的那个订阅。所以,这种架构下,MQ中的消息,最好将保存时间设置长一些,如3天。
去O时设计的一个方案和这个很相似。需要将56亿oracle的财务数据迁移到mysql中并保证一直提供稳定的线上服务。方案主要涉及: 1.设计同步worker以6亿/天的速度同步至MySQL,预先按照1000的间隔生成560w个任务,用于保证数据同步不丢失 2.MySQL业务数据库为防止和oracle中id碰撞,设计id生成器。前三位为库编号 3.因为高达100w/分钟的事务量,设计了前置防重表+任务表(按UUID分库),用于防重、规避热点和异步增加吞吐量 4.业务数据库通过在前置库生成的id分库,同时会异步同步至ES 5.原接口服务改造,写老库数据和发送MQ在一个事务(mq挂了会影响服务)。提供新接口服务写新库并发MQ 6.接收端消费MQ并根据标示同步至老库或新库 7.提前定义好一个阈值单号,比如当前数据已经累积到57亿,那我们可以根据增速估算一个阈值58亿。在上线后只有58亿以后的数据会通过mq同步至老库的。以前的数据都由worker同步(在适当时候:如目前单号已超过58亿,生成另外20w个任务,并开始执行另外两亿数据的同步,期间新库会缺少这两亿数据) 8.MQ同步稳定一段时间后,用新接口灰度替代老接口,在整体稳定后全量切换至新接口,并用MQ同步老库
Q:无限层级(每条记录都对应一个父id)怎样设计能够快速查询,之前设计是存一个path字段,用like查询,总感觉这样设计不优雅。
A:考虑使用关系型数据库中的递归查询(Recursive Query),如MySQL的WITH RECURSIVE语句、PostgreSQL中的WITH RECURSIVE语句。可快速地查询出某记录的所有子孙记录,同时避免了LIKE查询需扫描大量记录。
Q:热切开关具体什么方式实现?
A:一般编码实现。可对外暴露一个可供调用的接口或通过动态配置下发等方式触发。
Q:线上大表怎么在不影响业务的情况下增加字段? 我能想到的在从库增加字段,等从库同步追上来时,切换主从。
A:大表加字段过程中会锁表,期间所有写操作都会阻塞。若能接受业务停摆,直接加还是最方便。
开启双写和停止数据同步服务这一步需要停服或者锁库,不然会有数据不一致风险。
Q:双写阶段,会不会存在两份数据id不一致的情况(如MySQL自增ID),若下游有依赖这个表的ID,一旦切换就无法回退。很多公司的数仓表就是这样!
A:建议以一边的自增ID为准。
Q:既然有比对和补偿程序,能否不使用数据实时同步。先上线观察双写和补偿程序,没问题后,先进行数据从旧到新的快照复制,再开启双写,因有缝衔接而丢失的数据通过补偿程序来做?
A:考虑到很难实现一个完美对比补偿程序,推荐别这么做。
Q:现有系统的库存和订单耦合在一起,库存操作和订单状态更新在一个事务,现在要把库存迁出去作为一个独立服务。
组里方案:双写时发送RocketMQ事务消息,异步消费操作新库存。但后来发现库存操作对顺序有要求,如两个订单分别预占不同库位的库存,所以在消息中指定库位信息,消费时按库位跟新库存交互。
又遇到问题:双写和最终单写新库存的逻辑不一致,单写新库存预占库存的策略由库存控制,而非业务系统,所以担心全切过去后出现问题。
我现在想法:异步+修改调用规则(指定库位信息),这会导致单写新系统没有得到充分验证,不如开始时直接拆分事务做同步双写,对库存操作成功单据没更新状态的情况做监控报警,好处是调用逻辑完全一致,同步操作新库存出故障的风险通过开关和灰度来控制。有更好方案?
A:想法可行,可先同步双写验证新库存系统正确性,同时对库存操作成功单据没更新状态的情况进行监控报警,确保数据一致性。在验证通过后再逐步切换为异步+修改调用规则的方式,以降低同步操作新库存出故障的风险。
想避免同步操作新库存出故障风险,考虑Seata、TCC,避免双写和最终单写新库存的逻辑不一致问题,并保证数据一致性。但分布式事务也带来性能开销和复杂度,权衡!
Q:修改线上DB的表结构?原方案:建新表,把旧数据倒过去,然后新旧表分别改名。但数据一致问题较麻烦,改名过程中也易出错。
A:一般很少去删除表的字段,只增加字段。这样,新表能兼容旧表,也不用迁移数据。
Q:阿里云MySQL同步到RDS,需做啥?校验程序也必须?
A:考虑实施成本VS数据重要程度来决策。