基于图数据库huge的数据迁移实践

627 阅读10分钟

1.背景

hugegraph经过一次大的版本升级(v0.9-0.11),其存储结构发生变化,新旧版本存储不兼容,社区提供了数据导出组件tools(通过api),将数据从hbase中导出为原始数据,在导入到新版图中。但并不适用于大数据量业务,主要考虑有以下两点:

  • tools为单机,实测导入速度并不足以支撑我司业务的量级(千亿点边),预计使用tools需要两周可将所有业务数据导出,且不保证中途不会失败,失败则需要重新导入。
  • tools通过图客户端进行数据的导入导出,全量数据的导出无疑会增加hbase regionserver压力,影响线上业务稳定性,同时数据的持续写入会因为 flush,compaction 等机制占用较多的系统资源

2. 迁移方案

避开api,走存储层,通过mapreduce的方式将hbase数据表数据直接进行迁移到新版hbase数据表。直接去读region数据,将旧表图数据按旧版编码方式反序列化为图原始数据,在将其按新版图数据序列化方式序列化为新表点边格式写入新版图数据库。 (先sacnTable(旧)生成hfile,再将数据bulkload到(新)表)

在数据迁移中,针对图数据转换的特点可大大加快mr的处理速度:

  • 点表rowkey未发生变化,前缀(后续考虑改为hash)编码方式未发生变化,因此在新旧版本迁移过程中,每条数据迁移前后的region位置不会发生变化,也就是说,利用这一特性,我们可以建立和旧表相同数量相同startkey的region,避免mr过程中reduce排序(reduce num置为0),直接生成hfile,由于有序,可直接load,几乎是秒级别即可load完成十几T数据,大大加快迁移速度。
  • 边表的rowkey 发生的细微的编码变化,整体看并不会乱序,然迁移之前的rowkey数据在迁移后不一定落在一个region内,可能会落在相邻的region,在load过程中会进行再次split,在移动走hfile.此过程会慢于点表 (hbase数据排序格式按着rowkey,column,timestamps依次升序,本次迁移均符合条件) 迁移准备和注意事项⚠️:(血泪踩坑记录)
  1. 需要将新表region信息与旧表region保持一致,方便后续将map生成的hfile直接load(实际上就是移动hfile,通知regionserver,此过程速度极快)这里我采用复制旧表的region信息重建新表。
  2. region个数进行inputsplit,使得map个数=region个数,一个mapTask输出新表的一个region需要的hfile。
  3. 需要注意,旧版的反序列化和新版的序列化方式使用的是同名包不同版本,在引入过程中比较麻烦,千万要小心调用,我这里是直接将其同改名搬运在调用。
  4. 由于没有reduce,mapTask需要输出hfile需要的数据格式<ImmutableBytesWritable, KeyValue>
  5. 我们在数据迁移过程中,避免业务一边进行导入一边进行迁移,虽然可以事后进行增量数据的导入,但是
    • 首先,业务数据在不断增加,我们有小时级别的任务和天级别的定时任务,数据表可能会进行compaction,这可能导致你的任务大量失败
    • 其次,有些业务数据是不可以重复导入的,有些数据的导入策略是SUM,如我使用迁移工具进行一边scan后迁移,业务就不能确切的知道哪些数据需要导入,那么增量数据就可能出现重复导入的数据,一些原有值直接变double。这是不可接受的,我这边也险些犯了这个错误,及时让业务同学停止了一切定时任务,进行全量迁移。(我们会新旧版本并行一段时间,所以需要并行导入每日数据,稳定后下线旧版本图)
  6. 在旧版本的图数据库中,存在数据不一致的情况,如:删除某label的点/边,但数据表中仍存在这个label的数据,这会影响业务在执行gremlin时的结果。再此次升级过程中,一并去掉了非法数据,针对全量数据进行了清洗
  7. 有些业务数据在导入时为更新策略为合并union,积年累月的合并会产生一个大value,实际在hbase中可能出现超过65535的数据,此处会报错,要注意。
  8. 如果有异构策略注意在新db中设置异构策略(我们为ONE_SSD),否则读磁盘引发性能灾难
  9. 如果每天有定时任务进行导入,务必和业务方确认停掉任务并确认进程完全清除,我这里就踩了关闭任务却没有确认导入进程停止的坑,导致迁移后部分数据和原始数据不一致。
  10. 迁移完毕要对数据表进行本地化,否则对性能影响很大

数据迁移主要涉及点表,入边/出边表的迁移,因为了解到业务没有进行label index的相关查询,因此在迁移中未进行索引表迁移,此外还涉及到verlabel/edgelabel/propertykey以及count表的录入。

  • propertykeys 属性元信息 (pk表)
  • vertexlabel 点label元信息 (g_v表)
  • edgelabel 边label元信息 (g_ie以及g_oe表)
  • count 维护元信息自增id (c表)

3. 迁移详细步骤:

3.1 准备工作:初始化新版图数库

huge本身初始化会建立一套数据表,来存储图相关的元数据和业务数据以及索引数据,但是region数量此时为1,并不符合我们要求,而业务的旧版数据表的region分裂是非常稳定的,我们需要将新版的相关表其disable后进行drop,删除并按着就版本的region数量和startkey重新建立新的数据表。

3.2 输入切分与配置相关细节

3.2.1 inputsplit


// regionMap结构
public RegionMap(int size) {
        this.startKeys = new byte[size][];
        this.endKeys = new byte[size][];
        this.locationMap = new TreeMap<ImmutableBytesWritable, String>();
        this.regionKeysMap = new TreeMap<ImmutableBytesWritable, ImmutableBytesWritable>();
        }


// 1.获取region信息
public RegionMap getRegionMap(final Configuration conf, final byte[] tableName) throws IOException {
        List<SplitInfo> regions = getSplitInfo(conf, tableName);
        System.out.println("got " + regions.size() + " regions");
        RegionMap map = new RegionMap(regions.size());
        for (SplitInfo region : regions) {
                map.add(region.startKey, region.endKey, region.regionServer);
        }
        return map;
}

// 2. 按region进行split
splits.add(new TableSplit(TableName.valueOf(tableName), startRow, stopRow, regionMap.getLocation(startRow))

3.2.2 设置reduceTask nums ,提升导入性能关键一步

原有hbase的数据本来就是有序的,且,我们的点表rowkey并未变动,我们不需要花费大量的时间进行reduce,我们期望经过map处理后直接生成hfile.这里我们设置reduce数量为0

想一想为何设置reduceTask数量为0,map就可以输出hfile呢?

如下为reduce为0时, mapreduce-client-core-2.2.0的代码,符合我们的需要:

// 设置reduce数量为0,封装NewDirectOutputCollector对象直接将结果写入hdfs作为最终结果
   if (job.getNumReduceTasks() == 0) {
      output = 
        new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
    } else {
    // 若不为0,则将结果写入本地磁盘,提供给reduce task
      output = new NewOutputCollector(taskContext, job, umbilical, reporter);
    }

3.3 反序列化旧版数据

3.3.1 点

新旧版本图点数据的rowkey没有发生变化,但是propertykeys发生了较多变化: 如下图所示,点id没有发生变化,但column进行了压缩,将多行pk编码成为一行,极大的节约了存储空间,主要有以下两方面:

  • 节约了n倍的rowkey,hbase的存储结构使得有几个column就会存储几次rowkey,那么旧版本的vertex存储结构必然造成了大量空间的浪费。
  • 此外,此次版本升级还节约了属性值的空间利用率,对于数值型数据,之前按short/int/float/double开辟空间的现在都细分到每个byte,对1/3/5/7这样的单数位字节进行判断,按需求存储。emmm····可见作者对于存储空间的节约~

旧vertex存储结构:

image.png

新vertex存储结构:

image.png

可见,新旧版本的rowkey没变,因此region相对位置没有任何变化,我们只需要将旧版数据经过反序列化和序列化然后直接生成hfile,不需要进行排序

3.3.2 边

新旧版edge存储:

rowkey构成没有发生变化,column构成也没有发生变换(同vertex的新版column构成),但是编码方式发生了变化。

image.png 在点边的反序列化和序列化过程中,需要导入com.baidu.hugegraph.backend.serializer.BytesBuffer的不同版本的包,搬运过程中要小心,及时验证,尝试使用hbase api 方式读取旧版本cell并转换为新版cell并导入新版数据库表,看是否可正常读取,使用尽可能多的数据源,验证过程是否正确,务必保证这一步转换没有任何问题,不然就是脏数据灾难~

3.4 序列化为新版数据

此时已将序列化数据还原为点/边真实数据,再将图原始数据序列化为新版图数据,这一过程具体看我的上一篇文章 bulkload实践,原理相同,不在赘述。

3.4.1 点

3.4.2 边

3.5 生成hfile并load

  1. 这一过程和bulkload不同之处在于不需要进行排序
  • 对于点数据来说,rowkey在hbase中本身就是是有序的,且迁移前和迁移后数据rowkey没变,新旧数据表region信息完全一致的情况下,数据所落的region也就不会变,因此我们通过OutputFormat的RecordWriter,不运行排序操作也不运行buffer的缓冲操作,直接写入到output的文件里。这节约了大量时间避免了shuffle,spill,merge,sort! ~lucky。

    • 点数据load成功后,原有的列蔟下的hfile正常情况下会全部被move到hbase的hfile文件下,我们成功的秒速load了进去。这验证了我们的想法,即没有任何排序也不会造成数据的乱序,乱序的数据是不可load到hbase的
  • 对于边数据来说,情况稍有不同,但仍然保证了迁移前后的绝对排序。边数据的rowkey组成没有发生变化,只是labelid的编码前缀某一位字节发生了变化,且这位变化的字节在新旧版本均为统一值,因此并未影响我们数据的绝对顺序,也就不需要排序。

    • 需要注意,虽然绝对顺序没变,但是由于变化的那一位字节,导致总体数据region的位置可能发生变换,所以虽然我们创建了和旧版数据库一样的region信息,仍然不保证我们生成的数据能均匀的落在每一个region中,这和点数据有所不同
    • 数据相对region位置的不同导致了在我们生成的hfile列簇中,会有.split文件生成,load过程会对其进行切割为符合region,startkey和endkey的数据块,而原有的不符合条件的数据快会被留下,split生成的数据块会move进hbase中
-rw-r--r--   3 hbase supergroup           0 2021-04-07 22:03 /home/yarn/outputei/f/ff27a8670c8e48c7ac56c299c68fc211.splited
-rw-r--r--   3 hbase supergroup 11155275472 2021-04-07 20:22 /home/yarn/outputei/f/ff66e030ecaa4723bae0fd041972ee62
-rw-r--r--   3 hbase supergroup  7496589553 2021-04-07 21:02 /home/yarn/outputei/f/ff6a0afac39949ad8558fe8eac60b640

4.迁移效率

(yarn资源越多越快)

90min 800亿点

100min 1000+亿边