问题导读
1.什么是GraphX? 2.如何将Spark和GraphX引入到项目中? 3.从一个图中取出顶点特征加入到另外一个图中如何实现?
Spark中文手册-编程指南 GraphX编程指南
GraphX是一个新的(alpha)Spark API,它用于图和并行图(graph-parallel)的计算。GraphX通过引入 Resilient Distributed Property Graph:带有 顶点和边属性的有向多重图,来扩展Spark RDD。为了支持图计算,GraphX公开一组基本的功能操作以及Pregel API的一个优化。另外,GraphX包含了一个日益增长的图算法和图builders的 集合,用以简化图分析任务。
graph-parallel系统(如 Giraph和 GraphLab)的发展。 通过限制可表达的计算类型和引入新的技术来划分和分配图,这些系统可以高效地执行复杂的图形算法,比一般的 data-parallel系统快很多。
然而,通过这种限制可以提高性能,但是很难表示典型的图分析途径(构造图、修改它的结构或者表示跨多个图的计算)中很多重要的stages。另外,我们如何看待数据取决于我们的目标,并且同一原始数据可能有许多不同表和图的视图。
graph-parallel和 data- parallel系统`,从而实现大数据的迁移和复制并生成一个复杂的编程模型。
graph-parallel和 data-parallel统一到一个系统中,这个系统拥有一个唯一的组合API。GraphX允许用户将数据当做一个图和一个集合(RDD),而不需要数据移动或者复制。通过将最新的进展整合进 graph-parallel系统,GraphX能够优化图操作的执行。
- 开始
- 属性图
- 图操作符
- Pregel API
- 图构造者
- 顶点和边RDDs
- 图算法
- 例子
开始
开始的第一步是引入Spark和GraphX到你的项目中,如下面所示
1. mport org.apache.spark._
2. import org.apache.spark.graphx._
3. // To make some of the examples work we will also need RDD
4. import org.apache.spark.rdd.RDD
复制代码
如果你没有用到Spark shell,你还将需要SparkContext。
属性图
属性图是一个有向多重图,它带有连接到每个顶点和边的用户定义的对象。 有向多重图中多个并行(parallel)的边共享相同的源和目的地顶点。支持并行边的能力简化了建模场景,这个场景中,相同的顶点存在多种关系(例如co-worker和friend)。每个顶点由一个 唯一的64位长的标识符(VertexID)作为key。GraphX并没有对顶点标识强加任何排序。同样,顶点拥有相应的源和目的顶点标识符。
属性图通过vertex(VD)和edge(ED)类型参数化,这些类型是分别与每个顶点和边相关联的对象的类型。
在某些情况下,在相同的图形中,可能希望顶点拥有不同的属性类型。这可以通过继承完成。例如,将用户和产品建模成一个二分图,我们可以用如下方式
1. class VertexProperty()
2. case class UserProperty(val name: String) extends VertexProperty
3. case class ProductProperty(val name: String, val price: Double) extends VertexProperty
4. // The graph might then have the type:
5. var graph: Graph[VertexProperty, String] = null
复制代码
和RDD一样,属性图是不可变的、分布式的、容错的。图的值或者结构的改变需要按期望的生成一个新的图来实现。注意,原始图的大部分都可以在新图中重用,用来减少这种固有的功能数据结构的成本。 执行者使用一系列顶点分区试探法来对图进行分区。如RDD一样,图中的每个分区可以在发生故障的情况下被重新创建在不同的机器上。
逻辑上的属性图对应于一对类型化的集合(RDD),这个集合编码了每一个顶点和边的属性。因此,图类包含访问图中顶点和边的成员.
1. class Graph[VD, ED] {
2. val vertices: VertexRDD[VD]
3. val edges: EdgeRDD[ED]
4. }
复制代码
VertexRDD[VD]和EdgeRDD[ED]类分别继承和优化自RDD[(VertexID, VD)]和RDD[Edge[ED]]。VertexRDD[VD]和EdgeRDD[ED]都支持额外的功能来建立在图计算和利用内部优化。 属性图的例子 在GraphX项目中,假设我们想构造一个包括不同合作者的属性图。顶点属性可能包含用户名和职业。我们可以用描述合作者之间关系的字符串标注边缘。
所得的图形将具有类型签名
- val userGraph: Graph[(String, String), String]
复制代码
有很多方式从一个原始文件、RDD构造一个属性图。最一般的方法是利用 Graph object。 下面的代码从RDD集合生成属性图。
-
1. // Assume the SparkContext has already been constructed
2. val sc: SparkContext
3. // Create an RDD for the vertices
4. val users: RDD[(VertexId, (String, String))] =
5. sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
6. (5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))
7. // Create an RDD for edges
8. val relationships: RDD[Edge[String]] =
9. sc.parallelize(Array(Edge(3L, 7L, "collab"), Edge(5L, 3L, "advisor"),
10. Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi")))
11. // Define a default user in case there are relationship with missing user
12. val defaultUser = ("John Doe", "Missing")
13. // Build the initial Graph
14. val graph = Graph(users, relationships, defaultUser)
复制代码
在上面的例子中,我们用到了 Edge样本类。边有一个srcId和dstId分别对应 于源和目标顶点的标示符。另外,Edge类有一个attr成员用来存储边属性。
我们可以分别用graph.vertices和graph.edges成员将一个图解构为相应的顶点和边。
1. val graph: Graph[(String, String), String] // Constructed from above
2. // Count all users which are postdocs
3. graph.vertices.filter { case (id, (name, pos)) => pos == "postdoc" }.count
4. // Count all the edges where src > dst
5. graph.edges.filter(e => e.srcId > e.dstId).count
复制代码
注意,graph.vertices返回一个VertexRDD[(String, String)],它继承于 RDD[(VertexID, (String, String))]。所以我们可以用scala的case表达式解构这个元组。另一方面,
graph.edges返回一个包含Edge[String]对象的EdgeRDD。我们也可以用到case类的类型构造器,如下例所示。
1. graph.edges.filter { case Edge(src, dst, prop) => src > dst }.count
复制代码
除了属性图的顶点和边视图,GraphX也包含了一个三元组视图,三元视图逻辑上将顶点和边的属性保存为一个RDD[EdgeTriplet[VD, ED]],它包含EdgeTriplet类的实例。 可以通过下面的Sql表达式表示这个连接。
1. SELECT src.id, dst.id, src.attr, e.attr, dst.attr
2. FROM edges AS e LEFT JOIN vertices AS src, vertices AS dst
3. ON e.srcId = src.Id AND e.dstId = dst.Id
复制代码
除了属性图的顶点和边视图,GraphX也包含了一个三元组视图,三元视图逻辑上将顶点和边的属性保存为一个RDD[EdgeTriplet[VD, ED]],它包含 EdgeTriplet类的实例。 可以通过下面的Sql表达式表示这个连接。
1. SELECT src.id, dst.id, src.attr, e.attr, dst.attr
2. FROM edges AS e LEFT JOIN vertices AS src, vertices AS dst
3. ON e.srcId = src.Id AND e.dstId = dst.Id
复制代码
或者通过下面的图来表示。
EdgeTriplet类继承于Edge类,并且加入了srcAttr和dstAttr成员,这两个成员分别包含源和目的的属性。我们可以用一个三元组视图渲染字符串集合用来描述用户之间的关系。
1. val graph: Graph[(String, String), String] // Constructed from above
2. // Use the triplets view to create an RDD of facts.
3. val facts: RDD[String] =
4. graph.triplets.map(triplet =>
5. triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
6. facts.collect.foreach(println(_))
复制代码
图操作符
正如RDDs有基本的操作map, filter和reduceByKey一样,属性图也有基本的集合操作,这些操作采用用户自定义的函数并产生包含转换特征和结构的新图。定义在 Graph中的 核心操作是经过优化的实现。表示为核心操作的组合的便捷操作定义在 GraphOps中。然而, 因为有Scala的隐式转换,定义在GraphOps中的操作可以作为Graph的成员自动使用。例如,我们可以通过下面的方式计算每个顶点(定义在GraphOps中)的入度。
1. val graph: Graph[(String, String), String]
2. // Use the implicit GraphOps.inDegrees operator
3. val inDegrees: VertexRDD[Int] = graph.inDegrees
复制代码
区分核心图操作和GraphOps的原因是为了在将来支持不同的图表示。每个图表示都必须提供核心操作的实现并重用很多定义在GraphOps中的有用操作。 操作一览 一下是定义在Graph和GraphOps中(为了简单起见,表现为图的成员)的功能的快速浏览。注意,某些函数签名已经简化(如默认参数和类型的限制已删除),一些更高级的功能已经被 删除,所以请参阅API文档了解官方的操作列表。
1. /** Summary of the functionality in the property graph */
2. class Graph[VD, ED] {
3. // Information about the Graph ===================================================================
4. val numEdges: Long
5. val numVertices: Long
6. val inDegrees: VertexRDD[Int]
7. val outDegrees: VertexRDD[Int]
8. val degrees: VertexRDD[Int]
9. // Views of the graph as collections =============================================================
10. val vertices: VertexRDD[VD]
11. val edges: EdgeRDD[ED]
12. val triplets: RDD[EdgeTriplet[VD, ED]]
13. // Functions for caching graphs ==================================================================
14. def persist(newLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, ED]
15. def cache(): Graph[VD, ED]
16. def unpersistVertices(blocking: Boolean = true): Graph[VD, ED]
17. // Change the partitioning heuristic ============================================================
18. def partitionBy(partitionStrategy: PartitionStrategy): Graph[VD, ED]
19. // Transform vertex and edge attributes ==========================================================
20. def mapVertices[VD2](map: (VertexID, VD) => VD2): Graph[VD2, ED]
21. def mapEdges[ED2](map: Edge[ED] => ED2): Graph[VD, ED2]
22. def mapEdges[ED2](map: (PartitionID, Iterator[Edge[ED]]) => Iterator[ED2]): Graph[VD, ED2]
23. def mapTriplets[ED2](map: EdgeTriplet[VD, ED] => ED2): Graph[VD, ED2]
24. def mapTriplets[ED2](map: (PartitionID, Iterator[EdgeTriplet[VD, ED]]) => Iterator[ED2])
25. : Graph[VD, ED2]
26. // Modify the graph structure ====================================================================
27. def reverse: Graph[VD, ED]
28. def subgraph(
29. epred: EdgeTriplet[VD,ED] => Boolean = (x => true),
30. vpred: (VertexID, VD) => Boolean = ((v, d) => true))
31. : Graph[VD, ED]
32. def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
33. def groupEdges(merge: (ED, ED) => ED): Graph[VD, ED]
34. // Join RDDs with the graph ======================================================================
35. def joinVertices[U](table: RDD[(VertexID, U)])(mapFunc: (VertexID, VD, U) => VD): Graph[VD, ED]
36. def outerJoinVertices[U, VD2](other: RDD[(VertexID, U)])
37. (mapFunc: (VertexID, VD, Option[U]) => VD2)
38. : Graph[VD2, ED]
39. // Aggregate information about adjacent triplets =================================================
40. def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexID]]
41. def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[Array[(VertexID, VD)]]
42. def aggregateMessages[Msg: ClassTag](
43. sendMsg: EdgeContext[VD, ED, Msg] => Unit,
44. mergeMsg: (Msg, Msg) => Msg,
45. tripletFields: TripletFields = TripletFields.All)
46. : VertexRDD[A]
47. // Iterative graph-parallel computation ==========================================================
48. def pregel[A](initialMsg: A, maxIterations: Int, activeDirection: EdgeDirection)(
49. vprog: (VertexID, VD, A) => VD,
50. sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexID,A)],
51. mergeMsg: (A, A) => A)
52. : Graph[VD, ED]
53. // Basic graph algorithms ========================================================================
54. def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
55. def connectedComponents(): Graph[VertexID, ED]
56. def triangleCount(): Graph[Int, ED]
57. def stronglyConnectedComponents(numIter: Int): Graph[VertexID, ED]
58. }
复制代码
属性操作 如RDD的map操作一样,属性图包含下面的操作:
1. class Graph[VD, ED] {
2. def mapVertices[VD2](map: (VertexId, VD) => VD2): Graph[VD2, ED]
3. def mapEdges[ED2](map: Edge[ED] => ED2): Graph[VD, ED2]
4. def mapTriplets[ED2](map: EdgeTriplet[VD, ED] => ED2): Graph[VD, ED2]
5. }
复制代码
每个操作都产生一个新的图,这个新的图包含通过用户自定义的map操作修改后的顶点或边的属性。
注意,每种情况下图结构都不受影响。这些操作的一个重要特征是它允许所得图形重用原有图形的结构索引(indices)。下面的两行代码在逻辑上是等价的,但是第一个不保存结构索引,所以 不会从GraphX系统优化中受益。
1. val newVertices = graph.vertices.map { case (id, attr) => (id, mapUdf(id, attr)) }
2. val newGraph = Graph(newVertices, graph.edges)
复制代码
另一种方法是用[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@mapVertices[VD2]((VertexId,VD]mapVertices[/url]⇒VD2)(ClassTag[VD2]):Graph[VD2,ED])保存索引。
1. val newGraph = graph.mapVertices((id, attr) => mapUdf(id, attr))
复制代码
这些操作经常用来初始化的图形,用作特定计算或者用来处理项目不需要的属性。例如,给定一个图,这个图的顶点特征包含出度,我们为PageRank初始化它。
1. // Given a graph where the vertex property is the out degree
2. val inputGraph: Graph[Int, String] =
3. graph.outerJoinVertices(graph.outDegrees)((vid, _, degOpt) => degOpt.getOrElse(0))
4. // Construct a graph where each edge contains the weight
5. // and each vertex is the initial PageRank
6. val outputGraph: Graph[Double, Double] =
7. inputGraph.mapTriplets(triplet => 1.0 / triplet.srcAttr).mapVertices((id, _) => 1.0)
复制代码
结构性操作
当前的GraphX仅仅支持一组简单的常用结构性操作。下面是基本的结构性操作列表。
1. class Graph[VD, ED] {
2. def reverse: Graph[VD, ED]
3. def subgraph(epred: EdgeTriplet[VD,ED] => Boolean,
4. vpred: (VertexId, VD) => Boolean): Graph[VD, ED]
5. def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
6. def groupEdges(merge: (ED, ED) => ED): Graph[VD,ED]
7. }
复制代码
[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@reverse:Graph[VD,ED]]reverse[/url]操作返回一个新的图,这个图的边的方向都是反转的。例如,这个操作可以用来计算反转的PageRank。因为反转操作没有修改顶点或者边的属性或者改变边的数量,所以我们可以 在不移动或者复制数据的情况下有效地实现它。
[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@subgraph((EdgeTriplet[VD,ED]]subgraph[/url]⇒Boolean,(VertexId,VD)⇒Boolean):Graph[VD,ED])操作 利用顶点和边的谓词(predicates),返回的图仅仅包含满足顶点谓词的顶点、满足边谓词的边以及满足顶点谓词的连接顶点(connect vertices)。subgraph操作可以用于很多场景,如获取 感兴趣的顶点和边组成的图或者获取清除断开链接后的图。下面的例子删除了断开的链接。
1. // Create an RDD for the vertices
2. val users: RDD[(VertexId, (String, String))] =
3. sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
4. (5L, ("franklin", "prof")), (2L, ("istoica", "prof")),
5. (4L, ("peter", "student"))))
6. // Create an RDD for edges
7. val relationships: RDD[Edge[String]] =
8. sc.parallelize(Array(Edge(3L, 7L, "collab"), Edge(5L, 3L, "advisor"),
9. Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi"),
10. Edge(4L, 0L, "student"), Edge(5L, 0L, "colleague")))
11. // Define a default user in case there are relationship with missing user
12. val defaultUser = ("John Doe", "Missing")
13. // Build the initial Graph
14. val graph = Graph(users, relationships, defaultUser)
15. // Notice that there is a user 0 (for which we have no information) connected to users
16. // 4 (peter) and 5 (franklin).
17. graph.triplets.map(
18. triplet => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
19. ).collect.foreach(println(_))
20. // Remove missing vertices as well as the edges to connected to them
21. val validGraph = graph.subgraph(vpred = (id, attr) => attr._2 != "Missing")
22. // The valid subgraph will disconnect users 4 and 5 by removing user 0
23. validGraph.vertices.collect.foreach(println(_))
24. validGraph.triplets.map(
25. triplet => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
26. ).collect.foreach(println(_))
复制代码
注意,上面的例子中,仅仅提供了顶点谓词。如果没有提供顶点或者边的谓词,subgraph操作默认为true。
[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@mask[VD2,ED2](Graph[VD2,ED2]]mask[/url](ClassTag[VD2],ClassTag[ED2]):Graph[VD,ED])操作 构造一个子图,这个子图包含输入图中包含的顶点和边。这个操作可以和subgraph操作相结合,基于另外一个相关图的特征去约束一个图。例如,我们可能利用缺失顶点的图运行连通体(?连通组件connected components),然后返回有效的子图。
1. / Run Connected Components
2. val ccGraph = graph.connectedComponents() // No longer contains missing field
3. // Remove missing vertices as well as the edges to connected to them
4. val validGraph = graph.subgraph(vpred = (id, attr) => attr._2 != "Missing")
5. // Restrict the answer to the valid subgraph
6. val validCCGraph = ccGraph.mask(validGraph)
复制代码
groupEdges⇒ED):Graph[VD,ED])操作合并多重图 中的并行边(如顶点对之间重复的边)。在大量的应用程序中,并行的边可以合并(它们的权重合并)为一条边从而降低图的大小。 连接操作 在许多情况下,有必要将外部数据加入到图中。例如,我们可能有额外的用户属性需要合并到已有的图中或者我们可能想从一个图中取出顶点特征加入到另外一个图中。这些任务可以用join操作完成。 下面列出的是主要的join操作。
1. class Graph[VD, ED] {
2. def joinVertices[U](table: RDD[(VertexId, U)])(map: (VertexId, VD, U) => VD)
3. : Graph[VD, ED]
4. def outerJoinVertices[U, VD2](table: RDD[(VertexId, U)])(map: (VertexId, VD, Option[U]) => VD2)
5. : Graph[VD2, ED]
6. }
复制代码
[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.GraphOps@joinVertices[U](RDD[(VertexId,U]joinVertices[/url]])((VertexId,VD,U)⇒VD)(ClassTag[U]):Graph[VD,ED]) 操作将输入RDD和顶点相结合,返回一个新的带有顶点特征的图。这些特征是通过在连接顶点的结果上使用用户定义的map函数获得的。在RDD中没有匹配值的顶点保留其原始值。
注意,对于给定的顶点,如果RDD中有超过1个的匹配值,则仅仅使用其中的一个。建议用下面的方法保证输入RDD的唯一性。下面的方法也会预索引返回的值用以加快后续的join操作。
1. val nonUniqueCosts: RDD[(VertexID, Double)]
2. val uniqueCosts: VertexRDD[Double] =
3. graph.vertices.aggregateUsingIndex(nonUnique, (a,b) => a + b)
4. val joinedGraph = graph.joinVertices(uniqueCosts)(
5. (id, oldCost, extraCost) => oldCost + extraCost)
复制代码
除了将用户自定义的map函数用到所有顶点和改变顶点属性类型以外,更一般的[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@outerJoinVertices[U,VD2](RDD[(VertexId,U]outerJoinVertices[/url]])((VertexId,VD,Option[U])⇒VD2)(ClassTag[U],ClassTag[VD2]):Graph[VD2,ED])与joinVertices类似。 因为并不是所有顶点在RDD中拥有匹配的值,map函数需要一个option类型。
1. val outDegrees: VertexRDD[Int] = graph.outDegrees
2. val degreeGraph = graph.outerJoinVertices(outDegrees) { (id, oldAttr, outDegOpt) =>
3. outDegOpt match {
4. case Some(outDeg) => outDeg
5. case None => 0 // No outDegree means zero outDegree
6. }
7. }
复制代码
你可能已经注意到了,在上面的例子中用到了curry函数的多参数列表。虽然我们可以将f(a)(b)写成f(a,b),但是f(a,b)意味着b的类型推断将不会依赖于a。因此,用户需要为定义 的函数提供类型标注。
1. val joinedGraph = graph.joinVertices(uniqueCosts,
2. (id: VertexID, oldCost: Double, extraCost: Double) => oldCost + extraCost)
复制代码
相邻聚合(Neighborhood Aggregation) 图分析任务的一个关键步骤是汇总每个顶点附近的信息。例如我们可能想知道每个用户的追随者的数量或者每个用户的追随者的平均年龄。许多迭代图算法(如PageRank,最短路径和连通体) 多次聚合相邻顶点的属性。
为了提高性能,主要的聚合操作从graph.mapReduceTriplets改为了新的graph.AggregateMessages。虽然API的改变相对较小,但是我们仍然提供了过渡的指南。 聚合消息(aggregateMessages) GraphX中的核心聚合操作是[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@aggregateMessages[A]((EdgeContext[VD,ED,A]]aggregateMessages[/url]⇒Unit,(A,A)⇒A,TripletFields)(ClassTag[A]):VertexRDD[A])。 这个操作将用户定义的sendMsg函数应用到图的每个边三元组(edge triplet),然后应用mergeMsg函数在其目的顶点聚合这些消息。
1. class Graph[VD, ED] {
2. def aggregateMessages[Msg: ClassTag](
3. sendMsg: EdgeContext[VD, ED, Msg] => Unit,
4. mergeMsg: (Msg, Msg) => Msg,
5. tripletFields: TripletFields = TripletFields.All)
6. : VertexRDD[Msg]
7. }
复制代码
用户自定义的sendMsg函数是一个 EdgeContext类型。它暴露源和目的属性以及边缘属性 以及发送消息给源和目的属性的函数( sendToSrc:Unit)和 sendToDst:Unit))。 可将sendMsg函数看做map-reduce过程中的map函数。用户自定义的mergeMsg函数指定两个消息到相同的顶点并保存为一个消息。可以将mergeMsg函数看做map-reduce过程中的reduce函数。 [url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@aggregateMessages[A]((EdgeContext[VD,ED,A]]aggregateMessages[/url]⇒Unit,(A,A)⇒A,TripletFields)(ClassTag[A]):VertexRDD[A]) 操作返回一个包含聚合消息(目的地为每个顶点)的VertexRDD[Msg]。没有接收到消息的顶点不包含在返回的VertexRDD中。
另外,[url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@aggregateMessages[A]((EdgeContext[VD,ED,A]]aggregateMessages[/url]⇒Unit,(A,A)⇒A,TripletFields)(ClassTag[A]):VertexRDD[A]) 有一个可选的tripletFields参数,它指出在EdgeContext中,哪些数据被访问(如源顶点特征而不是目的顶点特征)。tripletsFields可能的选项定义在 TripletFields中。 tripletFields参数用来通知GraphX仅仅只需要EdgeContext的一部分允许GraphX选择一个优化的连接策略。例如,如果我们想计算每个用户的追随者的平均年龄,我们仅仅只需要源字段。 所以我们用TripletFields.Src表示我们仅仅只需要源字段。
在下面的例子中,我们用aggregateMessages操作计算每个用户更年长的追随者的年龄。
1. // Import random graph generation library
2. import org.apache.spark.graphx.util.GraphGenerators
3. // Create a graph with "age" as the vertex property. Here we use a random graph for simplicity.
4. val graph: Graph[Double, Int] =
5. GraphGenerators.logNormalGraph(sc, numVertices = 100).mapVertices( (id, _) => id.toDouble )
6. // Compute the number of older followers and their total age
7. val olderFollowers: VertexRDD[(Int, Double)] = graph.aggregateMessages[(Int, Double)](
8. triplet => { // Map Function
9. if (triplet.srcAttr > triplet.dstAttr) {
10. // Send message to destination vertex containing counter and age
11. triplet.sendToDst(1, triplet.srcAttr)
12. }
13. },
14. // Add counter and age
15. (a, b) => (a._1 + b._1, a._2 + b._2) // Reduce Function
16. )
17. // Divide total age by number of older followers to get average age of older followers
18. val avgAgeOfOlderFollowers: VertexRDD[Double] =
19. olderFollowers.mapValues( (id, value) => value match { case (count, totalAge) => totalAge / count } )
20. // Display the results
21. avgAgeOfOlderFollowers.collect.foreach(println(_))
复制代码
当消息(以及消息的总数)是常量大小(列表和连接替换为浮点数和添加)时,aggregateMessages操作的效果最好。 Map Reduce三元组过渡指南 在之前版本的GraphX中,利用[mapReduceTriplets]操作完成相邻聚合。
1. class Graph[VD, ED] {
2. def mapReduceTriplets[Msg](
3. map: EdgeTriplet[VD, ED] => Iterator[(VertexId, Msg)],
4. reduce: (Msg, Msg) => Msg)
5. : VertexRDD[Msg]
6. }
复制代码
mapReduceTriplets操作在每个三元组上应用用户定义的map函数,然后保存用用户定义的reduce函数聚合的消息。然而,我们发现用户返回的迭代器是昂贵的,它抑制了我们添加额外优化(例如本地顶点的重新编号)的能力。 [url=http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.graphx.Graph@aggregateMessages[A]((EdgeContext[VD,ED,A]]aggregateMessages[/url]⇒Unit,(A,A)⇒A,TripletFields)(ClassTag[A]):VertexRDD[A]) 暴露三元组字段和函数显示的发送消息到源和目的顶点。并且,我们删除了字节码检测转而需要用户指明三元组的哪些字段实际需要。
下面的代码用到了mapReduceTriplets
1. val graph: Graph[Int, Float] = ...
2. def msgFun(triplet: Triplet[Int, Float]): Iterator[(Int, String)] = {
3. Iterator((triplet.dstId, "Hi"))
4. }
5. def reduceFun(a: Int, b: Int): Int = a + b
6. val result = graph.mapReduceTriplets[String](msgFun, reduceFun)
复制代码
下面的代码用到了aggregateMessages
1. val graph: Graph[Int, Float] = ...
2. def msgFun(triplet: EdgeContext[Int, Float, String]) {
3. triplet.sendToDst("Hi")
4. }
5. def reduceFun(a: Int, b: Int): Int = a + b
6. val result = graph.aggregateMessages[String](msgFun, reduceFun)
复制代码
计算度信息 最一般的聚合任务就是计算顶点的度,即每个顶点相邻边的数量。在有向图中,经常需要知道顶点的入度、出度以及总共的度。 GraphOps 类包含一个操作集合用来计算每个顶点的度。例如,下面的例子计算最大的入度、出度和总度。
1. // Define a reduce operation to compute the highest degree vertex
2. def max(a: (VertexId, Int), b: (VertexId, Int)): (VertexId, Int) = {
3. if (a._2 > b._2) a else b
4. }
5. // Compute the max degrees
6. val maxInDegree: (VertexId, Int) = graph.inDegrees.reduce(max)
7. val maxOutDegree: (VertexId, Int) = graph.outDegrees.reduce(max)
8. val maxDegrees: (VertexId, Int) = graph.degrees.reduce(max)
复制代码
Collecting Neighbors 在某些情况下,通过收集每个顶点相邻的顶点及它们的属性来表达计算可能更容易。这可以通过 collectNeighborIds:VertexRDD[Array[VertexId]]) 和 collectNeighbors:VertexRDD[Array[(VertexId,VD)]])操作来简单的完成
1. class GraphOps[VD, ED] {
2. def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexId]]
3. def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[ Array[(VertexId, VD)] ]
4. }
复制代码
这些操作是非常昂贵的,因为它们需要重复的信息和大量的通信。如果可能,尽量用aggregateMessages操作直接表达相同的计算。 缓存和不缓存 在Spark中,RDDs默认是不缓存的。为了避免重复计算,当需要多次利用它们时,我们必须显示地缓存它们。GraphX中的图也有相同的方式。当利用到图多次时,确保首先访问Graph.cache()方法。
在迭代计算中,为了获得最佳的性能,不缓存可能是必须的。默认情况下,缓存的RDDs和图会一直保留在内存中直到因为内存压力迫使它们以LRU的顺序删除。对于迭代计算,先前的迭代的中间结果将填充到缓存 中。虽然它们最终会被删除,但是保存在内存中的不需要的数据将会减慢垃圾回收。只有中间结果不需要,不缓存它们是更高效的。这涉及到在每次迭代中物化一个图或者RDD而不缓存所有其它的数据集。 在将来的迭代中仅用物化的数据集。然而,因为图是由多个RDD组成的,正确的不持久化它们是困难的。对于迭代计算,我们建议使用Pregel API,它可以正确的不持久化中间结果。 |