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ù)edges
為EdgeRDD
的EdgePartition
。先來看看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)的邊。