目前市场上大多数图计算框架和图数据库都是解耦的, 用户一般需依赖外置存储来关联图数据, 打通图计算和图数据库有个关键点就在于如何回写计算完的数据 (比如三角数/pagerank 值), 今天一起来简单看看
0x00. 前言
HugeGraph-Computer 回写的方案核心思想是尽可能与原有的 HugeGraph 设计逻辑保持一致, 这样用户对读取/写入数据感知最轻, 然后期望它的兼容性也相对比较好, 既不影响旧版本的 server 使用, 也可以方便升级到新版.
那核心有这几个问题:
- 新增的 OLAP 属性, 存在哪, 如何区分?
- 新增的 OLAP 属性的数据, 存在哪? 常见方案
- OLAP 属性和原有点/边存在一起, 相当于追加新的属性
- OLAP 属性独立出来, 相当于单独的表来存储, 然后 rowkey 与主表保持一致
- 如果 OLAP 的属性增多, 是否会影响原有普通查询的性能?
- 如果后续不需要这个 OLAP 属性了, 如何高效删除?
那么之后围绕增/删两个 api 来看看 图计算的 OLAP 字段是如何设计 & 实现回写的, 其中的 tradeoff 又在哪.
0x01. 新增数据
完整说这里是新增图计算回写的属性值, 比如之前提到的三角计数运行完后返回每个点的三角形个数, 环路检测运行完后返回它的环路信息等. 这里 HugeGraph 的设计是把它也当做 Server 的一个属性来创建, 只不过会给它带上特定的 OLAP 标记, 在代码里称为 WrteType, 细分又有 4 种, 之后再说.
1 | enum WriteType { |
OLAP 属性在图的 schema 也有特定标识, 这样防止重名的属性产生, 也方便区分
1 | // OLAP_ID means all of vertex label ids |
图计算那么是如何创建一个 OLAP 属性的呢, 它只需要传入一个 WriteType 的可选标志位, 然后走新建 PK 的流程就可以了, 来看一下调用简图:
graph LR a(HTTP API - PropertyKey)--create-->b(createWithTask)-->c(addPropertyKey)--judge-->d(createOlapPk)--async-->e(OlapPropertyKeyCreateCallable)--execute1-->e1(createIndexLabelForOlapPk) e--execute2-->e2(createOlapPk)
前面主要是一些检查和判断, 区分普通属性和 OLAP 属性, 最后创建一个异步的执行调用, 这里 execute() 方法里做2件事:
- 创建 IndexLabel (可选, 若附带二级索引)
- 创建 OlapPK + Table (必须)
那么第一步创建二级索引其实没有什么特别的, 但是需要注意这里是否要加锁? 或者设置事务, 否则是否会出现创建 OLAP-IndexLabel 成功但是后续失败的问题? (好像就算出现了, 也没啥影响)
第二个是核心, 创建 OLAP 的属性, 以及对应的存储表, 这里就引出了具体回写的存储方式 (采用每个算法独立拥有一张对应的表). 目前只支持 RocksDB + Cassandra 两个后端存储. (在哪创建的 pk 呢, 似乎是在后面单独, 这里导致了不一致可能?)
数据表名是由 g_ap_id 组成, 二级索引表名是 g_ap_indexName 组成,
0x02. 删除数据
因为采用了把 olap 和 oltp 的数据分离的存储设计, 删除数据这里就简单许多了, 直接 truncate 或者 drop 掉 olap 属性对应的表就行, 因为每个属性都独立对应了1~2张表 (这样的不足是存储空间会有不小的放大)
那么简单说删除数据的流程如下, 仍然是复用了原有的核心流程, 只是针对 OLAP 的属性做了单独的处理.
graph LR a(HTTP API - PropertyKey)--delete-->b(removePropertyKey)--OLAP-->d(removeOlapPk)--async-->e(OlapPropertyKeyRemoveCallable)--execute1-->e1(removeIndexLabel) b--OLTP-->b1(removeSchema) e--execute2-->e2(removeOlapPk) e--execute3-->e3(removeSchema)
那么不一致可能出现在哪呢, 很显然就是异步执行的 execute 的地方, 因为这里会执行三个操作, 分别是移除 OLAP 索引, 移除 OLAP 表, 以及移除 OLAP 的属性本身, 那么就可能出现:
- 索引表删除了, 但数据表删除失败报错
- 索引表和数据表删除了, 但删除属性失败
不管是哪种情况, 都会导致下次再查询或者删除时候进入死循环, 因为会先检查表是否存在, 已经不存在的表就直接抛异常终止了, 就不会走到删除属性的代码了.
0x03. 疑问
相关的流程清楚之后, 结合代码阅读会发现下面几个问题 (具体的代码就不贴了, 根据流程图就能很快 trace 到)
- 在 server 启动/重置后的
serverStarted()方法里都会去检查 olap 属性对应的表, 如果不存在就会报错 (这里是否过严格?)- 考虑表不存在就重建一张新的 (✅, 选择此方案大部分情况自动重建, 并加以 error 日志提示)
- 考虑表不存在提示用户 error 日志, 提示它存在不一致, 可考虑手动重建/删除属性
- Cassandra 的普通表和 olap 表放在一起, 为何 RocksDB 是分开的? 应该把 Cassandra 的也独立出来么 (无需, 因为这里考虑到 RocksDB 的不同表可以指定不同磁盘, 而 Cassandra 不能指定)
- 在
createWithTask()里检查了属性是否存在, 存在则跳过/报错, 所以默认的情况下, 如果属性存在, 表不存在则不会创建表, 这里考虑几个方案:- 增加条件, 如果是 olap 属性, 则每次创建时无视是否存在, 都会去检查表是否存在, 如果不存在则创建, 存在则跳过 (✅)
- 增加 force 参数, 携带后会无视检查尝试重建所有属性 (或仅 olap 属性)
- 图计算回写在大部分情况下, 默认是否应该选择
olap_common而不是建二级索引, 因为额外放大一倍存储空间也降低写速度
0x04. 改进点
根据上面的读写和删除流程, 以及存在的逻辑问题, 做了以下几个改进:
- Server 重启或动态创建图时, 自动恢复丢失的 OLAP 表而不是抛异常
- 支持创建 OLAP 属性时自动恢复已经存在属性, 但表丢失的情况
- 支持删除 OLAP 属性时, 不因表丢失而无法删除进入死循环
- RocksDBStore 里的 olapTable 哈希表改为线程安全版本 (避免同时增/删时出现异常)
- 改进从内存的 olapTables 中找不到对应表时的错误提示信息
通过 3 种方案综合来保障 OLAP 表的丢失情况, 并且不需要用户手动操作, 也会从日志里提示用户存在不一致的情况, 让用户自行检查最近的操作是否有误.
另外, 通过分析回写流程和逻辑我们也能知道, 在图计算的算法中, 建议使用 writeType = olap_common , 这样可以加快回写速度, 节省存储空间, 最好是以后这提供一个选项给用户, 让他们自己决定哪些算法需要二级索引 (直接通过属性去做查询)