Pregel模型

简介

Hadoop兴起之后,google又发布了三篇研究论文,分别阐述了了Caffeine、Pregel、Dremel三种技术,这三种技术也被成为google的新“三驾马车”,其中的Pregel是google提出的用于大规模分布式图计算框架。主要用于图遍历(BFS)、最短路径(SSSP)、PageRank计算等等计算。
Pregel计算模式中,输入是一个有向图,该有向图的每一个顶点都有一个相应的独一无二的顶点id (vertex identifier)。每一个顶点都有一些属性,这些属性可以被修改,其初始值由用户定义。每一条有向边都和其源顶点关联,并且也拥有一些用户定义的属性和值,并同时还记录了其目的顶点的ID。

一个典型的Pregel计算过程如下:读取输入,初始化该图,当图被初始化好后,运行一系列的supersteps,每一次superstep都在全局的角度上独立运行,直到整个计算结束,输出结果。

pregel中顶点有两种状态:活跃状态(active)和不活跃状态(halt)。如果某一个顶点接收到了消息并且需要执行计算那么它就会将自己设置为活跃状态。如果没有接收到消息或者接收到消息,但是发现自己不需要进行计算,那么就会将自己设置为不活跃状态。这种机制的描述如下图:



计算过程

Pregel中的计算分为一个个“superstep”,这些”superstep”中执行流程如下:
1、 首先输入图数据,并进行初始化。
2、 将每个节点均设置为活跃状态。每个节点根据预先定义好的sendmessage函数,以及方向(边的正向、反向或者双向)向周围的节点发送信息。
3、 每个节点接收信息如果发现需要计算则根据预先定义好的计算函数对接收到的信息进行处理,这个过程可能会更新自己的信息。如果接收到消息但是不需要计算则将自己状态设置为不活跃。
4、 每个活跃节点按照sendmessage函数向周围节点发送消息。
5、 下一个superstep开始,像步骤3一样继续计算,直到所有节点都变成不活跃状态,整个计算过程结束。
下面以一个具体例子来说明这个过程:假设一个图中有4个节点,从左到右依次为第1/2/3/4个节点。圈中的数字为节点的属性值,实线代表节点之间的边,虚线是不同超步之间的信息发送,带阴影的圈是不活跃的节点。我们的目的是让图中所有节点的属性值都变成最大的那个属性值。

superstep 0:首先所有节点设置为活跃,并且沿正向边向相邻节点发送自身的属性值。
Superstep 1:所有节点接收到信息,节点1和节点4发现自己接受到的值比自己的大,所以更新自己的节点(这个过程可以看做是计算),并保持活跃。节点23没有接收到比自己大的值,所以不计算、不更新。活跃节点继续向相邻节点发送当前自己的属性值。
Superstep 2:节点3接受信息并计算,其它节点没接收到信息或者接收到但是不计算,所以接下来只有节点3活跃并发送消息。
Superstep 3:节点24接受到消息但是不计算所以不活跃,所有节点均不活跃,所以计算结束。
pregel计算框架中有两个核心的函数:sendmessage函数和F(Vertex)节点计算函数。


过程详解

接下来详解这个过程:
在调用pregel方法时,initialGraph会被隐式转换成GraphOps类,这个类中pregel方法的源码如下:

    def pregel[A: ClassTag](  
    initialMsg: A,  
    maxIterations: Int = Int.MaxValue,  
    activeDirection: EdgeDirection = EdgeDirection.Either)(  
    vprog: (VertexId, VD, A) => VD,  
    sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],  
    mergeMsg: (A, A) => A)  
    : Graph[VD, ED] = {  
    Pregel(graph, initialMsg, maxIterations, activeDirection)(vprog, sendMsg, mergeMsg)  
    }  
这个方法采用的是典型的柯里化定义方式,第一个括号中的参数序列分别为initialMsg、maxIterations、activeDirection。第一个参数initialMsg表示第一次迭代时即superstep 0,每个节点接收到的消息。maxIterations表示迭代的最大次数,activeDirection表示消息发送的方向,该值为EdgeDirection类型,这是一个枚举类型,有三个可能值:EdgeDirection.In/ EdgeDirection.Out/ EdgeDirection.Either.可以看到,第二和第三个参数都有默认值。
第二个括号中参数序列为三个函数,分别为vprog、sendMsg和mergeMsg。
vprog是节点上的用户定义的计算函数,运行在单个节点之上,在superstep 0,这个函数会在每个节点上以初始的initialMsg为参数运行并生成新的节点值。在随后的超步中只有当节点收到信息,该函数才会运行。
sendMsg在当前超步中收到信息的节点用于向相邻节点发送消息,这个消息用于下一个超步的计算。
mergeMsg用于聚合发送到同一节点的消息,这个函数的参数为两个A类型的消息,返回值为一个A类型的消息。
最后调用Pregel对象的apply方法返回一个graph对象。

Apply方法的源码如下,我们可以看到graph和计算的参数都被传过来了:


    def apply[VD: ClassTag, ED: ClassTag, A: ClassTag]  
    (graph: Graph[VD, ED],  
    initialMsg: A,  
    maxIterations: Int = Int.MaxValue,  
    activeDirection: EdgeDirection = EdgeDirection.Either)  
    (vprog: (VertexId, VD, A) => VD,  
    sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],  
    mergeMsg: (A, A) => A)  
    : Graph[VD, ED] =  
    {  
    //要求最大迭代数大于0,不然报错。  
    require(maxIterations > 0, s"Maximum number of iterations must be greater than 0," +  
    s" but got ${maxIterations}")  
    //第一次迭代,对每个节点用vprog函数计算。  
    var g = graph.mapVertices((vid, vdata) => vprog(vid, vdata, initialMsg)).cache()  
    // 根据发送、聚合信息的函数计算下次迭代用的信息。  
    var messages = GraphXUtils.mapReduceTriplets(g, sendMsg, mergeMsg)  
    //数一下还有多少节点活跃  
    var activeMessages = messages.count()  
    // 下面进入循环迭代  
    var prevG: Graph[VD, ED] = null  
    var i = 0  
    while (activeMessages > 0 && i < maxIterations) {  
    // 接受消息并更新节点信息  
    prevG = g  
    g = g.joinVertices(messages)(vprog).cache()  
      
    val oldMessages = messages  
    // Send new messages, skipping edges where neither side received a message. We must cache  
    // messages so it can be materialized on the next line, allowing us to uncache the previous  
    /*iteration这里用mapReduceTriplets实现消息的发送和聚合。mapReduceTriplets的*参数中有一个map方法和一个reduce方法,这里的*sendMsg就是map方法,*mergeMsg就是reduce方法 
    */  
    messages = GraphXUtils.mapReduceTriplets(  
    g, sendMsg, mergeMsg, Some((oldMessages, activeDirection))).cache()  
    // The call to count() materializes `messages` and the vertices of `g`. This hides oldMessages  
    // (depended on by the vertices of g) and the vertices of prevG (depended on by oldMessages  
    // and the vertices of g).  
    activeMessages = messages.count()  
      
    logInfo("Pregel finished iteration " + i)  
      
    // Unpersist the RDDs hidden by newly-materialized RDDs  
    oldMessages.unpersist(blocking = false)  
    prevG.unpersistVertices(blocking = false)  
    prevG.edges.unpersist(blocking = false)  
    // count the iteration  
    i += 1  
    }  
    messages.unpersist(blocking = false)  
    g  
    } // end of apply  


GraphX中的单源点最短路径例子,使用的是类Pregel的方式。


核心部分是三个函数:

1.节点处理消息的函数  vprog: (VertexId, VD, A) => VD (节点id,节点属性,消息) => 节点属性

2.节点发送消息的函数 sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId,A)]   (边元组) => Iterator[(目标节点id,消息)]

3.消息合并函数 mergeMsg: (A, A) => A)    (消息,消息) => 消息


    package myclass.GraphX  
      
    import org.apache.spark.graphx._  
    import org.apache.spark.SparkContext  
      
    // Import random graph generation library  
      
    import org.apache.spark.graphx.util.GraphGenerators  
      
    /**  
     * Created by jack on 3/4/14.  
     */  
    object Pregel {  
        def main(args: Array[String]) {  
            val sc = new SparkContext("local", "pregel test", System.getenv("SPARK_HOME"), SparkContext.jarOfClass(this.getClass))  
            // A graph with edge attributes containing distances  
            //初始化一个随机图,节点的度符合对数正态分布,边属性初始化为1  
            val graph: Graph[Int, Double] =  
                GraphGenerators.logNormalGraph(sc, numVertices = 10).mapEdges(e => e.attr.toDouble)  
    graph.edges.foreach(println)  
            val sourceId: VertexId = 4 // The ultimate source  
      
            // Initialize the graph such that all vertices except the root have distance infinity.  
            //初始化各节点到原点的距离  
            val initialGraph = graph.mapVertices((id, _) => if (id == sourceId) 0.0 else Double.PositiveInfinity)  
      
            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"))  
        }  
    }  
首先将所有除了源顶点的其它顶点的属性值设置为无穷大,源顶点的属性值设置为0.
Superstep 0:然后对所有顶点用initialmsg进行初始化,实际上这次初始化并没有改变什么。
Superstep 1 :对于每个triplet:计算triplet.srcAttr + triplet.attr 和 triplet.dstAttr比较,以第一次为例:假设有一条边从0到a,这时就满足triplet.srcAttr + triplet.attr < triplet.dstAttr,这个triplet.attr的值实际上为1(没有自己指定,默认值都是1),而0的attr值我们早已初始化为0,0+1<无穷,所以发出的消息就是(a,1)这个在每个triplet中是从src发放dst的。如果某个边是从3到5,那么triplet.srcAttr + triplet.attr < triplet.dstAttr就不成立,因为无穷大加1等于无穷大,这时消息就是空的。Superstep 1就是这样,这一步执行完后图中所有的与0直接相连的点的attr都成了1而且成为获跃节点,其它点的attr不变同时变成不活跃节点。活结点根据triplet.srcAttr + triplet.attr < triplet.dstAttr继续发消息,mergeMsg函数会对发送到同一节点的多个消息进行聚合,聚合的结果就是最小的那个值。
Superstep 2:所有收到消息的节点比较自己的attr和发过来的attr,将较小的值作为自己的attr。然后自己成为活节点继续向周围的节点发送attr+1这个消息,然后再聚合。
直到没有节点的attr被更新,不再满足activeMessages > 0 && i < maxIterations (活跃节点数为大于0且没有达到最大允许迭代次数)。这时就得到节点0到其它节点的最短路径了。这个路径值保存在其它节点的attr中。




猜你喜欢

转载自blog.csdn.net/pq561017_/article/details/80324333
今日推荐