1、 总体方案
OSS需要一台不提供服务从库辅助完成,通过这台从库建立数据快照,快照数据按照新的路由拆分规则分别导出,导出完成后创建trigger记录增量数据,
将导出的分片数据分别导入拆分后对应的新表并以此作为基准数据,导入完成后对增量数据进行回放,总体流程方案如图所示。
2、 怎么新老表数据的一致性
实现数据的一致性需要有些辅助工具。
(1)心跳表
心跳表作用是判断新表创建、基准数据导入及回放的增量数据是否同步至从库的依据,心跳表数据永远为一行记录,完成相关操作时,主库上对心跳表id字段加1操作,如果主从读取心跳表所获取的id值相同,那么就认为主从同步结束。
(2)触发器
将数据有从库的老表导入到主库的新表过程,从库上中断sql同步线程,创建触发器,所有对原表DML操作的记录都需要通过触发器记录至增量表中。
(3)增量表
将新表的数据导入老表的时候记录基准数据之后,对增量数据回放到新老表来保证新旧表间数据同步。
保证数据的一致性做法如下图所示:
在主库中创建新表和心跳表,然后将不提供服务的从库中老表的数据按照分库分表的路由规则分发到主库的新表中;与此同时从库中会创建一个增量表,用于记录迁移过程中数据库的增量
INSERT、UPDATE、DELETE操作,当老表中的数据全部导入到新表后,会校验新老表数据的一致性,数据一致以后,开始将增量表的数据重放到新老表中,当增量数据少于1000条时会锁住主库
中的老表,防止不断产生新的增量,同时把剩余的增量数据重放到从库的老表以及主库的新表中,从而达到新老表数据的一致。
3、怎么不影响线上服务
基准数据导出方是一个不提供服务的从库,主库的新表也不想外提供服务,最多在导入数据的时候影响到主库的IO,如果导入频率控制好的话对线上基本无影响。
4、 怎么保证业务迁移到新表的过程中不影响线上服务
很遗憾,OSS做不到这一点,因为数据迁移之前,业务方使用的是老表的数据,当数据迁移到新表以后,为了保证新老表数据的一致性,必须停止服务,然后业务方上线对新表的操作才可以,业务方在全推过程中不能
对外提供服务,对于像钱包的payserver,从验证到全推的过程至少1个小时,而停止一个小时的服务是不现实的,而这一点在下面的提供的方案中得以解决。
总结: 这个方案有他的好处,此方案是有OSC方案衍生而来的,能完保证数据的一致性,虽然在数据迁移过程中不影响线上的服务,但是在数据迁移后需要停止服务完成和业务方的对接,不能保证平滑迁移。
但是该方案迁移周期比较短,如果对线上服务影响要求不高,并且对迁移时间要求比较高的情况下建议使用该方案。
此方案的详细信息见附件:MySQL Online Schema Sharding详细设计.docx
MySQL NEW Online Schema Sharding 简称 NOSS)
1、 总体解决方案
NOSS方案需要业务方双写,并且同时需要通过单独的拉库操作来达到数据的一致性,通过控制拉库的速度从而控制对线上的影响,总体方案如下图所示,
除了选取拉库所需要的主键以及数据的一致性校验,其他的所有的操作都是在主库上执行。
2、怎么保证新表的数据和老表的数据一致
(1) 双写
对于双写后新产生的数据,通过新老表双写达到新老表数据的一致性。
(2)拉库
通过拉库,将双写前的数据从老表中按照路由规则分发到新表中。
注意:双写在事务中,业务会对记录加锁,拉库也是一样的需要在事务中并且对将拉去的数据进行加锁。
(3)数据的校验
当拉库脚本结束以后,会校验新老表数据的一致性,首先找一个从库断开和主库的同步,然后将从库中的新老表数据进行一致性校验,从而判断数据的一致性。
3、怎么保证不影响线上服务
(1)控制拉库的速度
在拉取数据的时候要选取一个合理的拉库速度,方法是拉库脚本刚开始会以一个比较小的速度的拉取数据,然后每隔一段时间增加一些速度;当然脚本会配置拉取速度的预期上限,
当速度达到预期时,速度不再增长,如果在达到预期前产生了对数据库的影响则停掉拉库脚本,记录下产生问题时的拉取速度,然后选取一个比这个速度小的速度作为拉取速度拉去数据。
(2)控制每个事务处理的数据量
拉库时每个事务处理的数据量要做限制,防止事务太大执行时间太长,当双写中的记录和拉库事务中的记录冲突时等待时间过长导致事务回滚。
通过控制这两点能达到对数据库较小的影响,从而不影响线上服务。
4、怎么保证业务迁移到新表的过程中不影响线上服务
业务方可以通过多次上线达到不影响线上服务的目标,上线流程分为3步
(1)第一轮业务方上线
此时业务方上线过程对新老表进行双写,但是提供给业务方数据的是老表。
作用:1. 保证新老表数据的一致性
2. 用老表数据提供给业务方是因为新表的数据还不全
(2)第二轮业务方上线
在老表中的数据迁移到新表以后,并且数据一致性得到验证后开始上,此时仍然是双写,但是提供给业务方数据的是新表。
作用:1. 防止有别的使用老表的业务方没有及时更新自己的业务
2. 用于老表向新表过渡,并且可以当新表出错时用老表提供服务。
(3)第三轮业务方上线
如果第二轮业务方上线后,新表提供的数据稳定,并且其他业务方也不再使用老表,那么下掉对老表的所有操作。
作用:完成业务方获取数据从老表到新表的过渡
此方案可以解决之前提出的所有问题,但是也有他的缺点,数据迁移由于有速度限制,所有数据迁移的时间比OSS时间长,业务方分三次上线,那么整体完成由老表到新的的过渡周期
比较长,所以,如果保证线上服务要求比较高,并且对迁移时间要求不是很紧急的可以使用该方案。
下面介绍一下这个方案在coredb收款表拆分上的应用:
四 实例分析
项目背景
百度钱包随着收款业务的增长,收款表的记录数目迅速增长,由于历史原因,收款表是单表,mysql单表数据量过亿就会对服务造成很大的影响,项目开始阶段已经达到7千万左右,
并且当时的增长速度一个月大约一千多万行数据,收款表拆分迫在眉睫。
项目中遇到的问题
一、 收款表怎么拆分
收款表拆分的过程中有两种方案
1. 按照时间维度分库分表
优点: (1)数据可以按照时间归档十分方便
(2) 数据库表不会突然猛增,因为按照收款表的后三位分库分表一下就要分出1000张表, 而通过月份分库分表分表一年只有12张表,需要80几年才会达到1000张表
(3)如果按照收款表的后三位分库分表,按照时间维度查询需要便利1000张表,如果按照时间维度分库分表,按照时间查询可以直接使用索引定位到时间范围内的表,不需要便利所有的表。
缺点: (1)数据不均匀,某个月如果做活动,数据量会暴增,并且随着业务的不断扩大,单月收款表的数据量可能达到一亿。
(2)由于某段时间数据量暴增,会造成这个表成为热表,影响数据库操作
2.按照收款单后三位分库分表
优点: (1)数据会均匀分布在1000张表里,不会造成短时间内所有表数据量达到1亿
(2)和其他表的分库分表规则一样,容易维护
缺点: (1)数据表一次I性增长2000张(还会有一个map表)
(2)按照时间条件查询必须便利所有表
虽然按照时间分库分表有他的好处,但是按照这个维度分库分表最大的致命点就是,数据分布不均匀,并且可能会出现一个月收款表的数据量达到1亿的可能,如果出现这种情况数据库就需要重新改造,代价太大。
二、 数据怎么迁移
数据无疑是由老表向新表迁移,并且迁移需要分两步才能完成
1. 按照create_time字段查询出主键
为什么要选取主键,因为第二部迁移需要按照这些主键将老表中的数据高效的选取出来,如果不用主键选取效率会降低,按照create_time字段查询主键是因为迁移的数据是一段时间内的数据,时间可以作为参照
,并且create_time字段是一个不变的字段,防止在迁移过程出现数据修改而照成迁移数据不准确,比如按照某些可变字段选取如modify_time那么在limit的时候由于字段改变照成数据变化而limit数据不准确。
2. 根据上一步选取的主键在老表中查出数据插入到新表中
通过第1步选取出来的数据作为输入,在老表中选取出来这个主键对应的记录,然后replace into插入到新表中,这所有的操作在一个事务中并加锁完成,后面会说明为什么这么做
这两部分别有两个脚本来完成,我们这里叫做拉库脚本。
为什么不直接查出数据插入到新表中
(1)不能直接把所有的数据选取出来一下子插入到新表中,因为对数据库的操作需要频率控制,如果频率过快会影响线上服务
(2)为了保证数据的一致性,在第2部迁移数据需要使用事务和锁来保证数据的一致性,所以先选出主键通过主键才能高效的在从老表中查数据插入到新表中。
三、怎么保证数据的一致性
以拉库脚本的输入时间为分割点,分割点之前数据的一致性由拉库脚本来保证;分割点以后的数据一致性由payserver的新老表双写来保证;当然这些数据的一致性都会有检验机制。
分割点:
分割点是这样的时间,是payserver全部上线以后的时间点,而不是在payserver上线过程中新老bin文件存在的某个时间,后面会解释分割点为什么选取这个点。
1. 拉库脚本
老库脚本上面已经介绍过了,之所以使用事务就是为了保证在对新表的数据一致性,由于老表中被拉取的数据在被拉取过程中可能被修改,比如某个记录的状态被改变,以这个为例子讲一下
怎么保证这些数据的一致性:
假如某条数据在新表中没有,在老表中存在,那么会出现两种情况,一个是payserver双写的事务先执行,另一个是拉库脚本中的事务先执行。
(1)payserver双写的事务先执行
如果payserver的双写先执行,那么在更新老表会成功,而更新新表的时候会失败,这时候老表中的数据是正确的,但是新表中的数据是错误的;这时候执行拉库脚本中的事务,老表中正确的数据会被
replace into到新表中,这样新表中的数据也是正确的了。
(2)拉库脚本中的事务先执行
如果拉库脚本中的事务先执行,那么老库中的数据会被插入单新表中,由于老表中的数据是正确的,从而保证新表中的数据也是正确的;如果这时候payserver的双写事务被执行,那么新老表中的数据被同时修改,数据依然是正确的。
2. payserver的双写
在分割点以后的数据通过双写来保证数据的一致性,原因是由于双写之后老表中有但是新表中没有的数据由拉库脚本保证了数据的一致性;对于原来老表中没有的数据新表中也没有,在双写的过程中会在同一个事务中对新表和老表进行
插入和更新操作,保证新表表中的数据和老表中的数据一致。
3. 数据的一致性校验
(1)对于由拉库脚本拉去的数据一致性校验方法是这样的,当老库脚本运行结束以后,将新表中拉库脚本产生的数据做校验;停止一个mysql从库与主库的同步,然后将新表中的数据按照分库分表的原则分别放在1000个文件中,同理老表中的数据,也按照这个原则放在1000个文件中,然后对所有的文件进行sort操作,操作成功以后逐一进行md5值校验,若果都相同那么数据一致性没问题。
如果校验发现md5值不一样,首先要回顾这个流程是否出了问题,如果问题不大,那么会将md5值不同的文件中的不同条目选取出来,由老表中的这条数据代替新表中的这条数据,如果问题比较大,有可能任务失败。
(2)对于有payserver双写保证数据的一致性校验是这样的,在payserver的代码中,在同一个事务里如果新表操作失败但是老表操作成功会打log作为提示,所以只要程序不打印失败的log,那么证明payserver双写的数据一致性已经保证,如果出现问题,那就是应用程序有问题了。
四、怎么保证不影响线上服务
(一) 不影响payserver的服务
为了不影响payserver的正常服务,payserver分三个版本依次上线,原因如下。
1. 第一轮payserver上线
第一轮payserver上线的主要作用是为了保证新老表数据的双写。
(1) 对于老表的操作
对老表的操作保持不变,业务中用到的数据是从老表中选出来的数据。
(2) 对新表的操作
插入: 老表插入的数据在新表中也插入,如果插入失败会报错
更新操作:如果新表中数据存在,那么直接更新
如果新表中不存在,不报错,但是会打印提示信息
选取操作:如果新表中有数据,不做任何处理,如果新表中没有数据,不报错,但是会打印提示信息
回滚方案:回滚到上一个版本
(3)逻辑修改以及原因
主要的修改思路增加对新表的操作,包括load,insert以及update,但是新表在select以及update找不到数据的时候根据错误码判断是是数据不存在,还是数据存在只是操作失败,并打印相关的log,系统并不报错,但是insert操作失败需要报错。
这样做的目的是,当payserver上线时,老表中存在的数据新表中不一定存在,所以select和update这些新表中不存在的数据时,对新表的操作会失败。
(4)在三中怎么保证数据的一致性中分割点的选取在这里解释一下
分割点之所以选取到payserver全部上线结束的时间点原因是当payserver上线的时候,会出现新老bin文件同时存在的情况,其中新文件对新表进行插入和更新操作,但是来的bin文件没有对新表的操作,这样就会出现一个问题,当某个插入操作
落到老的bin文件上时,在老表中会插入这条数据,但是在新表中却没有这条数据;但是刚好更新操作却落在了新的bin文件上,这时候对这个在上相过程中产生的老表中存在但是在新表中不存在的记录就会出现数据的不一致,到那时当新的bin文件
上线结束,那么就不会出现这种数据不一致的现象,也就是上线结束以后payserver的双写可以达到数据的一致性。
2. 第二轮poayserver 上线
第二轮payserver的上线有两个作用:
(1)是业务方使用的收款表数据由老表平滑过渡到新表
在这里对新老表的操作与第一轮操作刚刚好相反,对于插入操作失败新老表都报错,但是对老表更新操作失败但是对新表更新操作成功不会报错,只是打印log,这样业务方使用到的数据就是从新表中选出来的数据了。
(2)保证不影响payserver的服务
为什么不一下子去掉对老表的操作呢? 假如去掉了对老表的操作那么在上线过程中会出现以下问题:
上线过程中还会出现新老payserver同时存在的情况,插入请求落在新的payserver,此时数据会在新表中插入,但是老表中没有这条数据,但是刚好更新操作落在了老的payserver上,这样当老的bin文件去select以及update这条在新表中存在 但是在老表中不存在的数据时,由于这条数据在老表中不存在会出错FATAL,会影响支付成功率。
3. 第三轮payserver上线
第三轮payserver上线主要目的就是用新表完全替代老表,下掉对老表的操作,也就是在payserver的逻辑中去掉对老表的所有操作。
(二) 不影响业务方的服务
不影响业务方的服务主要做到以下两点
1. 排查对业务方的影响
(1) 把分库分表的项目向业务方说明,他们会根据情况排查自己负责的模块,并将受影响的模块反馈给payserver
(2)只是业务方的排查还是不够的,因为有些脚本的负责人可能没有通知到,或者有些模块虽然是他们负责,但是由于没有出现过问题,或者是新人多没有接触到也不知道这个事情;所以需要我们负责再次排查一遍,
比如排查收款表使用的业务时,我在 db-eb-pay-fn00.db01.baidu.com 排查了所有的线上业务,结果真的找到了一些之前业务方没有反馈 给payserver的业务。
2. 业务方配合payserver工作
(1)业务方根据payserver的改动修改自己的业务逻辑
(2)严格控制上线步骤,不要产生由于上线顺序造成的业务影响。
五 上线步骤
附加中有这个方案的上线步骤说明在这里不再累述 payserver_1.1.63_详细设计_收款表分库分表详细设计.docx
六 为什么选择了这个方案
大家会发现这个方案上线过程很复杂,流程很麻烦,但是为什么要选择这个方案,难道没有更好的方案吗?
期初是有四套方案供选择,但是最终确定了这个方案,这里简述一下为什么没有选择其他的方案。
方案一 :
这个方案与最终方案流程基本类似,最终方案也是在这个方案上做了优化,这里不再累述
方案二:
方案二采用的是DBA使用的MySQL Online Schema Sharding,之所以没有选择这套方案
(1) 这套方案并不是很成熟,存在bug并且维护这个方案的RD最近辞职,选择这套方案危险比较高
(2)这套方案需要停止服务一段时间,而这段时间可能比较长,是payserver不能容忍的,因为支付行业不同于其他产品,涉及到资金的流通,不允许超过五分钟的停服务时间。
方案三:
主要思路是通过binlog重放来达到新老表的数据一致,但是这也需要停止服务去完成新老表的数据一致,而且这个时间也是不能被接受的。
综上: 其他方案都需要停止payserver的服务,而且停止服务的时间都大于5分钟,这样不可以被接受,所以最终选择了现在这套方案。