Graphx 源碼剖析-圖的生成

Graphx的實(shí)現(xiàn)代碼并不多,這得益于Spark RDD niubility的設(shè)計(jì)。眾所周知,在分布式上做圖計(jì)算需要考慮點(diǎn)、邊的切割。而RDD本身是一個(gè)分布式的數(shù)據(jù)集,所以,做Graphx只需要把邊和點(diǎn)用RDD表示出來就可以了。本文就是從這個(gè)角度來分析Graphx的運(yùn)作基本原理(本文基于Spark2.0)。

分布式圖的切割方式

在單機(jī)上圖很好表示,在分布式環(huán)境下,就涉及到一個(gè)問題:圖如何切分,以及切分之后的不同子圖如何保持彼此的聯(lián)系構(gòu)成一個(gè)完整的圖。圖的切分方式有兩種:點(diǎn)切分和邊切分。在Graphx中,采用點(diǎn)切分。

在GraphX中,Graph類除了表示點(diǎn)的VertexRDD和表示邊的EdgeRDD外,還有一個(gè)將點(diǎn)的屬性和邊的屬性都包含在內(nèi)的RDD[EdgeTriplet]
方便起見,我們先從GraphLoader中來看看如何從一個(gè)用邊來描述圖的文件中如何構(gòu)建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一個(gè)表示邊的RDD(這里所謂的邊依舊是文本描述的),然后再經(jīng)過一系列的轉(zhuǎn)換來生成Graph。

EdgeRDD

GraphImpl.fromEdgePartitions中傳入的第一個(gè)參數(shù)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 為本地邊的源點(diǎn)的本地編號。
localDstIds 為本地邊的目的點(diǎn)的本地編號,與localSrcIds一一對應(yīng)成邊的兩個(gè)點(diǎn)。
data 為邊的屬性值。
index 為本地邊的源點(diǎn)全局ID到localSrcIds中下標(biāo)的映射。
global2local 為點(diǎn)的全局ID到本地ID的映射。
local2global 是一個(gè)Vector,依次存儲了本地出現(xiàn)的點(diǎn),包括跨節(jié)點(diǎn)的點(diǎn)。
通過這樣的方式做到了點(diǎn)切割。
有了EdgePartition之后,再通過得到EdgeRDD就容易了。

VertexRDD

現(xiàn)在看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 中調(diào)用了 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)
  }

從代碼中可以看到先創(chuàng)建了一個(gè)路由表,這個(gè)路由表的本質(zhì)依舊是RDD,然后通過路由表的轉(zhuǎn)得到RDD[ShippableVertexPartition],最后再構(gòu)造出VertexRDD。先講解一下路由表,每一條邊都有兩個(gè)點(diǎn),一個(gè)源點(diǎn),一個(gè)終點(diǎn)。在構(gòu)造路由表時(shí),源點(diǎn)標(biāo)記位或1,目標(biāo)點(diǎn)標(biāo)記位或2,并結(jié)合邊的partitionID編碼成一個(gè)Int(高2位表示源點(diǎn)終點(diǎn),低30位表示邊的partitionID)。再根據(jù)這個(gè)編碼的Int反解出ShippableVertexPartition。值得注意的是,在createRoutingTables中,反解生成ShippableVertexPartition過程中根據(jù)點(diǎn)的id hash值partition了一次,這樣,相同的點(diǎn)都在一個(gè)分區(qū)了。有意思的地方來了:我以為這樣之后就會把點(diǎn)和這個(gè)點(diǎn)的鏡像合成一個(gè),然而實(shí)際上并沒有。點(diǎn)和邊是相互關(guān)聯(lián)的,通過邊生成點(diǎn),通過點(diǎn)能找到邊,如果合并了點(diǎn)和點(diǎn)的鏡像,那也找不到某些邊了。ShippableVertexPartition依舊以邊的區(qū)分為標(biāo)準(zhǔn),并記錄了點(diǎn)的屬性值,源點(diǎn)、終點(diǎn)信息,這樣邊和邊的點(diǎn),都在一個(gè)分區(qū)上。
最終,通過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調(diào)用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是邊和圖的視圖,當(dāng)點(diǎn)的屬性發(fā)生改變時(shí),將改變傳輸?shù)綄?yīng)的邊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容