Graphx 原始碼剖析-圖的生成

dav2100發表於2021-09-09

Graphx的實現程式碼並不多,這得益於Spark RDD niubility的設計。眾所周知,在分散式上做圖計算需要考慮點、邊的切割。而RDD本身是一個分散式的資料集,所以,做Graphx只需要把邊和點用RDD表示出來就可以了。本文就是從這個角度來分析Graphx的運作基本原理(本文基於Spark2.0)。

分散式圖的切割方式

在單機上圖很好表示,在分散式環境下,就涉及到一個問題:圖如何切分,以及切分之後的不同子圖如何保持彼此的聯絡構成一個完整的圖。圖的切分方式有兩種:點切分和邊切分。在Graphx中,採用點切分。

在GraphX中,Graph類除了表示點的VertexRDD和表示邊的EdgeRDD外,還有一個將點的屬性和邊的屬性都包含在內的RDD[EdgeTriplet]
方便起見,我們先從GraphLoader中來看看如何從一個用邊來描述圖的檔案中如何構建Graph的。

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] =
  {    // Parse the edge data table directly into edge partitions
    val lines = ... ...
    val edges = lines.mapPartitionsWithIndex { (pid, iter) =>
      ... ...
      Iterator((pid, builder.toEdgePartition))
    }.persist(edgeStorageLevel).setName("GraphLoader.edgeListFile - edges (%s)".format(path))
    edges.count()

    GraphImpl.fromEdgePartitions(edges, defaultVertexAttr = 1, edgeStorageLevel = edgeStorageLevel,
      vertexStorageLevel = vertexStorageLevel)
  } // end of edgeListFile

從上面精簡的程式碼中可以看出來,先得到lines一個表示邊的RDD(這裡所謂的邊依舊是文字描述的),然後再經過一系列的轉換來生成Graph。

EdgeRDD

GraphImpl.fromEdgePartitions中傳入的第一個引數edgesEdgeRDDEdgePartition。先來看看EdgePartition究竟為何物。

class EdgePartition[
    @specialized(Char, Int, Boolean, Byte, Long, Float, Double) ED: ClassTag, VD: ClassTag](    localSrcIds: Array[Int],    localDstIds: Array[Int],    data: Array[ED],    index: GraphXPrimitiveKeyOpenHashMap[VertexId, Int],    global2local: GraphXPrimitiveKeyOpenHashMap[VertexId, Int],    local2global: Array[VertexId],    vertexAttrs: Array[VD],    activeSet: Option[VertexSet])
  extends Serializable {

其中:
localSrcIds 為本地邊的源點的本地編號。
localDstIds 為本地邊的目的點的本地編號,與localSrcIds一一對應成邊的兩個點。
data 為邊的屬性值。
index 為本地邊的源點全域性ID到localSrcIds中下標的對映。
global2local 為點的全域性ID到本地ID的對映。
local2global 是一個Vector,依次儲存了本地出現的點,包括跨節點的點。
透過這樣的方式做到了點切割。
有了EdgePartition之後,再透過得到EdgeRDD就容易了。

VertexRDD

現在看fromEdgePartitions

  def fromEdgePartitions[VD: ClassTag, ED: ClassTag](      edgePartitions: RDD[(PartitionID, EdgePartition[ED, VD])],      defaultVertexAttr: VD,      edgeStorageLevel: StorageLevel,      vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = {
    fromEdgeRDD(EdgeRDD.fromEdgePartitions(edgePartitions), defaultVertexAttr, edgeStorageLevel,
      vertexStorageLevel)
  }

fromEdgePartitions 中呼叫了 fromEdgeRDD

  private def fromEdgeRDD[VD: ClassTag, ED: ClassTag](      edges: EdgeRDDImpl[ED, VD],      defaultVertexAttr: VD,      edgeStorageLevel: StorageLevel,      vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = {
    val edgesCached = edges.withTargetStorageLevel(edgeStorageLevel).cache()
    val vertices =
      VertexRDD.fromEdges(edgesCached, edgesCached.partitions.length, defaultVertexAttr)
      .withTargetStorageLevel(vertexStorageLevel)
    fromExistingRDDs(vertices, edgesCached)
  }

可見,VertexRDD是由EdgeRDD生成的。接下來講解怎麼從EdgeRDD生成VertexRDD

def fromEdges[VD: ClassTag](
      edges: EdgeRDD[_], numPartitions: Int, defaultVal: VD): VertexRDD[VD] = {
    val routingTables = createRoutingTables(edges, new HashPartitioner(numPartitions))
    val vertexPartitions = routingTables.mapPartitions({ routingTableIter =>
      val routingTable =        if (routingTableIter.hasNext) routingTableIter.next() else RoutingTablePartition.empty
      Iterator(ShippableVertexPartition(Iterator.empty, routingTable, defaultVal))
    }, preservesPartitioning = true)    new VertexRDDImpl(vertexPartitions)
  }  private[graphx] def createRoutingTables(
      edges: EdgeRDD[_], vertexPartitioner: Partitioner): RDD[RoutingTablePartition] = {    // Determine which vertices each edge partition needs by creating a mapping from vid to pid.
    val vid2pid = edges.partitionsRDD.mapPartitions(_.flatMap(      Function.tupled(RoutingTablePartition.edgePartitionToMsgs)))
      .setName("VertexRDD.createRoutingTables - vid2pid (aggregation)")

    val numEdgePartitions = edges.partitions.length
    vid2pid.partitionBy(vertexPartitioner).mapPartitions(
      iter => Iterator(RoutingTablePartition.fromMsgs(numEdgePartitions, iter)),
      preservesPartitioning = true)
  }

從程式碼中可以看到先建立了一個路由表,這個路由表的本質依舊是RDD,然後透過路由表的轉得到RDD[ShippableVertexPartition],最後再構造出VertexRDD。先講解一下路由表,每一條邊都有兩個點,一個源點,一個終點。在構造路由表時,源點標記位或1,目標點標記位或2,並結合邊的partitionID編碼成一個Int(高2位表示源點終點,低30位表示邊的partitionID)。再根據這個編碼的Int反解出ShippableVertexPartition。值得注意的是,在createRoutingTables中,反解生成ShippableVertexPartition過程中根據點的id hash值partition了一次,這樣,相同的點都在一個分割槽了。有意思的地方來了:我以為這樣之後就會把點和這個點的映象合成一個,然而實際上並沒有。點和邊是相互關聯的,透過邊生成點,透過點能找到邊,如果合併了點和點的映象,那也找不到某些邊了。ShippableVertexPartition依舊以邊的區分為標準,並記錄了點的屬性值,源點、終點資訊,這樣邊和邊的點,都在一個分割槽上。
最終,透過new VertexRDDImpl(vertexPartitions)生成VertexRDD

Graph

 def fromExistingRDDs[VD: ClassTag, ED: ClassTag](      vertices: VertexRDD[VD],      edges: EdgeRDD[ED]): GraphImpl[VD, ED] = {
    new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))
  }

fromExistingRDDs呼叫new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))來生成圖。

class ReplicatedVertexView[VD: ClassTag, ED: ClassTag](
    var edges: EdgeRDDImpl[ED, VD],
    var hasSrcId: Boolean = false,
    var hasDstId: Boolean = false)

ReplicatedVertexView是邊和圖的檢視,當點的屬性發生改變時,將改變傳輸到對應的邊。



作者:AlbertCheng
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4479/viewspace-2818581/,如需轉載,請註明出處,否則將追究法律責任。

相關文章