上亿级别数据迁移方案 - 高飞网
455 人阅读

上亿级别数据迁移方案

2017-07-28 02:09:46

一、背景

    我们有两个表,分别用于存储用户行为数据和行为状态,但由于数据量比较大,会影响查询性能,因此对表进行了分表,由原来1张表分为100张表。但是由于此种分表策略(user_id % 100)难以扩展(如果想扩展为200张表,则需要将原来100张表所有数据重新散列到200张表中,工作量很大),所以现在策略了一致性哈希算法分表。先来看看现在两张表长啥样(限于商业保密,笔者对表名及字段名做了更改,但不妨碍理解):

表1:user_action_0


表2:user_action_status_0



    其中第一张表user_id和data_id为一个唯一索引;第二张表中action_id与status为唯一索引。其中action_id即为第一个表的id。

    新的分表策略采用一致性哈希算法,关于一致性哈希,网上有若干博文可以查阅,此处不展开来讲,如:
五分钟理解一致性哈希算法(consistent hashing)

    我们的实现思路是,将分表shard字段(这里是user_id)作md5,结果是一个32位的16进制字符串,然后取其中4位,前3位转为数字,后一位转为数字,则前3位正好是163=4096种可能,后一位16种可能。4096种可能表现为环形哈希的4096个桶,而这些桶最多可以存放4096个库,初期我只有16个库即可,后一位的16种可能,作为一个库中的16个分表。这样下来,本次要将之前100张表的数据,洗到16*16=256个表中。

    目前user_action_0-100表中有1.8亿条数据;user_action_status_0-100中有3.5亿条数据。共77G的数据。并且每天以百万的量增加。

二、思路&方案

    了解上面的背景以后,来看看如何设计方案。

   1、建库:创建新的数据库与分表
   2、程序:将现在的程序改为新的分库分表策略
   3、全量:从旧库旧表中全量移植数据到新库新表
这里停顿一下,当移植数据时,user_action_n没有什么问题,只要从旧表取出数据,根据user_id按照新的策略,将数据插入到新库新表即可。但是user_action_status_n表中的action_id依赖于前表,在读取了状态数据以后,不能直接根据新策略插入到新表,因为其中的action_id还是旧的。
    关于这个依赖问题,可以有以下的方案:
    1)两个表同时进行迁移,即插入action表,成功后获取id;读取以该条记录为id的status数据,查询并更新其中的action_id,将状态数据插入到新库新表中。
    2)两个表独立插入,action表插入时,维护一个中间表,保存新旧的action_id的映射关系,插入status表时,再从这个中间表获取。
    3)两个表独立插入,action表插入时,主键不再用自增,而是让主键加上一个偏移量,100张表的主键出现重复的可能,再插入status表时,更新其中的action_id时只需要一个很少的函数计算就能得到新的action_id。简单说下这个偏移量的计算,假如有100张表,其中最大的表大小为100万,则第一张表主键id+0迁移,第二张表主键id+100万,第三张表主键id+2*100万...以此类推。这样,将旧表中数据重新散列到新库以后,就不会出现主键的重复问题。
    4、增量:背景中说过,数据并不是静止的,每天都在发生变化,那就要有一个增量程序在全量结束以后,把新增加的数据迁入新库,这个也可以有下面的方案:
    1)全量结束以后,从旧库中扫表,将大于某个id或时间点的数据移植过来,但这个有个缺点,100个表如何去扫?扫完user_action_0再去扫user_action_1的时候,0可能又增加了不少数据。
    2)建一个change_log表,旧库在线上写入时,同时往这个表中记录增删改查的行为(包括操作类型,操作表,主键id),如action表的insert、update。这样全量以后,只要从这个表出发,就可以简单而高效地全表扫描了。

    5、检查:数据迁移完成之后,测试是难免的,让QA找那么20来个用户数据进行验证,但人的力量终究太小了。因此需要一个检查程序
    1)读取旧库分表条目数加和,新库的分表条目数加和,看差别的大小;
    2)从旧库的每个表中取出1w条数据,与新库中的数据进行按字段比较

以上就是这次迁移的整理思路,下面理一下先后顺序:

1、建库建表,建库建表(包括change_log表)
2、将线上程序,在每次写入action、status表数据时,插入change_log表,上线以后观察数据是否正确
3、编写全量移植程序、增量移植程序、检查程序
4、将线上程序,去掉写入change_log表,改为新的分库策略(这是最终上线版本)
5、在线下进行全量、增量的程序测试,尤其是全量,要能根据线下估算出线上的大概执行时间,能否全部执行完成(会不会在中间因为内存等问题歇菜),日志是否足够(日志非常重要)。
6、测试没问题后,在线上进行数据移植,最后上新程序。

三、代码设计

    根据上面的思路,来设计代码,先看结构:

├── all        ———— 全量
├── bean       ———— 存放用到的中间Java对象
├── check      ———— 检查程序
├── common     ———— 存放一些如SQL语句,工具类等
├── dao        ———— 数据操作层
└── incr       ———— 增量

    由于数据量比较大,单线程边读边写性能将非常差,因此这里要用到多线程,使用线程池控制线程在合理的数量;另外迁移程序的边读边写非常适合用生产者-消费者模式来做,因此要用到阻塞队列来保存中间数据。

private final LinkedBlockingQueue<OldEntity> dataQueue = new LinkedBlockingQueue<OldEntity>(1000);

    如上用LinkedBlockingQueue数据结构作为数据池,读取数据使用put()方法将数据放入队列,当数据满1000时,读线程将被阻塞;写库线程从队列中通过task()方法拿出数据,当了队列为空时,写线程被阻塞。

    另外,为了避免在数据迁移完时,写线程无限等待下去,可以在读取完所有的数据以后,在队列中设置一定数量的“毒瘤”,如放置OldEntity时故意将id设置为null,这样在拿出数据时,检查下这个状态,如果拿到这样的数据,就退出循环体。

    在线下测试发现,读取数据是一个非常快的操作,而相对面议,写将慢的不是一个档次,因此在设计线程组的时候,要记住一读多写,具体多少个写,要看你的线程数。多线程数的设计,以保证最小的线程切换,这块可以用java/bin目录下提供的jvisualvm来进行性能测试,如下图:


图中展示的就是在jvisualvm中看到线程使用情况,这里我有了四组线程(1读+n写为一组),每组有一个读线和4个写线程,可以看到,即使这样,读线程大部分时间仍然在等待(黄色),而写线程一直在繁忙地写库。另外执行期间也要观察内存状态,尤其要确保没有内存溢出情况发生,即old区不会涨:


如果发现old区域不断上涨,有可能在诸如没有关闭prepareStatement等引起的。

四、上线及问题

     线程数不能太大:按最初的设想,是通过配置线程组和写线程数来提高程序的执行速度,但是忘了线的数据库还在支持线上业务,因此不能任性的使用太多线程,最终使用线下测试时1/4的线程数据,用38个小时完成了数据移植。

    主从延迟:由于线上数据库采用的是一主二从,为了不影响线上的业务,从从库中读取数据写入到新库的主库中。但是由于新建的库与老库在一个实例下面,导致从库同步主库的数据异常缓慢(因为从读同步主库sql log以串行的方式写入数据),大根延迟了10000s,即近一天的数据,这样导致专门从从库读取数据的业务不能正常读取到实时的数据。

还没有评论!
23.20.166.68