cassandra百亿级数据库迁移实践

迁移背景

cassandra集群隔段时间出现rt飙高的问题,带来的影响就是请求cassandra短期内出现大量超时,这个问题发生已经达到了平均两周一次的频率,已经影响到正常业务了。而出现这些问题的缘由主要有如下3点:mysql

  1. 当初设计表的时候partition key设计的不是很合理,当数据量上去(最大的单表行数达到百亿级)以后,出现了一些数据量比较大的partition。单partition最多的数据量达到了上百万行(cassandra不支持mysql的limit m, n的查询),当查询这个partition的数据时,会带来比较大的压力。
  2. cassandra自己的墓碑机制,cassandra的一大特性就是快速写入,若是遇到delete一条记录时,cassandra并不会实时的对这条记录作物理删除,而是在这行记录上添加一个逻辑删除的标志位,而下次查询会load出这些已经删除了的记录,再作过滤。这样就可能带来及时某个partition的查询出的数据量不大,可是墓碑比较多的时候会带来严重的性能问题。
  3. 公司dba也不推荐使用cassandra,出现问题的时候,难于定位解决问题。因此决定将cassandra数据库迁移至社区比较成熟的关系型数据库mysql。

迁移方案

整个迁移方案主要分为如下5个步骤:程序员

  1. 全量迁移:搬迁当前库中全部的历史数据(该过程会搬掉库中大部分数据)
  2. 增量迁移:记录全量迁移开始的时间,搬迁全量迁移过程当中变动了的数据
  3. 数据比对:经过接口比对cassandra和mysql中的数据,最终数据一致性达到必定99.99%以上
  4. 开双写:经过数据比对确保全量迁移和增量迁移没问题之后,打开双写。若是双写有问题,数据比对还能够发现双写中的问题。
  5. 切mysql读:确保双写没问题之后,而后根据服务的重要性级别,逐步按服务切mysql读。全部服务切mysql读之后,确保没问题后关闭cassandra写,最终下线cassandra。

mysql的分库分表方案

  1. 分多少张表?在DBA的推荐下,单表的数据最好不要超过200w,估算了下最大一张表数据量100亿左右,再考虑到数据将来数据增加的状况,最大的这张表分了8192张表,单表的数据量120w左右,总共分了4个物理库,每一个库2048张表。
  2. 字段对应的问题? 这里须要权衡一个问题,cassandra有List、Set、Map等结构,到mysql这边怎么存?这里能够根据本身实际状况选择,
    • 集合结构的转成json以后长度都在1000个字符之内的,能够直接转成json用varchar来保存,优势:处理起来简单。缺点:须要考虑集合的数据增加问题。
    • 转成json以后长度比较长,部分已经达到上万个字符了,用单独的一张表来保存。优势:不用考虑集合的数据增加问题。缺点:处理起来麻烦,须要额外维护新的表。
  3. mysql分片键的选择,咱们这里直接采用的cassandra的partition key。
  4. mysql表的主键和cassandra表保持一致。

全量迁移方案调研

  1. copy导出:经过cqlsh提供的copy命令,把keyspace导出到文件。 缺陷:sql

    • 在测试过程当中,导出速度大概4500行每秒,在导出过程当中偶尔会有超时,导出若是中断,不能从中断处继续。
    • 若是keyspace比较大,则生成的文件比较大。因此这种方式不考虑
  2. sstableloader方式:这种方式仅支持从一个cassandra集群迁移到另外一个cassandra集群。因此该方式也不考虑数据库

  3. token环遍历方式:cassandra记录的存储原理是采用的一致性hash的策略<br>整个环的范围是[Long.MIN_VALUE, Long.MAX_VALUE],表的每条记录都是经过partition key进行hash计算,而后肯定落到哪一个位置。json

    • 例若有这样一张表:
    CREATE TABLE test_table ( a text, b INT, c INT, d text, PRIMARY KEY ( ( a, b ), c ) );
    • 经过如下两个cql就能够遍历该张表:
    cqlsh:> select token(a,b), c, d from test_table where token(a,b) >= -9223372036854775808 limit 50;
    
    
     token(a, b)          | c | d
    ----------------------+---+----
     -9087493578437596993 | 2 | d1
     ...
     -8987733732583272758 | 9 | x1
    
    (50 rows)
    cqlsh:> select token(a,b), c, d from test_table where token(a,b) >= -8987733732583272758 limit 50
    • 循环以上两个过程,直到token(a, b) = LONG.MAX_VALUE,表示整个表遍历完成。最终采用了该方式。以上几个方案都有一个共同的问题,在迁移过程当中,数据有变动,这种状况须要额外考虑。

全量迁移详细过程

最终采用了以上方案3,经过遍历cassandra表的token环的方式,遍历表的全部数据,把数据搬到mysql中。具体以下:后端

  1. 把整个token环分为2048段,这么作的目的是为了,把每张表的一个大的迁移任务,划分为2048个小任务,当单个迁移任务出现问题的时候,不用全部数据重头再来, 只须要把出问题的一个小任务重跑就行了。这里采用多线程。
  2. 迁移模式:主要有single和batch两种模式:
    • single模式:逐一insert至mysql。数据量不大的状况选择,单表亿级别如下选择,在64个线程状况下,16个线程读cassandra的状况下,速度能够达到1.5w行每秒。
    • batch模式:batch insert至mysql。数据量比较大的状况下选择,单表过亿的状况下选择。最大的一张100亿数据量的表,迁移过程实际上峰值速度只有1.6w行每秒的速度。这是由于cassandra读这部分达到瓶颈了。自己线上应用耗掉了部分资源。若是cassandra读没有达到瓶颈,速度翻倍是没问题的。
  3. 迁移性能问题:这时候cassandra和mysql和应用机器自己均可能成为瓶颈点,数据量比较大,尽可能采用性能好一点的机器。咱们当时迁移的时候,采用的一台40核、100G+内存的机器。
  4. 该过程遇到的一些问题:
    • 异常处理问题:因为自己cassandra和mysql的字段限制有必定区别。在这个过程确定会遇到部分记录由于某列不符合mysql列的限制,致使写入失败,写入失败的记录会记录到文件。这一过程最好是在测试过程当中覆盖的越全越好。具体的一些case以下:
      • cassandra text长度超过mysql的限制长度
      • cassandra为null的状况,mysql字段设置为is not null(这种状况须要建立表的时候多考虑)
      • cassandra的timestamp类型超过了mysql的datetime的范围(eg:1693106-07-01 00:00:00)
      • cassandra的decimail类型超过了mysql的decimail范围(eg:6232182630000136384.0)
    • 数据遗漏问题:因为部分表的字段比较多,代码中字段转换的时候最好仔细一点。咱们这边遇到过字段错乱、字段漏掉等问题。再加上该过程没有测试接入,本身开发上线了,数据迁移完成后才发现字段漏掉,而后又重头再来,其中最大的一张表,从头迁一次差很少须要花掉2周的时间。如今回过头去看,这张表当初迁移的时候,还不止返工一次。这个过程其实是很是浪费时间的。
    • 慢查询问题:在最大的一张表的迁移过程当中,超时比其余小表要严重一些。而且在跑的过程当中发现,速度越跑越慢,排查发现是部分线程遇到了某个token查询始终超时的状况。而后线程一直死循环查询查token。当把cassandra超时时间设置为30s时,这种状况有所改善,但还存在极个别token存在该问题。此处有一点奇怪的是,经过登陆到线上cassandra机器,经过cqlsh直接查询,数据是可以查询出来的。最终处理方案是针对该token加了5次重试,若是仍是不成功,则记录日志单独处理。

增量迁移详细过程

记录全量迁移开始的时间,以及记录这段时间全部变动的account(一个user包含多个account),把这部分数据发往kafka。再经过额外的增量迁移程序消费kakfa的方式把这部分数据搬到mysql,循环往复该过程,直到mysql中的数据追上cassandra中的数据。多线程

  1. 消费两个kafka队列,一个为全量迁移这段时间离线变动的account队列,另外一个是当前业务实时变动的account队列。
  2. 处理过程当中须要考虑两个队列中account冲突的问题,能够根据accountid进行加锁。
  3. 起初是按照user维度,进行增量迁移。实际上线后发现,按照user维度搬迁速度根本追不上正常业务数据变动的速度。而后选择了比user低一个维度的account(一个user包含多个account)进行迁移。

数据比对

为何有该步骤?为了确保cassandra和mysql数据源尽量的一致。app

  1. 在全量迁移完成之后,增量迁移过程当中,便上线了该比对功能。如何比对?当线上业务产生了数据变动,根据accountid,把该accountid下的cassandra的全部数据和mysql的全部数据经过调接口的形式查询出来进行比对。精确到具体字段的值
  2. 本来认为全量迁移和增量迁移基本没什么问题了,可是经过数据比对还发现了很多的数据不一致地方。排查发现有全量迁移过程致使的,也有增量迁移过程致使的,都是代码bug致使。发现了问题若是某张表全量迁移过程都出了问题,除了须要从新全量迁移该表。而且增量迁移也须要重头再来。
  3. 全部的比对结果存入数据库,而后定时任务发现比对不过的数据,再按照account维度进行增量迁移。
  4. 遇到的主要问题以下:
    • 时间精度的问题:cassandra的timestamp时间戳精确到毫秒(cassandra的一个客户端工具DevCenter查询出来的时间只精确到秒,毫秒部分被截断了,若是经过该工具肉眼比对,不容易发现该问题),而mysql的datetime默认条件只精确到了秒。
    • decimal小数位问题:cassandra中采用的decimal,对应mysql的字段类型是decimal(18,2),cassandra中若是是0或者0.000,迁移到mysql中会变成0.00,须要注意该精度问题。
    • 两张表来保存同一份数据致使脏数据问题:因为cassandra查询有不少限制,为了支持多种查询类型。建立了两张字段如出一辙的表,除了primary key不同。而后每次增删改的时候,两张表分别都增删改,虽然这种方式带来了查询上的遍历,可是产生脏数据的概率很是大。在比对的过程当中,发现同一份数据两张表的数据量相差不小,排查发现因为早期代码bug致使表一写成功,表二写失败这种状况(好在的是这些数据都是很早以前的数据,因此直接忽略该问题)。而迁移至mysql,只迁移一张表过去。若是两张表的数据不能彻底一致,必然有接口表现不一致。我我的对这种一份数据保存两份用法也是不推荐的,若是不在物理层作限制,只经过代码逻辑层来保证数据的一致性,是几乎不可能的事。
    • 空字符和NULL的问题:cassandra中""空字符串的状况下转换至mysql变为了NULL,这种状况会带来接口返回的数据不一致的问题,在不肯定下游如何使用该数据的时候,最好保证彻底一致。
    • 字段漏掉的问题:比对发现有张表的一个字段漏掉了,根本没有迁移过去,除了须要从新全量迁移该表。而且增量迁移也须要重头再来(尽可能避免该问题,该过程是很是耗时的)。
    • cassandra数据不一致的问题:同一条select查询语句,连续查询两次返回的结果数不一致。这个比例在万分之一-千分之一,带来的问题就是有的数据始终是比较不过的。
    • 应用本地时钟不一致致使的问题:现象就是随着应用的发版,某张表的lastModifyTime的时间,出现了cassandra比mysql小的状况,而从业务角度来讲,mysql的时间是正确的。大概有5%的这种状况,而且不会降下去。可能随着下一次发版,该问题就消失了。近10次发版有3次出现了该问题,最终排查发现,因为部署线上应用机器的本地时钟相差3秒,而cassandra会依赖客户端的时间,带来的问题就是cassandra后提交的写入,可能被先提交的写入覆盖。为何该问题会随着发版而偶然出现呢?由于应用是部署在容器中,每次发版都会分配新的容器。

开双写

通过以上步骤,基本能够认为cassandra和mysql的数据是一致的。而后打开双写,再关闭增量迁移。这时候若是双写有问题,经过比对程序也可以发现。工具

切mysql读

双写大概一周后,没什么问题的话,就能够逐步按服务切mysql读,而后就能够下线cassandra数据库了。性能

总结

  1. cassandra的使用:
    • 表的设计:特别须要注意partition key的设计,尽可能要保证单个partition的数据量不要太大。
    • 墓碑机制:须要注意cassandra的自己的墓碑机制,主要产生的墓碑的状况,主要是delete操做和insert null字段这两种状况。咱们这里曾经由于某个用户频繁操做本身app的某个动做,致使数据库这边频繁的对同一个partition key执行delete操做再insert操做。用户执行操做接近上百次后,致使该partition产生大量墓碑,最终查询请求打到该partition key。形成慢查询,应用超时重试,致使cassandra cpu飙升,最终致使其余partition key也受到影响,大量查询超时。
    • cassandra客户端时钟不一致的问题,可能致使写入无效。
  2. 迁移相关:
    • 全量迁移和增量迁移,最好在上线以前测试充分,千万注意字段漏掉错位的问题,尽量的让测试参与。在正式迁移以前,最好在线上建立一个预备库,先能够预跑一次。尽量的发现线上正式迁移时遇到的问题。不然正式迁移的时候遇问题的时候,修复是比较麻烦的。
    • 在切或关闭读写过程当中,必定要有回滚计划。

版权声明 做者:wycm
出处:https://my.oschina.net/wycm/blog/3046173
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归做者全部,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。
一个程序员平常分享,包括但不限于爬虫、Java后端技术,欢迎关注

相关文章
相关标签/搜索