Spark—GraphX編程指南

Spark系列面試題

GraphX 是新的圖形和圖像並行計算的Spark API。從整理上看,GraphX 通過引入 彈性分佈式屬性圖(Resilient Distributed Property Graph)繼承了Spark RDD:一個將有效信息放在頂點和邊的有向多重圖。爲了支持圖形計算,GraphX 公開了一組基本的運算(例如,subgraph,joinVertices和mapReduceTriplets),以及在一個優化後的 PregelAPI的變形。此外,GraphX 包括越來越多的圖算法和 builder 構造器,以簡化圖形分析任務。

圖並行計算的背景

從社交網絡到語言建模,日益擴大的規模和圖形數據的重要性已帶動許多新的圖像並行系統(例如,Giraph和 GraphLab)。通過限制可表示計算的類型以及引入新的技術來劃分和分佈圖,這些系統比一般的數據並行系統在執行復雜圖形算法方面有大幅度地提高。

然而,這些限制在獲得重大性能提升的同時,也使其難以表達一個典型的圖表分析流程中的許多重要階段:構造圖,修改它的結構或表達計算跨越多重圖的計算。此外,如何看待數據取決於我們的目標,相同的原始數據,可能有許多不同的表(table)和圖表視圖(graph views)。

因此,能夠在同一組物理數據的表和圖表視圖之間切換是很有必要的,並利用各視圖的屬性,以方便地和有效地表達計算。但是,現有的圖形分析管道必須由圖並行和數據並行系統組成,從而導致大量的數據移動和重複以及複雜的編程模型。

該 GraphX 項目的目標是建立一個系統,建立一個統一的圖和數據並行計算的 API。該GraphX API 使用戶能夠將數據既可以當作一個圖,也可以當作集合(即RDDS)而不用進行數據移動或數據複製。通過引入在圖並行系統中的最新進展,GraphX能夠優化圖形操作的執行。

GraphX 替換 Spark Bagel 的 API

在GraphX 的發佈之前,Spark的圖計算是通過Bagel實現的,後者是Pregel的一個具體實現。GraphX提供了更豐富的圖屬性API,從而增強了Bagel。從而達到一個更加精簡的Pregel抽象,系統優化,性能提升以及減少內存開銷。雖然我們計劃最終棄用Bagel,我們將繼續支持Bagel的API和Bagel編程指南。不過,我們鼓勵Bagel用戶,探索新的GraphXAPI,並就從Bagel升級中遇到的障礙反饋給我們。

入門

首先,你要導入 Spark 和 GraphX 到你的項目,如下所示:

import org.apache.spark._
import  org.apache.spark.graphx._
// To make some of the examples work we will also need RDD
import  org.apache.spark.rdd.RDD

如果你不使用Spark shell,你還需要一個 SparkContext。要了解更多有關如何開始使用Spark參考 Spark快速入門指南。

屬性圖

該 屬性圖是一個用戶定義的頂點和邊的有向多重圖。有向多重圖是一個有向圖,它可能有多個平行邊共享相同的源和目的頂點。多重圖支持並行邊的能力簡化了有多重關係(例如,同事和朋友)的建模場景。每個頂點是 唯一 的 64位長的標識符(VertexID)作爲主鍵。GraphX並沒有對頂點添加任何順序的約束。同樣,每條邊具有相應的源和目的頂點的標識符。

該屬性表的參數由頂點(VD)和邊緣(ED)的類型來決定。這些是分別與每個頂點和邊相關聯的對象的類型。

GraphX 優化頂點和邊的類型的表示方法,當他們是普通的舊的數據類型(例如,整數,雙精度等)通過將它們存儲在專門的陣列減小了在內存佔用量。

在某些情況下,可能希望頂點在同一個圖中有不同的屬性類型。這可以通過繼承來實現。例如,以用戶和產品型號爲二分圖我們可以做到以下幾點:

class  VertexProperty()
case  class  UserProperty( val name: String)  extends  VertexProperty
case  class  ProductProperty( val name: String,  val price: Double)  extends
VertexProperty
// The graph might then have the type:
var graph: Graph[VertexProperty, String] = null

和 RDDS 一樣,屬性圖是不可變的,分佈式的和容錯的。對圖中的值或結構的改變是通過生成具有所需更改的新圖來完成的。注意原始圖的該主要部分(即不受影響的結構,屬性和索引)被重用,從而減少這個數據結構的成本。該圖是通過啓發式執行頂點分區,在不同的執行器(executor)中進行頂點的劃分。與 RDDS 一樣,在發生故障的情況下,圖中的每個分區都可以重建。

邏輯上講,屬性圖對應於一對類型集合(RDDS),這個組合記錄頂點和邊的屬性。因此,該圖表類包含成員訪問該圖的頂點和邊:

class  VertexProperty()
case  class  UserProperty( val name: String)  extends  VertexProperty
case  class  ProductProperty( val name: String,  val price: Double)  extends
VertexProperty
// The graph might then have the type:
var graph: Graph[VertexProperty, String] = null

類 VertexRDD [VD]和 EdgeRDD[ED,VD]繼承和並且分別是一個優化的版本的RDD[(VertexID,VD)]和RDD[Edge[ED]]。這兩個VertexRDD[VD]和EdgeRDD[ED,VD]提供各地圖的計算內置附加功能,並充分利用內部優化。我們在上一節頂點和邊RDDS中詳細討論了VertexRDD和EdgeRDD的API,但現在,他們可以簡單地看成是RDDS形式的: RDD[(VertexID,VD)]和 RDD [EDGE[ED]] 。

屬性圖的例子

假設我們要建立一個 GraphX項目各合作者的屬性圖。頂點屬性可能會包含用戶名和職業。我們可以使用一組字符註釋來描述代表合作者關係的邊:

由此產生的圖形將有類型簽名:

val userGraph: Graph[(String, String), String]

有許多方法可以從原始數據文件,RDDS,甚至合成生成器來生成圖,我們會在 graph builders 更詳細的討論。可能是最通用的方法是使用 Graph ojbect。例如,下面的代碼從一系列的RDDS的集合中構建圖:

// Assume the SparkContext has already been constructed
val sc: SparkContext
// Create an RDD for the vertices
val users: RDD[(VertexId, (String, String))] =
    sc.parallelize( Array((3L, ("rxin", "student")), (7L, ("jgonzal","postdoc")),(5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))
// Create an RDD for edges
val relationships: RDD[Edge[String]] =
    sc.parallelize( Array( Edge(3L, 7L, "collab"), Edge(5L, 3L, "advisor"),Edge(2L, 5L, "colleague"),  Edge(5L, 7L, "pi")))
// Define a default user in case there are relationship with missing user
val defaultUser = ("John Doe", "Missing")
// Build the initial Graph
val graph =  Graph(users, relationships, defaultUser)

在上面的例子中,我們利用了Edge的case類。Edge具有srcId和dstId,它們分別對應於源和目的地頂點的標識符。此外,Edge 類具有 attr屬性,並存儲的邊的特性。

我們可以通過 graph.vertices 和 graph.edges屬性,得到圖到各自的頂點和邊的視圖。

val graph: Graph[(String, String), String]  // Constructed from above
// Count all users which are postdocs
graph.vertices.filter {  case (id, (name, pos))  => pos == "postdoc" }.count
// Count all the edges where src > dst
graph.edges.filter(e  => e.srcId > e.dstId).count

需要注意的是graph.vertices返回VertexRDD[(String,String)]延伸RDD[(VertexID,(String,String))],所以我們使用Scala的case表達來解構元組。在另一方面,graph.edges返回EdgeRDD包含Edge[String]對象。我們可以也使用的如下的類型的構造器:

graph.edges.filter {  case  Edge(src, dst, prop)  => src > dst }.count

除了 圖的頂點和邊的屬性,GraphX 也提供了三重視圖。三重視圖邏輯連接點和邊的屬性產生的 RDD[EdgeTriplet [VD,ED]包含的實例 EdgeTriplet類。此 連接 可以表示如下的SQL表達式:

SELECT src.id, dst.id, src.attr, e.attr, dst.attr
FROM edges  AS e  LEFT  JOIN vertices  AS src, vertices  AS dst
ON e.srcId = src.Id  AND e.dstId = dst.Id

或圖形方式:

該 EdgeTriplet類繼承了 Edge並加入了類屬性:srcAttr和dstAttr,用於包含了源和目標屬性。我們可以用一個圖的三元組視圖渲染描述用戶之間的關係字符串的集合。

val graph: Graph[(String, String), String]  // Constructed from above
// Use the triplets view to create an RDD of facts.
val facts: RDD[String] =
    graph.triplets.map(triplet  =>
        triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
facts.collect.foreach(println(_ _))

也可以參照下面的操作獲取全部屬性

graph.triplets.foreach(t => println(s"triplet:${t.srcId},${t.srcAttr},${t.dstId},${t.dstAttr},${t.attr}"))

Graph 操作

正如RDDs有這樣基本的操作map,filter,和reduceByKey,屬性圖也有一系列基本的運算,採用用戶定義的函數,併產生新的圖形與變換的性質和結構。定義核心運算已優化的實現方式中定義的Graph,並且被表示爲核心操作的組合定義在GraphOps。然而,由於Scala的implicits特性,GraphOps中的操作會自動作爲Graph的成員。例如,我們可以計算各頂點的入度(定義在的 GraphOps):

val graph: Graph[(String, String), String]
// Use the implicit GraphOps.inDegrees operator
val inDegrees: VertexRDD[Int] = graph.inDegrees

將核心圖操作和 GraphOps區分開來的原因是爲了將來能夠支持不同的圖表示。每個圖的表示必須實現核心操作並且複用 GraphOps中很多有用的操作。

運算列表總結

以下列出了Graph圖 和 GraphOps中同時定義的操作.爲了簡單起見,我們都定義爲Graph的成員函數。請注意,某些函數簽名已被簡化(例如,默認參數和類型的限制被刪除了),還有一些更高級的功能已被刪除,完整的列表,請參考API文檔。

/** Summary of the functionality in the property graph */
class  Graph[VD, ED] {
    //  Information  about  the  Graph
    val numEdges: Long
    val numVertices: Long
    val inDegrees: VertexRDD[Int]
    val outDegrees: VertexRDD[Int]
    val degrees: VertexRDD[Int]
    //  Views  of  the  graph  as  collections
    val vertices: VertexRDD[VD]
    val edges: EdgeRDD[ED, VD]
    val triplets: RDD[EdgeTriplet[VD, ED]]
    //  Functions  for  caching  graphs
    def persist(newLevel: StorageLevel =  StorageLevel. MEMORY_ONLY): Graph[VD, ED]
    def cache(): Graph[VD, ED]
    def unpersistVertices(blocking: Boolean =  true): Graph[VD, ED]
    //  Change  the  partitioning  heuristic
    def partitionBy(partitionStrategy: PartitionStrategy): Graph[VD, ED]
    //  Transform  vertex  and  edge  attributes
    def mapVertices[VD2](map: (VertexID, VD)  =>  VD2): Graph[VD2, ED]
    def mapEdges[ED2](map: Edge[ED]  =>  ED2): Graph[VD, ED2]
    def mapEdges[ED2](map: (PartitionID, Iterator[Edge[ED]])  =>  Iterator[ED2]):Graph[VD, ED2]
    def mapTriplets[ED2](map: EdgeTriplet[VD, ED]  =>  ED2): Graph[VD, ED2]
    def mapTriplets[ED2](map: (PartitionID, Iterator[EdgeTriplet[VD, ED]])  =>
    Iterator[ED2]): Graph[VD, ED2]
    //  Modify  the  graph  structure
    def reverse: Graph[VD, ED]
    def subgraph(
            epred: EdgeTriplet[VD,ED]  =>  Boolean = (x  =>  true),
            vpred: (VertexID, VD)  =>  Boolean = ((v, d)  =>  true)
        ): Graph[VD, ED]
    def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
    def groupEdges(merge: (ED, ED)  =>  ED): Graph[VD, ED]
    //  Join  RDDs  with  the  graph
    def joinVertices[U](table: RDD[(VertexID, U)])(mapFunc: (VertexID, VD, U)
    =>  VD): Graph[VD, ED]
    def outerJoinVertices[U, VD2](other: RDD[(VertexID, U)])
    (mapFunc: (VertexID, VD,  Option[U])  =>  VD2)
    : Graph[VD2, ED]
    //  Aggregate  information  about  adjacent  triplets
    def  collectNeighborIds(edgeDirection:  EdgeDirection): VertexRDD[Array[VertexID]]
    def  collectNeighbors(edgeDirection:  EdgeDirection):VertexRDD[Array[(VertexID, VD)]]
    def mapReduceTriplets[A: ClassTag](
            mapFunc: EdgeTriplet[VD, ED]  =>  Iterator[(VertexID, A)],
            reduceFunc: (A, A)  => A,
            activeSetOpt: Option[(VertexRDD[_ _], EdgeDirection)] =  None
        ): VertexRDD[A]
    //  Iterative  graph-parallel  computation
    def pregel[A](initialMsg: A, maxIterations: Int, activeDirection:  EdgeDirection)(
            vprog: (VertexID, VD, A)  =>  VD,
            sendMsg: EdgeTriplet[VD, ED]  =>  Iterator[(VertexID,A)],
            mergeMsg: (A, A)  => A
        ): Graph[VD, ED]
    //  Basic  graph  algorithms
    def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
    def connectedComponents(): Graph[VertexID, ED]
    def triangleCount(): Graph[Int, ED]
    def stronglyConnectedComponents(numIter: Int): Graph[VertexID, ED]
}

屬性操作

和RDD的 map操作類似,屬性圖包含以下內容:

class  Graph[VD, ED] {
    def mapVertices[VD2](map: (VertexId, VD)  =>  VD2): Graph[VD2, ED]
    def mapEdges[ED2](map: Edge[ED]  =>  ED2): Graph[VD, ED2]
    def mapTriplets[ED2](map: EdgeTriplet[VD, ED]  =>  ED2): Graph[VD, ED2]
}

每個運算產生一個新的圖,這個圖的頂點和邊屬性通過 map方法修改。

請注意,在所有情況下的圖的機構不受影響。這是這些運算符的關鍵所在,它允許新得到圖可以複用初始圖的結構索引。下面的代碼段在邏輯上是等效的,但第一個不保留結構索引,所以不會從 GraphX 系統優化中受益:

123 val newVertices = graph.vertices.map { case (id, attr) => (id, mapUdf(id,attr)) }val newGraph = Graph(newVertices, graph.edges)

相反,使用 mapVertices保存索引:

1 val newGraph = graph.mapVertices((id, attr) => mapUdf(id, attr))

這些操作經常被用來初始化圖的特定計算或者去除不必要的屬性。例如,給定一個將出度作爲頂點的屬性圖(我們之後將介紹如何構建這樣的圖),我們初始化它作爲 PageRank:

// Given a graph where the vertex property is the out-degree
val inputGraph: Graph[Int, String] =
    graph.outerJoinVertices(graph.outDegrees)((vid,  _ _,  degOpt)  => degOpt.getOrElse(0))
// Construct a graph where each edge contains the weight
// and each vertex is the initial PageRank
val outputGraph: Graph[Double, Double] =
    inputGraph.mapTriplets(triplet  => 1.0 / triplet.srcAttr).mapVertices((id, _ _) => 1.0)

結構操作

當前 GraphX 只支持一組簡單的常用結構化操作,我們希望將來增加更多的操作。以下是基本的結構運算符的列表。

class  Graph[VD, ED] {   
	def reverse: Graph[VD, ED]   
  def subgraph(epred: EdgeTriplet[VD,ED]  =>  Boolean, vpred: (VertexId, VD)  =>  Boolean): Graph[VD, ED] 
  def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]  
  def groupEdges(merge: (ED, ED)  =>  ED): Graph[VD,ED]}

該reverse操作符返回一個新圖,新圖的邊的方向都反轉了。這是非常實用的,例如,試圖計算逆向PageRank。因爲反向操作不修改頂點或邊屬性或改變的邊的數目,它的實現不需要數據移動或複製。

該子圖subgraph將頂點和邊的預測作爲參數,並返回一個圖,它只包含滿足了頂點條件的頂點圖(值爲true),以及滿足邊條件 並連接頂點的邊。subgraph子運算符可應用於很多場景,以限制圖表的頂點和邊是我們感興趣的,或消除斷開的鏈接。例如,在下面的代碼中,我們刪除已損壞的鏈接:

// Create an RDD for the vertices
val users: RDD[(VertexId, (String, String))] =
        sc.parallelize( Array((3L, ("rxin", "student")), (7L, ("jgonzal","postdoc")),(5L, ("franklin", "prof")), (2L, ("istoica", "prof")),(4L, ("peter", "student"))))
// Create an RDD for edges
val relationships: RDD[Edge[String]] =
        sc.parallelize( Array( Edge(3L, 7L, "collab"), Edge(5L, 3L, "advisor"),Edge(2L, 5L, "colleague"),  Edge(5L, 7L, "pi"),Edge(4L, 0L, "student"), Edge(5L, 0L, "colleague")))
// Define a default user in case there are relationship with missing user
val defaultUser = ("John Doe", "Missing")
// Build the initial Graph
val graph =  Graph(users, relationships, defaultUser)
// Notice that there is a user 0 (for which we have no information) connected to users
// 4 (peter) and 5 (franklin).
graph.triplets.map(
    triplet  => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
).collect.foreach(println(_ _))
// Remove missing vertices as well as the edges to connected to them
val validGraph = graph.subgraph(vpred = (id, attr)  => attr._2 != "Missing")
// The valid subgraph will disconnect users 4 and 5 by removing user 0
validGraph.vertices.collect.foreach(println(_ _))
validGraph.triplets.map(
    triplet  => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
).collect.foreach(println(_ _))

注意,在上面的例子中,僅提供了頂點條件。如果不提供頂點或邊的條件,在subgraph 操作中默認爲 真 。

mask操作返回一個包含輸入圖中所有的頂點和邊的圖。這可以用來和subgraph一起使用,以限制基於屬性的另一個相關圖。例如,我們用去掉頂點的圖來運行聯通分量,並且限制輸出爲合法的子圖。

// Run Connected Components
val ccGraph = graph.connectedComponents()  // No longer contains missing field
// Remove missing vertices as well as the edges to connected to them
val validGraph = graph.subgraph(vpred = (id, attr)  => attr._2 != "Missing")
// Restrict the answer to the valid subgraph
val validCCGraph = ccGraph.mask(validGraph)

該 groupEdges操作合併在多重圖中的平行邊(即重複頂點對之間的邊)。在許多數值計算的應用中,平行的邊緣可以加入 (他們的權重的會被彙總)爲單條邊從而降低了圖形的大小。

Join 操作

在許多情況下,有必要從外部集合(RDDS)中加入圖形數據。例如,我們可能有額外的用戶屬性,想要與現有的圖形合併,或者我們可能需要從一個圖選取一些頂點屬性到另一個圖。這些任務都可以使用來 join 經操作完成。下面我們列出的關鍵聯接運算符:

class  Graph[VD, ED] {
    def joinVertices[U](table: RDD[(VertexId, U)])(map: (VertexId, VD, U)  => VD): Graph[VD, ED]
    def outerJoinVertices[U, VD2](table: RDD[(VertexId, U)])(map: (VertexId, VD,  Option[U])  =>  VD2): Graph[VD2, ED]
}

該 joinVertices運算符連接與輸入RDD的頂點,並返回一個新的圖,新圖的頂點屬性是通過用戶自定義的 map功能作用在被連接的頂點上。沒有匹配的RDD保留其原始值。

需要注意的是,如果RDD頂點包含多於一個的值,其中只有一個將會被使用。因此,建議在輸入的RDD在初始爲唯一的時候,使用下面的 pre-index 所得到的值以加快後續join。

val nonUniqueCosts: RDD[(VertexID, Double)]
val uniqueCosts: VertexRDD[Double] = 
        graph.vertices.aggregateUsingIndex(nonUnique, (a,b) => a + b)
val joinedGraph = graph.joinVertices(uniqueCosts)(
        (id, oldCost, extraCost)  => oldCost + extraCost)

更一般 outerJoinVertices操作類似於joinVertices,除了將用戶定義的map函數應用到所有的頂點,並且可以改變頂點的屬性類型。因爲不是所有的頂點可能會在輸入匹配值RDD的mpa函數接受一個Optin類型。例如,我們可以通過

用 outDegree 初始化頂點屬性來設置一個圖的 PageRank。

val outDegrees: VertexRDD[Int] = graph.outDegrees
val degreeGraph = graph.outerJoinVertices(outDegrees) { 
    (id, oldAttr, outDegOpt)  =>
        outDegOpt  match {
            case  Some(outDeg)  => outDeg
            case  None  => 0  // No outDegree means zero outDegree
        }
}

您可能已經注意到,在上面的例子中採用了多個參數列表的curried函數模式(例如,f(a)(b))。雖然我們可以有同樣寫f(a)(b)爲f(a,b),這將意味着該類型推斷b不依賴於a。其結果是,用戶將需要提供類型標註給用戶自定義的函數:

val joinedGraph = graph.joinVertices(uniqueCosts,  (id: VertexID, oldCost: Double, extraCost: Double)  => oldCost + extraCost) 

鄰居聚集

圖形計算的一個關鍵部分是聚集每個頂點的鄰域信息。例如,我們可能想要知道每個用戶追隨者的數量或每個用戶的追隨者的平均年齡。許多圖迭代算法(如PageRank,最短路徑,連通分量等)反覆聚集鄰居節點的屬性, (例如,當前的 PageRank 值,到源節點的最短路徑,最小可達頂點 ID)。

mapReduceTriplets

GraphX中核心(大量優化)聚集操作是 mapReduceTriplets操作:

class  Graph[VD, ED] {
    def reverse: Graph[VD, ED]
    def subgraph(epred: EdgeTriplet[VD,ED]  =>  Boolean, vpred: (VertexId, VD)  =>  Boolean): Graph[VD, ED]
    def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
    def groupEdges(merge: (ED, ED)  =>  ED): Graph[VD,ED]
}

該 mapReduceTriplets運算符將用戶定義的map函數作爲輸入,並且將map作用到每個triplet,並可以得到triplet上所有的頂點(或者兩個,或者空)的信息。爲了便於優化預聚合,我們目前僅支持發往triplet的源或目的地的頂點信息。用戶定義的reduce功能將合併所有目標頂點相同的信息。該mapReduceTriplets操作返回 VertexRDD [A] ,包含所有以每個頂點作爲目標節點集合消息(類型 A)。沒有收到消息的頂點不包含在返回 VertexRDD。

需要注意的是 mapReduceTriplets需要一個附加的可選activeSet(上面沒有顯示,請參見API文檔的詳細信息),這限制了 VertexRDD地圖提供的鄰接邊的map階段:

activeSetOpt: Option[(VertexRDD[_], EdgeDirection)] = None 

該EdgeDirection指定了哪些和頂點相鄰的邊包含在map階段。如果該方向是in,則用戶定義的 mpa函數 將僅僅作用目標頂點在與活躍集中。如果方向是out,則該map函數將僅僅作用在那些源頂點在活躍集中的邊。如果方向是 either,則map函數將僅在任一頂點在活動集中的邊。如果方向是both,則map函數將僅作用在兩個頂點都在活躍集中。活躍集合必須來自圖的頂點中。限制計算到相鄰頂點的一個子集三胞胎是增量迭代計算中非常必要,而且是GraphX 實現Pregel中的關鍵。

在下面的例子中我們使用 mapReduceTriplets算子來計算高級用戶追隨者的平均年齡。

// Import random graph generation library
import  org.apache.spark.graphx.util.GraphGenerators
// Create a graph with "age" as the vertex property. Here we use a random graph for simplicity.
val graph: Graph[Double, Int] =
        GraphGenerators.logNormalGraph(sc, numVertices = 100).mapVertices( 
            (id, _ _) => id.toDouble )
// Compute the number of older followers and their total age
val olderFollowers: VertexRDD[(Int, Double)] = graph.mapReduceTriplets[(Int,Double)](
    triplet  => {  // Map Function
        if (triplet.srcAttr > triplet.dstAttr) {
            // Send message to destination vertex containing counter and age
            Iterator((triplet.dstId, (1, triplet.srcAttr)))
        }  else {
            // Don't send a message for this triplet
            Iterator.empty
        }
    },
    // Add counter and age
    (a, b)  => (a._1 + b._1, a._2 + b._2)  // Reduce Function
)
// Divide total age by number of older followers to get average age of older followers
val avgAgeOfOlderFollowers: VertexRDD[Double] =
    olderFollowers.mapValues( (id, value)  => value  
        match {  case (count, totalAge) => totalAge / count } )
// Display the results
avgAgeOfOlderFollowers.collect.foreach(println(_ _))

注意,當消息(和消息的總和)是固定尺寸的時候(例如,浮點運算和加法而不是列表和連接)時,mapReduceTriplets 操作執行。更精確地說,結果 mapReduceTriplets 最好是每個頂點度的次線性函數。

計算度信息

一個常見的聚合任務是計算每個頂點的度:每個頂點相鄰邊的數目。在有向圖的情況下,往往需要知道入度,出度,以及總度。該GraphOps類包含一系列的運算來計算每個頂點的度的集合。例如,在下面我們計算最大的入度,出度,總度:

// Define a reduce operation to compute the highest degree vertex
def max(a: (VertexId, Int), b: (VertexId, Int)): (VertexId, Int) = {
    if (a._2 > b._2) a  else b
}
// Compute the max degrees
val maxInDegree: (VertexId, Int) = graph.inDegrees.reduce(max)
val maxOutDegree: (VertexId, Int) = graph.outDegrees.reduce(max)
val maxDegrees: (VertexId, Int) = graph.degrees.reduce(max)

收集鄰居

在某些情況下可能更容易通過收集相鄰頂點和它們的屬性來表達在每個頂點表示的計算。這可以通過使用容易地實現 collectNeighborIds和 collectNeighbors運算。

class GraphOps[VD, ED] {
    def  collectNeighborIds(edgeDirection:  EdgeDirection): VertexRDD[Array[VertexId]]
    def  collectNeighbors(edgeDirection:  EdgeDirection): VertexRDD[ Array[(VertexId, VD)] ]
}

需要注意的是,這些運算計算代價非常高,因爲他們包含重複信息,並且需要大量的通信。如果可能的話儘量直接使用 mapReduceTriplets。

緩存和清空緩存

在Spark中,RDDS默認並不保存在內存中。爲了避免重複計算,當他們需要多次使用時,必須明確地使用緩存(見 Spark編程指南)。在GraphX中Graphs行爲方式相同。當需要多次使用圖形時,一定要首先調用Graph.cache。

在迭代計算,爲了最佳性能,也可能需要清空緩存。默認情況下,緩存的RDDS和圖表將保留在內存中,直到內存壓力迫使他們按照LRU順序被刪除。對於迭代計算,之前的迭代的中間結果將填補緩存。雖然他們最終將被刪除,內存中的不必要的數據會使垃圾收集機制變慢。一旦它們不再需要緩存,就立即清空中間結果的緩存,這將會更加有效。這涉及物化(緩存和強迫)圖形或RDD每次迭代,清空所有其他數據集,並且只使用物化數據集在未來的迭代中。然而,由於圖形是由多個RDDS的組成的,正確地持續化他們將非常困難。對於迭代計算,我們推薦使用 Pregel API,它正確地 unpersists 中間結果。

Pregel 的 API

圖本質上是遞歸的數據結構,因爲頂點的性質取決於它們的鄰居,這反過來又依賴於鄰居的屬性。其結果是許多重要的圖形算法迭代重新計算每個頂點的屬性,直到定點條件滿足爲止。一系列圖像並行方法已經被提出來表達這些迭代算法。GraphX 提供了類似與Pregel 的操作,這是 Pregel 和 GraphLab 方法的融合。

從總體來看,Graphx 中的 Pregel 是一個批量同步並行消息傳遞抽象 約束到該圖的拓撲結構。Pregel 運算符在一系列超步驟中,其中頂點收到從之前的步驟中流入消息的總和,計算出頂點屬性的新值,然後在下一步中將消息發送到相鄰的頂點。不同於Pregel,而是更像GraphLab消息被並行計算,並且作爲edge-triplet,該消息的計算可以訪問的源和目的地的頂點屬性。沒有收到消息的頂點在一個超級步跳過。當沒有消息是,Pregel 停止迭代,並返回最終圖形。

請注意,不像更標準的 Pregel的實現,在GraphX中頂點只能將消息發送到鄰近的頂點,並且信息構建是通過使用用戶定義的消息函數並行執行。這些限制使得在 GraphX 有額外的優化。

以下是類型簽名 Pregel,以及一個 初始的實現 (注調用graph.cache已被刪除)中:

class  GraphOps[VD, ED] {
    def pregel[A](
    initialMsg: A,
    maxIter: Int =  Int. MaxValue,
    activeDir: EdgeDirection =  EdgeDirection. Out)(
        vprog: (VertexId, VD, A)  =>  VD,
        sendMsg: EdgeTriplet[VD, ED]  =>  Iterator[(VertexId, A)],
        mergeMsg: (A, A)  => A
    ): Graph[VD, ED] = {
        // Receive the initial message at each vertex
        var  g  =  mapVertices(  (vid,  vdata)  =>  vprog(vid,  vdata, initialMsg) ).cache()
        // compute the messages
        var messages = g.mapReduceTriplets(sendMsg, mergeMsg)
        var activeMessages = messages.count()
        // Loop until no messages remain or maxIterations is achieved
        var i = 0
        while (activeMessages > 0 && i < maxIterations) {
            //  Receive  the  messages:------
            // Run the vertex program on all vertices that receive messages
            val newVerts = g.vertices.innerJoin(messages)(vprog).cache()
            // Merge the new vertex values back into the graph
            g  =  g.outerJoinVertices(newVerts)  {  (vid,  old,  newOpt)  =>  newOpt.getOrElse(old) }.cache()
            //  Send  Messages:-----
            --
            // Vertices that didn't receive a message above don't appear in newVerts and therefore don't
            // get to send messages. More precisely the map phase of mapReduceTriplets is only invoked
            // on edges in the activeDir of vertices in newVerts
            messages = g.mapReduceTriplets(sendMsg, mergeMsg,  Some((newVerts, activeDir))).cache()
            activeMessages = messages.count()
            i += 1
        }
        g
    }
}

請注意,Pregel 需要兩個參數列表(即graph.pregel(list1)(list2))。第一個參數列表中包含的配置參數包括初始信息,迭代的最大次數,以及發送消息(默認出邊)的方向。第二個參數列表包含用於用戶定義的接收消息(頂點程序 vprog),計算消息(sendMsg),並結合信息 mergeMsg。

我們可以使用 Pregel 運算符來表達計算,如在下面的例子中的單源最短路徑。

import  org.apache.spark.graphx._
// Import random graph generation library
import  org.apache.spark.graphx.util.GraphGenerators
// A graph with edge attributes containing distances
val graph: Graph[Int, Double] =
        GraphGenerators.logNormalGraph(sc, numVertices = 100).mapEdges(e => e.attr.toDouble)
val sourceId: VertexId = 42  // 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)(
    (id, dist, newDist)  => math.min(dist, newDist),  // Vertex Program
    triplet  => { // Send Message
        if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
            Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
        }  else {
            Iterator.empty
        }
    },
    (a,b)  => math.min(a,b)  // Merge Message
)
println(sssp.vertices.collect.mkString("\n"))

Graph Builder

GraphX 提供多種從RDD或者硬盤中的節點和邊中構建圖。默認情況下,沒有哪種Graph Builder會重新劃分圖的邊;相反,邊會留在它們的默認分區(如原來的HDFS塊)。Graph.groupEdges需要的圖形進行重新分區,因爲它假設相同的邊將被放在同一個分區同一位置,所以你必須在調用Graph.partitionBy之前調用groupEdges。

object  GraphLoader {
    def edgeListFile(
        sc: SparkContext,
        path: String,
        canonicalOrientation: Boolean =  false,
        minEdgePartitions: Int = 1
    ): Graph[Int, Int]
}

GraphLoader.edgeListFile提供了一種從磁盤上邊的列表載入圖的方式。它解析了一個以下形式的鄰接列表(源頂點ID,目的地頂點ID)對,忽略以#開頭的註釋行:

 # This is a comment2 14 11 2

它從指定的邊創建了一個圖表,自動邊中提到的任何頂點。所有頂點和邊的屬性默認爲1。canonicalOrientation參數允許重新定向邊的正方向(srcId < dstId),這是必需的connected-component算法。該minEdgePartitions參數指定邊緣分區生成的最小數目;例如,在HDFS文件具有多個塊, 那麼就有多個邊的分割.

object Graph {
    def apply[VD, ED](
            vertices: RDD[(VertexId, VD)],
            edges: RDD[Edge[ED]],
            defaultVertexAttr: VD =  null
    ): Graph[VD, ED]
    
    def fromEdges[VD, ED](
            edges: RDD[Edge[ED]],
            defaultValue: VD): Graph[VD, ED]
    
    def fromEdgeTuples[VD](
            rawEdges: RDD[(VertexId, VertexId)],
            defaultValue: VD,
            uniqueEdges: Option[PartitionStrategy] =  None
    ): Graph[VD, Int]
}

Graph.apply允許從頂點和邊的RDDS中創建的圖。重複的頂點會任意選擇,並在邊RDD中存在的頂點, 但不是頂點RDD會被賦值爲默認屬性。

Graph.fromEdges允許從只有邊的元組RDD創建的圖,自動生成由邊中存在的頂點,並且給這些頂點賦值爲缺省值。

Graph.fromEdgeTuples允許從只有邊的元組的RDD圖中創建圖,並將的邊的值賦爲1,並自動創建邊中所存在的頂點,並設置爲缺省值。它也支持刪除重邊; 進行刪除重邊時,傳入 PartitionStrategy的Some 作爲uniqueEdges參數(例如,uniqueEdges=Some(PartitionStrategy.RandomVertexCut))。分區策略是必要的,因爲定位在同一分區相同的邊,才能使他們能夠進行重複刪除。

頂點和邊 RDDs

GraphX 公開了圖中 RDD 頂點和邊的視圖。然而,因爲GraphX將頂點和邊保存在優化的數據結構,並且爲這些數據結構提供額外的功能,頂點和邊分別作爲VertexRDD和EdgeRDD返回。在本節中,我們回顧一些這些類型的其他有用的功能。

VertexRDDs

該VertexRDD [A]繼承RDD [(VertexID, A)],並增加了一些額外的限制 ,每個VertexID只出現 一次 。此外,VertexRDD[A]表示一個頂點集合,其中每個頂點與類型的屬性爲A。在內部,這是通過將頂點屬性中存儲在一個可重複使用的哈希表。因此,如果兩個VertexRDDs繼承自相同的基類VertexRDD(例如,通過filter或mapValues ),他們可以參加在常數時間內實現合併,而不需要重新計算hash值。要充分利用這個索引數據結構,VertexRDD提供了以下附加功能:

class  VertexRDD[VD]  extends  RDD[(VertexID, VD)] {
    // Filter the vertex set but preserves the internal index
    def filter(pred: Tuple2[VertexId, VD]  =>  Boolean): VertexRDD[VD]
    // Transform the values without changing the ids (preserves the internal index)
    def mapValues[VD2](map: VD =>  VD2): VertexRDD[VD2]
    def mapValues[VD2](map: (VertexId, VD)  =>  VD2): VertexRDD[VD2]
    // Remove vertices from this set that appear in the other set
    def diff(other: VertexRDD[VD]): VertexRDD[VD]
    // Join operators that take advantage of the internal indexing to accelerate joins (substantially)
    def leftJoin[VD2, VD3](other: RDD[(VertexId, VD2)])(f: (VertexId, VD, Option[VD2])  =>  VD3): VertexRDD[VD3]
    def innerJoin[U, VD2](other: RDD[(VertexId, U)])(f: (VertexId, VD, U)  => VD2): VertexRDD[VD2]
    // Use the index on this RDD to accelerate a `reduceByKey` operation on the input RDD.
    def aggregateUsingIndex[VD2](other: RDD[(VertexId, VD2)], reduceFunc: (VD2, VD2)  =>  VD2): VertexRDD[VD2]
}

請注意,例如,如何filter操作符返回一個VertexRDD。過濾器使用的是實際通過BitSet實現的,從而複用索引和保持能快速與其他 VertexRDD 實現連接功能。 類似地,mapValues 操作不允許mapha函數改變 VertexID,從而可以複用統一HashMap中的數據結構。當兩個VertexRDD派生自同一HashMap,並且是通過線性少買而非代價昂貴的逐點查詢時,無論是 leftJoin 和 innerJoin 連接時能夠識別 VertexRDD 。

該aggregateUsingIndex操作是一種新的有效的從RDD[(VertexID,A)]構建新的VertexRDD的方式。從概念上講,如果我在一組頂點上構建了一個VertexRDD[B],這是一個在某些頂點RDD[(VertexID,A)]的超集,然後我可以重用該索引既聚集,隨後爲RDD[(VertexID, A)]建立索引。例如:

val setA: VertexRDD[Int] =  VertexRDD(sc.parallelize(0L until 100L).map(id  => (id, 1)))
val rddB: RDD[(VertexId, Double)] = sc.parallelize(0L until 100L).flatMap(id =>  List((id, 1.0), (id, 2.0)))
// There should be 200 entries in rddB
rddB.count
val setB: VertexRDD[Double] = setA.aggregateUsingIndex(rddB, _ _ + _ _)
// There should be 100 entries in setB
setB.count
// Joining A and B should now be fast!
val setC: VertexRDD[Double] = setA.innerJoin(setB)((id, a, b)  => a + b)

EdgeRDDs

該EdgeRDD [ED,VD] ,它繼承RDD[Edge[ED],以各種分區策略PartitionStrategy將邊劃分成不同的塊。在每個分區中,邊屬性和鄰接結構,分別存儲,這使得更改屬性值時,能夠最大限度的複用。

EdgeRDD 是提供的三個額外的函數:

// Transform the edge attributes while preserving the structure
def mapValues[ED2](f: Edge[ED]  =>  ED2): EdgeRDD[ED2, VD]
// Revere the edges reusing both attributes and structure
def reverse: EdgeRDD[ED, VD]
// Join two `EdgeRDD`s partitioned using the same partitioning strategy.
def innerJoin[ED2, ED3](other: EdgeRDD[ED2, VD])(f: (VertexId, VertexId,  ED, ED2) => ED3): EdgeRDD[ED3, VD]

在大多數應用中,我們發現,在 EdgeRDD 中的操作是通過圖形運算符來實現,或依靠在基類定義的 RDD 類操作。

優化圖的表示

關於 GraphX 中如何表示分佈式圖結構的詳細描述,這個話題超出了本指南的範圍,一些高層次的理解可能有助於設計可擴展的算法設計以及 API 的最佳利用。GraphX 採用頂點切的方法來分發圖劃分:

不通過邊劃分圖,GraphX 沿頂點來劃分圖,這樣可以減少頂點之間的通信和存儲開銷。邏輯上,這對應於將邊分配到不同的機器,並允許頂點跨越多個機器。分配邊的確切方法取決於PartitionStrategy並有多個權衡各種試探法。用戶可以通過重新分區圖與不同的策略之間進行選擇Graph.partitionBy操作。默認分區策略是按照圖的構造,使用圖中初始的邊。但是,用戶可以方便地切換到二維-分區或GraphX中其他啓發式分區方法。

一旦邊被劃分,並行圖計算的關鍵挑戰在於有效的將每個頂點屬性和邊的屬性連接起來。由於在現實世界中,邊的數量多於頂點的數量,我們把頂點屬性放在邊中。因爲不是所有的分區將包含所有頂點相鄰的邊的信息,我們在內部維護一個路由表,這個表確定在哪裏廣播頂點信息,執行 triplet 和 mapReduceTriplets 的連接操作。

圖算法

GraphX 包括一組圖形算法來簡化分析任務。該算法被包含於org.apache.spark.graphx.lib包中,並可直接通過 GraphOps而被Graph中的方法調用。本節介紹這些算法以及如何使用它們。

PageRank

PageRank記錄了圖中每個頂點的重要性,假設一條邊從u到v,代表從u傳遞給v的重要性。例如,如果一個Twitter用戶有很多粉絲,用戶排名將很高。

GraphX 自帶的PageRank的靜態和動態的實現,放在PageRank對象中。靜態的PageRank運行的固定數量的迭代,而動態的PageRank運行,直到排名收斂(即當每個迭代和上一迭代的差值,在某個範圍之內時停止迭代)。GraphOps允許Graph中的方法直接調用這些算法。

GraphX 還包括,我們可以將PageRank運行在社交網絡數據集中。一組用戶給出graphx/data/users.txt,以及一組用戶之間的關係,給出了graphx/data/followers.txt。我們可以按照如下方法來計算每個用戶的網頁級別:

// Load the edges as a graph
val graph =  GraphLoader.edgeListFile(sc, "graphx/data/followers.txt")
// Run PageRank
val ranks = graph.pageRank(0.0001).vertices
// Join the ranks with the usernames
val users = sc.textFile("graphx/data/users.txt").map { line  =>
    val fields = line.split(",")(
        fields(0).toLong, fields(1)
    )
}
val ranksByUsername = users.join(ranks).map {
    case (id, (username, rank))  => (username, rank)
}
// Print the result
println(ranksByUsername.collect().mkString("\n"))

聯通分量

連接分量算法標出了圖中編號最低的頂點所聯通的子集。例如,在社交網絡中,連接分量類似集羣。GraphX 包含在ConnectedComponents對象的算法,並且我們從該社交網絡數據集中計算出連接組件的PageRank部分,如下所示:

// Load the graph as in the PageRank example
val graph =  GraphLoader.edgeListFile(sc, "graphx/data/followers.txt")
// Find the connected components
val cc = graph.connectedComponents().vertices
// Join the connected components with the usernames
val users = sc.textFile("graphx/data/users.txt").map { line  =>
    val fields = line.split(",")(
        fields(0).toLong, fields(1)
    )
}
val ccByUsername = users.join(cc).map {
    case (id, (username, cc))  => (username, cc)
}
// Print the result
println(ccByUsername.collect().mkString("\n"))

三角計數

當頂點周圍與有一個其他兩個頂點有連線時,這個頂點是三角形的一部分。GraphX在TriangleCount對象實現了一個三角形計數算法,這個算法計算通過各頂點的三角形數目,從而提供集羣的度。我們從PageRank部分計算社交網絡數據集的三角形數量。注意TriangleCount要求邊是規範的指向(srcId < dstId),並使用 Graph.partitionBy來分割圖形。

// Load the edges in canonical order and partition the graph for triangle count
val graph =  GraphLoader.edgeListFile(sc, "graphx/data/followers.txt", true).partitionBy( PartitionStrategy. RandomVertexCut)
// Find the triangle count for each vertex
val triCounts = graph.triangleCount().vertices
// Join the triangle counts with the usernames
val users = sc.textFile("graphx/data/users.txt").map { line => 
        val fields = line.split(",")(
            fields(0).toLong, fields(1)
        )
    }
val triCountByUsername = users.join(triCounts).map {  
        case (id, (username, tc)) => (username, tc)
    }
// Print the result
println(triCountByUsername.collect().mkString("\n"))
  • 示例

假設我想從一些文本文件中構建圖,只考慮圖中重要關係和用戶,在子圖中運行的頁面排名算法,然後終於返回與頂級用戶相關的屬性。我們可以在短短的幾行 GraphX 代碼中實現這一功能:

// Connect to the Spark cluster
val sc =  new  SparkContext("spark://master.amplab.org", "research")
// Load my user data and parse into tuples of user id and attribute list
val users = (sc.textFile("graphx/data/users.txt").map(line  =>  
        line.split(",")).map(  
            parts  =>  (parts.head.toLong,parts.tail) 
        )
    )
// Parse the edge data which is already in userId -> userId format
val  followerGraph  =  GraphLoader.edgeListFile(sc,"graphx/data/followers.txt")
// Attach the user attributes
val graph = followerGraph.outerJoinVertices(users) {
        case (uid, deg,  Some(attrList))  => attrList
        // Some users may not have attributes so we set them as empty
        case (uid, deg,  None)  =>  Array.empty[String]
}
// Restrict the graph to users with usernames and names
val subgraph = graph.subgraph(vpred = (vid, attr)  => attr.size == 2)
// Compute the PageRank
val pagerankGraph = subgraph.pageRank(0.001)
// Get the attributes of the top pagerank users
val  userInfoWithPageRank  =
        subgraph.outerJoinVertices(pagerankGraph.vertices) {
            case (uid, attrList,  Some(pr))  => (pr, attrList.toList)
            case (uid, attrList,  None)  => (0.0, attrList.toList)
        }
println(userInfoWithPageRank.vertices.top(5)( 
    Ordering.by(_ _._2._1)).mkString("\n")
)

猜你喜歡
Hadoop3數據容錯技術(糾刪碼)
Hadoop 數據遷移用法詳解
Flink實時計算topN熱榜
數倉建模分層理論
數倉建模方法論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章