nebula 会对稠密点做存储上的优化吗
https://zhuanlan.zhihu.com/p/109401046
参考字节跳动的byteGraph
在字节跳动的业务场景中,存在很多访问热度和“数据密度”极高的场景,比如抖音的大 V、热门的文章等,其粉丝数或者点赞数会超过千万级别;但作为 KV store,希望业务方的 KV 对的大小(Byte 数)是控制在 KB 量级的,且最好是大小均匀的:对于太大的 value,是会瞬间打满 I/O 路径的,无法保证线上稳定性;对于特别小的 value,则存储效率比较低。事实上,数据大小不均匀这个问题困扰了很多业务团队,在线上也会经常爆出事故。
对于一个有千万粉丝的抖音大 V,相当于图中的某个点有千万条边的出度,不仅要能存储下来,而且要能满足线上毫秒级的增删查改,那么 ByteGraph 是如何解决这个问题的呢?
思路其实很简单,总结来说,就是采用灵活的边聚合方式,使得 KV store 中的 value 大小是均匀的,具体可以用以下四条来描述:
- 一个点(Vertex)和其所有相连的边组成了一数据组(Group);不同的起点和及其终点是属于不同的 Group,是存储在不同的 KV 对的;比如用户 A 的粉丝和用户 B 的粉丝,就是分成不同 KV 存储;
- 对于某一个点的及其出边,当出度数量比较小(KB 级别),将其所有出度即所有终点序列化为一个 KV 对,我们称之为一级存储方式;
- 当一个点的出度逐渐增多,比如一个普通用户逐渐成长为抖音大 V,我们则采用分布式 B-Tree 组织这百万粉丝,我们称之为二级存储;
- 一级存储和二级存储之间可以在线并发安全的互相切换;
一级存储格式
一级存储格式中,只有一个 KV 对,key 和 value 的编码:
- key: 某个起点 id + 起点 type + 边 type
- value: 此起点的所有出边(Edge)及其边上属性聚合作为 value,但不包括终点的属性
二级存储(点的出度大于阈值)
如果一个大 V 疯狂涨粉,则存储粉丝的 value 就会越来越大,解决这个问题的思路也很朴素:拆成多个 KV 对。
但如何拆呢? ByteGraph 的方式就是把所有出度和终点拆成多个 KV 对,所有 KV 对形成一棵逻辑上的分布式 B-Tree,之所以说“逻辑上的”,是因为树中的节点关系是靠 KV 中 key 来指向的,并非内存指针; B-Tree 是分布式的,是指构成这棵树的各级节点是分布在集群多个实例上的,并不是单机索引关系。具体关系如下图所示:
其中,整棵 B-Tree 由多组 KV 对组成,按照关系可以分为三种数据:
根节点:根节点本质是一个 KV 系统中的一个 key,其编码方式和一级存储中的 key 相同
Meta 数据:
Meta 数据本质是一个 KV 中的 value,和根节点组成了 KV 对;
Meta 内部存储了多个 PartKey,其中每个 PartKey 都是一个 KV 对中的 key,其对应的 value 数据就是下面介绍的 Part 数据;
Part 数据
对于二级存储格式,存在多个 Part,每个 Part 存储部分出边的属性和终点 ID
每个 Part 都是一个 KV 对的 value,其对应的 key 存储在 Meta 中。
从上述描述可以看出,对于一个出度很多的点和其边的数据(比如大 V 和其粉丝),在 ByteGraph 中,是存储为多个 KV 的,面对增删查改的需求,都需要在 B-Tree 上做二分查找。相比于一条边一个 KV 对或者所有边存储成一个 KV 对的方式,B-Tree 的组织方式能够有效的在读放大和写放大之间做一些动态调整。
但在实际业务场景下,粉丝会处于动态变化之中:新诞生的大 V 会快速新增粉丝,有些大 V 会持续掉粉;因此,存储方式会在一级存储和二级存储之间转换,并且 B-Tree 会持续的分裂或者合并;这就会引发分布式的并发增删查改以及分裂合并等复杂的问题,有机会可以再单独分享下这个有趣的设计。