为什么nebula不支持同时scan点和边?

背景:
全量图数据导出(我们通过此方式用来做1.0到3.6版本升级)

【nebula1.0版本】scan原理(个人理解)
通过ScanVertexProcessor.cpp和ScanEdgeProcessor.cpp源码以及nebula-client学习,其实scan就是先通过metad获取part分片的leader地址,然后使用StorageClient连接对应的leader,通过Thrift接口调用Storage服务,scan最终调用的就是ScanVertexProcessor.cpp和ScanEdgeProcessor.cpp里面的process方法,方法实现大致原理:根据传入的space、part、cursor进行rocksdb的Iterator前缀迭代操作,从cursor的位置往后面进行读取数据,其中就会判断扫描的数据类型是点还是边、是否为指定的tag或者edge,并且会判断时间是否满足要求,直到找到符合条件的数据量或者大小超过max_scan_block_size就会结束单次scan中的next操作,对于client端来说每次的next就是一次的scan操作,只不过每次scan完毕之后会回传cursor游标回来,next()的时候又会把cursor发送到nebula-storage那边。

疑问:
为什么nebula不支持同时scan点和边?如果同时支持点和边的scan感觉导出效率更高,因为指定tag或者edge就会造成没必要的磁盘数据检索。此外还可以避免点边数据量相差非常大的时候scan导致磁盘IO过高,比如:edge有100亿,vertex有只1w数据,此时去扫描vertex就会很大可能大量无效的磁盘数据加载,因为nebula服务直接定位哪条数据是点或者边的位置,只能从头到尾的进行扫描(我们线上就出现了此场景导致磁盘IO过高)

不懂代码,纯粹个人理解:

  1. 全量数据导出的场景我觉得确实是可以考虑同时 scan 点和边的,但是 scan 出来的点和边类型是混的,后续感觉没法用?全量导出后续是否还考虑导回来;
  2. 更多的场景,是指定点和边导的比较多,这样的话可能要实现两套
1 个赞

感谢回复,您讲得非常有道理,但是我还是有一丢丢疑问:
1.scan 出来的点和边类型是混的,后续感觉没法用 ------这点有可能,因为点和边返回的数据结构属性不一样,但是按理可以一个reponse中包含点和边两种类型作为属性
2. 更多的场景,是指定点和边导的比较多,这样的话可能要实现两套----我感觉一套应该也能做,只不过request增加两个参数来指定特定类型点或者边应该就可以,如果这两个参数为空就代表全量扫描,不为空就扫描指定类型的点和边

可能从技术手段上是能实现,实现的代价和使用的人数不是正比,做的意义就不是很大了,我也不是太懂里面的逻辑,单纯个人理解 :thinking:

关于 1,导出来的文件肯定是可以带类型的,但一个文件的类型可能有多种点、有多种边,这个后续在导入的时候,文件处理会比较麻烦。
至少现在 importer 的逻辑是指定一个csv 文件,然后在 yaml 里对这个 csv 文件进行映射配置。csv 文件里的数据都是对齐的

我大概明白你的意思了,抱歉,可能前面我描述得不太清晰,其实我指的是通过spark-connector这类同步nebula的场景,导出数据直接组装成insert语句插入到另外一个nebula

1 个赞

哦哦,如果是这个场景,我觉得在 3.x是 OK 的。因为允许悬挂边的存在
不过我觉得在后续的版本里,应该还是先扫点再扫边合适。这样可以避免悬挂边

1 个赞

1.0 不清楚,3.x 的情况下

  1. scan 接口支持部分属性,比如某个 Node 有一共 10 个属性,业务使用上可以只取其中的 3 个属性,这样传输的数据更少。
  2. Tag 和 Edge 的 key 前缀不一样,scan NodeA 和 scan NodeB 可以并发,这样 connecter 可以启很多的 job。如果全部导出的话,对 connector 来说,如果 partition 有 10 个,那只能最多 10 个 executor。
  3. 不太理解 ”扫描vertex就会很大可能大量无效的磁盘数据加载“, 如果只 scan 1w 个 vertex 是不会读边上的数据的。
2 个赞

因为 1.0 和 3.x 的编码区别比较大,可以参考文章

不太理解 ”扫描vertex就会很大可能大量无效的磁盘数据加载“, 如果只 scan 1w 个 vertex 是不会读边上的数据的。

一下为1.0的ScanVertexProcessor.cpp的源码,通过源码是可以看到扫描的时候的确不区分点和边

std::string prefix = NebulaKeyUtils::prefix(partId_);
    std::unique_ptr<kvstore::KVIterator> iter;
   //这里并没有类型作为前缀的一部分
    auto kvRet = doRangeWithPrefix(spaceId_, partId_, start, prefix, &iter);

    for (; iter->valid() && rowCount < rowLimit && blockSize < FLAGS_max_scan_block_size;
         iter->next()) {
        auto key = iter->key();
       //这里还加上了是否为顶点的判断
        if (!NebulaKeyUtils::isVertex(key)) {
            continue;
        }

刚看了下3.6的代码,比1.0的难懂了很多 :joy:,有点看不懂

3.6的找到了,的确扫描的时候已经区分点或者边了,那就是在低版本的nebula存在我说的那个问题了

    std::string prefix = NebulaKeyUtils::tagPrefix(partId);
    auto kvRet = context_->env()->kvstore_->rangeWithPrefix(
        context_->planContext_->spaceId_, partId, start, prefix, &iter, enableReadFollower_);