import org.apache.spark.graphx._
import org.apache.spark.SparkContext
import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkContext, SparkConf}
import org.apache.spark.rdd.RDD
// Import random graph generation library
//import org.apache.spark.graphx.util.GraphGenerators
import org.apache.spark.graphx.lib.ShortestPaths
object Pregel {
def main(args: Array[String]) {
//屏蔽日志
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
//设置运行环境
val conf = new SparkConf().setAppName("SimpleGraphX")
val sc = new SparkContext(conf)
//
val edgeFile:RDD[String] = sc.textFile("xrli/PregelEdge.txt")
//
val vertexFile:RDD[String] = sc.textFile("xrli/PregelVertex.txt")
//
//
//edge
//
val edge = edgeFile.map { e =>
//
val fields = e.split(" ")
//
Edge(fields(0).toLong,fields(1).toLong,fields(2))
//
}
//
//vertex
//
val vertex = vertexFile.map{e=>
//
val fields = e.split(" ")
//
(fields(0).toLong,fields(1))
//
//
}
//
//
val graph = Graph(vertex,edge,"").persist()
//
println(graph.edges.collect.mkString("\n"))
//设置顶点和边,注意顶点和边都是用元组定义的Array
//顶点的数据类型是VD:(String,Int)
val vertexArray = Array(
(1L, ("Alice", 28)),
//在这里后面的属性("Alice", 28)没有用上。
(2L, ("Bob", 27)),
(3L, ("Charlie", 65)),
(4L, ("David", 42)),
(5L, ("Ed", 55)),
(6L, ("Fran", 50))
)
//边的数据类型ED:Int
val edgeArray = Array(
Edge(2L, 1L, 7),
Edge(2L, 4L, 2),
Edge(3L, 2L, 4),
Edge(3L, 6L, 3),
Edge(4L, 1L, 1),
Edge(5L, 2L, 2),
Edge(5L, 3L, 8),
Edge(5L, 6L, 3)
)
//构造vertexRDD和edgeRDD
val vertexRDD: RDD[(Long, (String, Int))] = sc.parallelize(vertexArray)
val edgeRDD: RDD[Edge[Int]] = sc.parallelize(edgeArray)
//构造图Graph[VD,ED]
val graph: Graph[(String, Int), Int] = Graph(vertexRDD, edgeRDD)
val sourceId: VertexId = 5L // 定义源点
//初始化一个新的图,该图的节点属性为graph中各节点到原点的距离
val initialGraph = graph.mapVertices((id, _) => if (id == sourceId) 0.0 else Double.PositiveInfinity)
println(initialGraph.vertices.collect.mkString("\n"))
println(initialGraph.edges.collect.mkString("\n"))
val sssp = initialGraph.pregel(Double.PositiveInfinity)(
// Vertex Program,节点处理消息的函数,dist为原节点属性(Double),newDist为消息类(Double)
(id, dist, newDist) => math.min(dist, newDist),
// Send Message,发送消息函数,返回结果为(目标节点id,消息(即最短距离))
triplet => {
// 计算权重
if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
} else {
Iterator.empty
}
},
//Merge Message,对消息进行合并的操作,类似于Hadoop中的combiner
(a,b) => math.min(a,b) // 最短距离
)
println(sssp.vertices.collect.mkString("\n"))
sc.stop()
}
}
该程序是求所有节点到5号点的最短距离长度。
建立好
initialGraph后
println(initialGraph.vertices.collect.mkString("\n")) 打印结果如下
(1,Infinity)
(2,Infinity)
(3,Infinity)
(4,Infinity)
(5,0.0)
(6,Infinity)
println(initialGraph.edges.collect.mkString("\n")) 打印结果如下
Edge(2,1,7)
Edge(2,4,2)
Edge(3,2,4)
Edge(3,6,3)
Edge(4,1,1)
Edge(5,2,2)
Edge(5,3,8)
Edge(5,6,3)
结果输出:
(1, 5.0)
(2, 2.0)
(3, 8.0)
(4, 4.0)
(5, 0.0)
(6, 3.0)
Pregel的计算模型
某个图节点v从之前的超级步中接收到的消息队列中查找目前看到的最短路径,如果这个值比节点v当前获得的最短路径小,说明找到更短的路径,则更新节点数值为新的最短路径,之后将新值通过邻接节点传播出去,否则将当前节点转换为不活跃状态。
Pregel操作是一个约束到图拓扑的批量同步(bulk-synchronous)并行消息抽象。Pregel操作者执行一系列的超级步骤(super steps),在这些步骤中,顶点从 之前的超级步骤中接收进入(inbound)消息的总和,为顶点属性计算一个新的值,然后在以后的超级步骤中发送消息到邻居顶点。不像Pregel而更像GraphLab,消息作为一个边三元组的函数被并行 计算,消息计算既访问了源顶点特征也访问了目的顶点特征。在超级步中,没有收到消息的顶点被跳过。当没有消息遗留时,Pregel操作停止迭代并返回最终的图。
注意,与更标准的Pregel实现不同的是,GraphX中的顶点仅仅能发送信息给邻居顶点,并利用用户自定义的消息函数构造消息。这些限制允许在GraphX进行额外的优化。
主要分为三个函数:
1、vertexProgram函数
2、sendMessage函数
3、messageCombiner函数
def pregel[A](initialMsg: A, maxIterations: Int, activeDirection: EdgeDirection)(
vprog: (VertexId, Double, A) => Double,
sendMsg: EdgeTriplet[Double, Int] => Iterator[(VertexId, A)],
mergeMsg: (A, A) => A)
: Graph[Double, Int]
pregel有两个参数列表。第一个参数列表包含配置参数初始消息、最大迭代数、发送消息的边的方向(默认是沿边方向出)。第二个参数列表包含用户自定义的函数用来接收消息(vprog)、计算消息(sendMsg)、合并消息(mergeMsg)。
这里的泛型A在本程序中是Double类型
首先initialGraph.pregel(Double.PositiveInfinity)
这里传递了一个参数:initialMsg:在第一次迭代的时候顶点收到的消息,为无穷大Double.PositiveInfinity。这里也可以写成initialGraph.pregel[Double](Double.PositiveInfinity)
然后vprog:用户定义的顶点程序运行在每一个顶点中,负责接收进来的信息,和计算新的顶点值。在第一次迭代的时候,所有的顶点程序将会被默认的initialMsg调用,在次轮迭代中,顶点程序只有接收到message才会被调用。
(id, dist, newDist) => math.min(dist, newDist),
显而易见的是:两个消息来的时候,取它们当中路径的最小值。
initialGraph(vertexRDD, edgeRDD,属性)
这个属性是每个顶点到原点5的距离。
SendMessage函数的原理是:
triplet => {
// 计算权重
if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
} else {
Iterator.empty
}
},
我们再回顾一下triplet的形式
这里vertices形式如下(2,Infinity),(1,Infinity)
Edges形式如下: Edge(2L, 1L, 7),
假设我们当前处理的triplet的 A对应点2,B对应点1
所以triplet.srcAttr 就是点2到原点5的距离,
triplet.attr 就是这里的7(A、B间的长度权重)
triplet.dstAttr就是点1到原点5的距离。
如果triplet.srcAttr + triplet.attr < triplet.dstAttr)
即如果和更小,则通过Iterator发送该信息到目标顶点的函数。
Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
该信息的结构为(目标节点id,消息(即最短距离)),否则不发送。
注意到Pregel图算法整体思路是通过迭代计算每个顶点的属性直到到达定点条件。
第一次迭代,初始消息发的是无穷大,这个反正最后都会被math.min(a,b)给弄掉的。
下一次迭代的时候,对于下面几条边对应的triple暂时都不会有新的消息出来,还都是无穷大
Edge(2,1,7)
Edge(2,4,2)
Edge(3,2,4)
Edge(3,6,3)
Edge(4,1,1)
而对于下面这几条边对应的
triple则会有消息更新
Edge(5,2,2)
Edge(5,3,8)
Edge(5,6,3)
比如
Edge(5,2,2)
可以看出triplet.srcAttr + triplet.attr < triplet.dstAttr) 这里是 0+2
Iterator((triplet.dstId, triplet.srcAttr + triplet.attr)) 为(2, 2)
这样,再到下一轮的迭代中,节点处理函数vprog: (id, dist, newDist) => math.min(dist, newDist),
dist为原节点属性(Double)点2原来的节点属性是Inf,newDist为消息2。
所以,就会更新新的节点2属性值为2。
。。。。以此类推。
//Merge Message,对消息进行合并的操作,类似于Hadoop中的combiner
(a,b) => math.min(a,b)
// 对于同一定点所有接受到的消息(这里是最短距离)进行整合,取最小的。因为每一轮迭代中跟某一节点有关的消息可能不止一个,而我们只关心最小的就行了。