这篇文章是从dblp上面自行下载的唐老师发的A类文章,主要讲的是对spark源码当中sparkgraphX模块的优化;
incgraph:基于Spark GraphX的分布式增量图计算模型和框架;
原文:IncGraph: An Improved Distributed Incremental Graph Computing Model and Framework Based on Spark GraphX
目录
引入:关于spark的原生图处理框架spark graphX
关于文章当中使用的性能验证方法
关于论文本身
文章的具体实现
incgraph的功能总结:
实验评价以及性能验证:
性能验证
这部分的提升还是相当明显的
可扩展性验证
文章结论
spark GraphX的源码阅读
个人下一步的安排
杂谈
引入:关于spark的原生图处理框架spark graphX
spark的garphX实际上是一种分布式的图处理框架,图的分布式或者并行处理其实是把图拆分成很多的子图,然后分别对这些子图进行计算,计算的时候可以分别迭代进行分阶段的计算,即对图进行并行计算。
如同Spark本身,每个子模块都有一个核心抽象。GraphX的核心抽象是Resilient Distributed Property Graph,一种点和边都带属性的有向多重图。它扩展了Spark RDD的抽象,有Table和Graph两种视图,而只需要一份物理存储。两种视图都有自己独有的操作符,从而获得了灵活操作和执行效率。
这个spark graphX的视图跟数据库概念里面的视图的特性似乎是有挺大的差别的,graphX里面如果对视图进行操作,最后是真的都会变成对底层的RDD来进行操作,但是数据库里面的操作视图就仅仅只是真实数据的一份拷贝;
关于文章当中使用的性能验证方法
经典问题pagerank
pagerank算法主要处理的也是图问题,其实际上遵循一个符合直觉的策略:
越是重要的网页:(1)一般会被更多的网页引用,(2)一般会被更重要的网页引用
评价一个网页,一个节点重要性所使用的是一个叫做PR值的东西,这种现在看来算是“传统”的迭代,遵循如下的递推策略
对于一个页面A,那么它的PR值为:
- 其中 PR(A) 是页面(节点)A的PR值 ,
- PR(Ti)是页面(节点)Ti的PR值,Ti值A页面(节点)指向的节点(即引用了A的节点),
- C(Ti)是页面(节点)Ti的出度,也就是Ti指向其他页面(节点)的边的个数,
- d 为阻尼系数,其意义是,在任意时刻,用户到达某页面(节点)后并继续向后浏览的概率。
PageRank是Google专有的算法,用于衡量特定网页相对于搜索引擎索引中的其他网页而言的重要程度。它由Larry Page和Sergey Brin在20世纪90年代后期发明。PageRank实现了将链接价值概念作为排名因素。
PageRank将对页面的链接看成投票,指示了重要性。
文章当中也是使用了pageRank来检验积分图框架的有效性;
关于论文本身
以下为本人对论文整体内容的一个理解:
当动态图中的数据发生变化时,挖掘出的信息将会过时。为了计算最新的结果,图算法必须从头开始重新计算整个数据,这将消耗大量的计算时间和资源。为了降低这种计算的成本,文章提出了一种称为积分图的模型来支持在动态图上的增量迭代计算。不同于传统的迭代方式,IncGraph可以实现对之前都图的计算结果的重用,以此来实现计算的加速;
设计了这个新的框架之后也在一个八个节点上面的集群里面做了性能的验证
IncGraph有两个关键组成部分:
(1)增量迭代计算模型,包括两个步骤:
其一为增量步骤计算改变图顶点的结果,其二为合并步骤计算整个图的结果,使用前面图的结果和增量步骤;
(2)增量更新方法,加速迭代图算法中的迭代过程。
这个部分的话,个人理解就是类似于动态规划算法里面的状态描述和状态转移方程,设计状态转移方程很多时候才是关键
迭代图计算模型
文章的具体实现
文章主要是在spark原有的组件之上添加了新的组件,以此来实现对动态图的解析
Spark是一种流行的分布式计算平台,采用master-worker架构。GraphX基于Spark平台,为图计算和图挖掘提供了丰富易用的接口。IncGraph实现的架构 如图4所示,包括原有组件和新增组件。原有组件包含DAGScheduler、 TaskSetManager、TaskScheduler、Spark平台的Spark核心和Hadoop分布式文件系统;并且新添加的组件包含
增量控制器、预处理器和迭代控制器。
原生spark框架所包含的几个处理单元
- DAG调度器。DAGScheduler负责将Spark作业转化为DAG(Directed Acyclic Graph),根据RDD与stage的关系找出开销最小的调度方式,然后将stage以taskSet的形式提交给TaskScheduler 。
- 硬盘文件系统。Hadoop 分布式文件系统 (HDFS) 用于存储应用程序的输入和输出数据。在IncGraph实现中,我们使用 HDFS 来存储由一系列静态图组成的动态图。每个静态图都存储为一个单独的文件。
- spark核心区。Spark 核心是计算引擎,包括 Spark 的基本功能,包含 RDD API、操作和两者上的操作。其他 Spark 库构建在 RDD 和 Spark 核心之上。
- 任务集管理器。TaskSetManager负责管理taskSet ,包括推测执行、任务局部性和任务的资源分配。
- 任务调度器。TaskScheduler负责将任务分配给不同的执行器来处理和监督所有的任务。当执行者发出请求时,TaskScheduler 根据剩余资源分配相应的任务。此外,TaskScheduler还维护着所有任务的运行标签,并对失败的任务进行重试。
文章所设计的incgraph框架在原生spark框架的基础之上还添加了下面几个处理单
- 预处理器。预处理器模块用于在更改的顶点和先前图形的顶点之间找到新添加的顶点。在实现中,我们使用 BFS 算法获取增量步骤中新增的顶点。
- 增量控制器。增量控制器是一个独立的模块,用于支持增量迭代,包括基本编程接口、执行流程、增量迭代流程控制等。该模块可分为两个子模块:spark job创建和增量迭代过程控制。前者完成增量迭代的参数初始化,后者根据3.3节描述的模型和方法进行迭代算法。
- 迭代控制器。迭代控制器模块提供了迭代控制功能,使迭代图算法在每次迭代过程中采用增量更新的方式对每个顶点进行更新。
incgraph的功能总结:
组件增量控制器、预处理和迭代控制器是为在 Spark GraphX上实现IncGraph而设计和开发的。这些组件通过调用 Spark GraphX的 API 接口来工作,它不解决框架本身的内部变化。由于IncGraph在Spark GraphX、Pregel、Hama上的实现机制基本相同,因此本文提出的IncGraph 模型也可以适用于其他这些图引擎。
实验评价以及性能验证:
实验验证环境:
在一个由 8 台物理机器(1 台主机和 7 台工人)组成的实际集群上进行实验
测试使用的数据集(incgraph还是处理的动态的无向图,所以设计的测试数据集里面的内容也就是无向图)
性能验证
对比的指标:
其中的tra那条线是traditional iteration,传统迭代,嗯,比如说,传统的求解单源最短路径的迭代式算法就是dijstra算法,从上面的time cost对比来看,incgraph这种积分图处理方法在处理这些经典的问题都拥有明显的性能优势;
incgraph积分图对pagerank问题的解决策略的伪代码如下:
参数说明:
α是阻尼系数,pagerank当中的一个重要参数,其值的高低影响计算;
低阻尼系数(=多阻尼)将使计算更容易。由于PageRank的流程受到抑制,迭代将很快收敛。
低阻尼系数(=大阻尼)意味着相对PageRank将由从外部页面收到的PageRank决定 - 而不是内部链接结构。
高阻尼系数(=小阻尼)将导致网站的总PageRank增长更高。由于几乎没有阻尼,从外部页面收到的PageRank将在系统中传递。它不会永远增长,最大限制是入站PageRank * d/(1-d)。
我们使用PageRank作为一个测试用例,以理解积分图的增量更新方法的优势。积分图(采用增量更新法的增量迭代计算)记为“Inc”,增量迭代计算记为“IncIter”,增量更新法记为“IncUp”,原始GraphX记为“Tra”。图9显示了在不同的数据集中添加了100k个顶点的情况下,增量更新方法的性能。实验结果表明,增量更新方法的性能优于原始GraphX,而低于增量迭代计算和IncGraph;
这部分的提升还是相当明显的
可扩展性验证
用来验证采用的数据集还是之前给出的那个,使用的对比算法是pagerank和连接组件
文章当中描述的“可扩展性”,个人理解指的是
上图显示了Spark集群中节点数从5到30时的性能。可以看出,在两种迭代方法下,PageRank和连接组件的执行时间随着节点的增加而减少,并且在每个测试用例中,IncGraph的执行时间始终低于原始GraphX。此外,随着集群大小的增加,IncGraph和GraphX之间的性能差距也越来越小。其原因是迭代图算法的任务需要频繁的数据通信。
在真实的Spark运行环境中,任务根据数据分布情况分布在不同的计算节点中,在shuffle阶段需要大量的中间数据传输。随着集群大小的增加,在集群中运行的任务的通信成本也会增加。此时,数据通信延迟越来越成为影响性能的主要因素;
文章结论
本文提出了一种增量迭代计算模型,该模型通过根据变化的顶点和之前图的结果进行更新来执行迭代图算法。此外,还提出了一种增量更新方法来加速增量迭代计算中的图算法的迭代过程。我们实现了基于GraphX的积分图模型,并在理论和实验上证明了结果的正确性。基于一系列测试用例的实验结果表明,与传统迭代和先进的增量图处理模型相比,集成具有性能优势。
spark GraphX的源码阅读
graphX的实现机理可以概括为图的并行化处理策略;
以pagerank为例,我们首先可以将对网页的处理转换为对图的处理,具体如下:
- 网页和网页之间的关系用图来表示
- 网页A和网页B之间的连接关系表示任意一个用户从网页A到转到网页B的可能性(概率)
- 所有网页的排名用一维向量来B来表示
所有网页之间的连接用矩阵A来表示,所有网页排名用B来表示。
则有:
pagerank的并行化
上面的数学阐述说明了“网页排名的计算可以最终抽象为矩阵相乘”,而在开始的时候已经证明过矩阵相乘可以并行化处理。
图的存储和加载
在进行数学计算的时候,图可以使用用线性代数中的矩阵来表示,那么如何进行存储呢?
在大数据的环境下,如果图很巨大,表示顶点和边的数据不足以放在一个文件中怎么办?用HDFS
加载的时候,一台机器的内存不足以容下怎么办?延迟加载,在真正需要数据时,将数据分发到不同机器中,采用级联方式。
一般来说,我们会将所有与顶点相关的内容保存在一个文件中vertexFile,所有与边相关的信息保存在另一个文件中edgeFile。
生成某一个具体的图时,用edge就可以表示图中顶点的关联关系,同时图的结构也表示出来了。
代码部分如下:
代码路径:
我是把整个spark项目放在了scala_code这个文件夹下面的
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.spark.graphx
import java.util.concurrent.TimeUnit
import org.apache.spark.SparkContext
import org.apache.spark.graphx.impl.{EdgePartitionBuilder, GraphImpl}
import org.apache.spark.internal.Logging
import org.apache.spark.storage.StorageLevel
/**
* Provides utilities for loading [[Graph]]s from files.
*/
object GraphLoader extends Logging {
/**
* Loads a graph from an edge list formatted file where each line contains two integers: a source
* id and a target id. Skips lines that begin with `#`.
*
* If desired the edges can be automatically oriented in the positive
* direction (source Id is less than target Id) by setting `canonicalOrientation` to
* true.
*
* @example Loads a file in the following format:
* {{{
* # Comment Line
* # Source Id <\t> Target Id
* 1 -5
* 1 2
* 2 7
* 1 8
* }}}
*
* @param sc SparkContext
* @param path the path to the file (e.g., /home/data/file or hdfs://file)
* @param canonicalOrientation whether to orient edges in the positive
* direction
* @param numEdgePartitions the number of partitions for the edge RDD
* Setting this value to -1 will use the default parallelism.
* @param edgeStorageLevel the desired storage level for the edge partitions
* @param vertexStorageLevel the desired storage level for the vertex partitions
*/
def edgeListFile(
sc: SparkContext,
path: String,
canonicalOrientation: Boolean = false,
numEdgePartitions: Int = -1,
edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,
vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY)
: Graph[Int, Int] =
{
val startTimeNs = System.nanoTime()
// Parse the edge data table directly into edge partitions
val lines =
if (numEdgePartitions > 0) {
sc.textFile(path, numEdgePartitions).coalesce(numEdgePartitions)
} else {
sc.textFile(path)
}
val edges = lines.mapPartitionsWithIndex { (pid, iter) =>
val builder = new EdgePartitionBuilder[Int, Int]
iter.foreach { line =>
if (!line.isEmpty && line(0) != '#') {
val lineArray = line.split("\\s+")
if (lineArray.length < 2) {
throw new IllegalArgumentException("Invalid line: " + line)
}
val srcId = lineArray(0).toLong
val dstId = lineArray(1).toLong
if (canonicalOrientation && srcId > dstId) {
builder.add(dstId, srcId, 1)
} else {
builder.add(srcId, dstId, 1)
}
}
}
Iterator((pid, builder.toEdgePartition))
}.persist(edgeStorageLevel).setName("GraphLoader.edgeListFile - edges (%s)".format(path))
edges.count()
logInfo(s"It took ${TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)} ms" +
" to load the edges")
GraphImpl.fromEdgePartitions(edges, defaultVertexAttr = 1, edgeStorageLevel = edgeStorageLevel,
vertexStorageLevel = vertexStorageLevel)
} // end of edgeListFile
}
这部分描述了spark中GraphX是如何执行对图的加载和存储的,采用的策略是点与边分开存储
直接用矩阵去存实际上也是不太现实了吧,至少用矩阵去存一个大图就跟分布式没有什么关系了;
变量
- path可以是本地路径(文件或文件夹),也可以是hdfs路径,本质上是使用sc.textFile来生成HadoopRDD的,numEdgePartitions是分区数。
- Graph的存储是分EdgeRDD和VertexRDD两块,可以分别设置StorageLevel。默认是内存。
- 这个函数接受边文件,即’1 2’, ‘4 1’这样的点到点的数据对组成的文件。把这份文件按分区数和存储level转化成一个可以操作的图。
流程
- sc.textFile读文件,生成原始的RDD
- 每个分区(的计算节点)把每条记录放进PrimitiveVector里,这个结构是spark里为primitive数据优化的存储结构。
- 把PrimitiveVector里的数据一条条取出,转化成EdgePartition,即EdgeRDD的分区实现。这个过程中生成了面向列存的结构:src点的array,dst点的array,edge的属性array,以及两个正反向map(用于对应点的local id和global id)。
- 对EdgeRDD 做一次count触发这次边建模任务,真正persist起来。
- 用EdgePartition去生成一个RoutingTablePartition,里面是vertexId到partitionId的对应关系,借助RoutingTablePartition生成VertexRDD。
- 由EdgeRDD和VertexRDD生成Graph。EdgeRDD维护了边的属性、边两头顶点的属性、两头顶点各自的global vertexID、两头顶点各自的local Id(在一个edge分区里的array index)、用于寻址array的正反向map。VertexRDD维护了点存在于哪个边的分区上的Map。
从这个流程当中可以看出RDD在GraphX当中依然是核心处理对象
杂谈
我还是比较喜欢富文本编辑器,跟本地的typora编辑器可以做到手感一致,用惯了typora,感觉非富文本编辑器都像抽风了一样
不用像word一样去改字体,真是不错
不过csdn最多只支持三级标题