图解图库JanusGraph系列-图分区(JanusGraph Partitioning)

2022-09-07 15:19:08 浏览数 (1)

图数据库文章总目录:
  • 整理所有图相关文章,请移步(超链):图数据库系列-文章总目录
  • 地址:https://liyangyang.blog.csdn.net/article/details/111031257

源码分析相关可查看github: https://github.com/YYDreamer/janusgraph

转载文章请保留以下声明: 原文地址:https://liyangyang.blog.csdn.net/ 公众号:“图数据库” or “Geek Tech”

前言

当JanusGraph部署在具有多个存储后端实例的集群上时,图将被分区存储在这些后端实例上。

由于JanusGraph将图数据以邻接列表的形式进行存储,因此将顶点分配给计算机时需要确定节点分配到哪个分区;

JanusGraph默认采用切边进行图存储,如果将一条边的两个节点sourceVertexA 和 targetVertexB 分配到两个不同的后端实例中,当进行查询时,需要进行一次机器间的网络通信,这会减慢查询速度!

一个好的分区方式,可以提升查询速度,下面我们来看下JanusGraph的分区方式!

ps:下述中“节点”与图中的“顶点”为相同释义

分区方式

Janusgraph中分区方式为: 随机分区自定义分区 两种方式

在讨论分区之前,考虑一个问题:

为什么JausGraph分配的逻辑区间值,可以影响hbase物理存储并可以将分区相同的数据存放的更近呢?

我在《图解JanusGraph系列-存储结构》一文中提到过:hbase使用vertex id作为rowkey,hbase根据rowkey顺序排序存储; 每个hbase region存储是一段连续的Rowkey行;

janusgraph的vertex id的设计中,可以发现将分区值放到了64位的前5位存储! 在存储数据到hbase时,对rowkey进行排序,因为partition id在前5位,所以同一个分区的vertex id对应的rowkey值相差较小,所以会存储在一块;

1、随机分区

随机分区是janusgraph的默认分区方式,该策略将节点随机的分配给不同的后端实例;

优点:

  • janusgraph默认方式,不需要配置
  • 节点分配均衡

配置方式:

代码语言:javascript复制
cluster.max-partitions = 32 
id.placement = simple 

其中的cluster.max-partitions控制janusgraph创建多少个虚拟分区;官网建议,该数目最好配置为后端实例数目的两倍大小; 必须 >1和2的次幂;

注意: 当前JanusGraph不支持显示分区!

2、自定义分区

在图分区中,有两个方面可以控制:edge cuts 和 vertex cuts。

2.1 Edge Cut

在对节点进行分区时,我们都会努力将 频繁一块被遍历的节点 存放在同一个分区中,从而减少遍历时机器间的网络通信次数,提升遍历速度;

节点通过分配的唯一节点ID放置在分区中。 分区实质上可以理解为一批节点ID的顺序范围。 按照以上的理解,要将顶点放置在特定分区中,JanusGraph需要从特定分区的顶点ID范围中选择一个ID;

默认策略:

JanusGraph通过配置的放置策略控制节点到分区的分配。在按照边切割存储的方式中,默认情况下,在相同事务中创建的顶点被分配到相同的分区。

默认的这种策略很容易推论,并且在同一事务中创建频繁共同遍历的顶点的情况下效果很好-可以通过优化加载策略达到这种效果,或者因为顶点自然会以这种方式添加到图形中。

但是,该策略是有限的,当数据在大型事务中加载时,大批的数据被分配到一个分区中会导致分区不平衡,对于许多用例来说,这并不是最佳的策略。

自定义策略:

用户可以通过实现IDPlacementStrategy接口并通过ids.placement选项在配置中注册自定义的策略类来提供特定于用例的顶点放置策略。比如我们将

代码语言:javascript复制
public interface IDPlacementStrategy {
	int getPartition(InternalElement element);
	void getPartitions(Map<InternalVertex, PartitionAssignment> vertices);
	void injectIDManager(IDManager idManager);
	boolean supportsBulkPlacement();
    void setLocalPartitionBounds(List<PartitionIDRange> localPartitionIdRanges);
    void exhaustedPartition(int partitionID);
}

在实现IDPlacementStrategy接口时,需要注意点:分区由整数ID标识,范围为0到配置的虚拟分区数减1。在上述的示例配置中,分区为0、1、2、3,… 31。分区ID与顶点ID是不同的。

2.2 Vertex Cut

上述的边切割优化的目的是减少后端存储实例间的交叉通信,从而提高查询的执行效率,而顶点切割解决了具有大量关联边的顶点引起的热点问题;

虽然以顶点为中心的索引(vertex-centric index)有效地解决了大度顶点的查询性能,但仍需要进行顶点切割以解决非常大的图上的热点问题。

切割顶点意味着将顶点邻接列表的子集存储在图中的每个分区上。换句话说,对顶点及其邻接列表进行了分区,从而有效地将单个顶点上的负载分布在集群中的所有实例上,从而解决了热点问题。

配置方式:

JanusGraph中按vertex label 切割顶点。顶点标签可以定义为分区的,这意味着该标签的所有顶点将以上述方式在整个群集中分区。

代码语言:javascript复制
mgmt = graph.openManagement()
mgmt.makeVertexLabel('user').make()
mgmt.makeVertexLabel('product').partition().make()
mgmt.commit()

上述,我们有两种节点userproduct,将product节点类型设置为Vertex cut类型;

假设存在1000个product 和 100 0000个user节点,表明一个产品会被许多的用户使用,表现在图中就是一个product节点会对应上万个user 节点,也就是存在上万个用户使用该产品的边;

在这种情况下,product顶点将具有很高的度,并且如果不进行分区,受欢迎的product将成为热点!

思考

什么情况使用随机分区? 什么情况下使用自定义分区呢?

首先说下官方的建议:当图很小或者只有几个存储实例时,为了简单起见,最好使用随机分区。作为经验法则,当图增长到数十亿条边时,应该强烈考虑启用显式图划分并配置合适的划分策略。

我的个人看法是两个方面:图数据的体量 和 图数据查询和计算的诉求;

先说第一点:图数据的体量

图的体量到底多少算大多少算小呢,主观上来说就如同官方建议的,亿级以下的图算是小图,几十亿算是大图;

什么时候进行自定义分区,主要还是取决于是否为大图?响应时间是否满足,这里需要考虑后续业务增长的情况,虽然当下图可能非常小,但是数据体量增长迅速,这里要考虑前置的自定义分区;但是如果数据体量增长比较慢或者大概率是不会很大,建议就不要过度优化;

为什么要说图的体量是一个衡量指标,因为图数据多必然分片会多,分片多查询和图计算在随机分片的情况下访问不同分片的数据产生的网络消耗势必会很大;

第二点:图数据的查询和计算诉求

什么情况下,我们才需要去手动设置分片规则呢? 查询和图计算的诉求不满足我们的标准;

比如,我们要深度查询,就会有不同分片网络消耗的问题,那么我们就可以通过自定义分片将一批数据分配到同一个机构的不同分片中,这样网络消耗就会大大减少;这个我会再《图解图数据-如何更好的设计图》一文中详细解释;

归根结底到底使用什么类型的分区,是否要自定义分区,主要是取决于当前业务的使用情况;毕竟,脱离业务的设计都是耍流氓嘛

0 人点赞