Spark与Scala:基于阈值并行分组VertexRDD顶点
并行化你的Spark顶点分类逻辑
嘿,我来帮你把这段串行代码改成Spark原生的并行实现——毕竟用Spark就是要 leverage 它的分布式计算能力,别把所有数据都拉到Driver端串行跑嘛!先说说原来代码的问题:你用take把整个RDD的数据拉到本地数组,然后循环遍历,这在数据量小的时候没问题,但数据量大的话,Driver内存会爆,而且完全浪费了Spark的集群资源。
先修正类型冗余(可选但推荐)
原来的代码里有多余的toString().toLong转换,我们可以先把顶点属性的类型固定为Long,让代码更简洁:
// 修正outerJoin后的顶点属性类型为Long,避免后续类型转换 val fixedGraph: Graph[Long, Any] = usersGraph.outerJoinVertices(userNodesOutDegreesRdd)( (vid, _, outDegOpt) => outDegOpt.map(_.toLong).getOrElse(0L) ) val completeUserNodesOutDegreesRdd: VertexRDD[Long] = fixedGraph.vertices
并行实现方案
阈值计算部分可以保留(因为是在Driver端计算的常量,会自动广播到Executor),接下来分两种并行实现方式:
方式一:直接用Filter生成四个RDD
这种方式简单直接,每个filter操作都是分布式并行执行的:
// 计算阈值(和原来逻辑一致) val maxNumOutDegreeEdgesPerNode = numUserGraphNodes - 1 val soglia25 = maxNumOutDegreeEdgesPerNode * 0.25 val soglia50 = maxNumOutDegreeEdgesPerNode * 0.5 val soglia75 = maxNumOutDegreeEdgesPerNode * 0.75 val soglia100 = maxNumOutDegreeEdgesPerNode.toDouble // 并行过滤每个阈值对应的顶点,所有操作在集群节点上执行 val lowSAUsersRdd = completeUserNodesOutDegreesRdd.filter { case (vid, outDeg) => outDeg <= soglia25 } val mediumLowSAUsersRdd = completeUserNodesOutDegreesRdd.filter { case (vid, outDeg) => outDeg > soglia25 && outDeg <= soglia50 } val mediumHighSAUsersRdd = completeUserNodesOutDegreesRdd.filter { case (vid, outDeg) => outDeg > soglia50 && outDeg <= soglia75 } val highSAUsersRdd = completeUserNodesOutDegreesRdd.filter { case (vid, outDeg) => outDeg > soglia75 && outDeg <= soglia100 } // 如果需要把结果收集到Driver端(比如本地后续处理),再用collect()(大数据量谨慎使用) val lowSAUsers = lowSAUsersRdd.collect().toListBuffer val mediumLowSAUsers = mediumLowSAUsersRdd.collect().toListBuffer val mediumHighSAUsers = mediumHighSAUsersRdd.collect().toListBuffer val highSAUsers = highSAUsersRdd.collect().toListBuffer // 组装集群容器(和原来逻辑一致) val clustersContainer = ListBuffer( "lowSAUsers" -> lowSAUsers, "mediumLowSAUsers" -> mediumLowSAUsers, "mediumHighSAUsers" -> mediumHighSAUsers, "highSAUsers" -> highSAUsers )
方式二:先标记集群再分组(更高效,仅扫描一次RDD)
如果数据量很大,多次扫描RDD会浪费资源,我们可以先给每个顶点标记所属集群,再一次性分组:
// 计算阈值(同上) val maxNumOutDegreeEdgesPerNode = numUserGraphNodes - 1 val soglia25 = maxNumOutDegreeEdgesPerNode * 0.25 val soglia50 = maxNumOutDegreeEdgesPerNode * 0.5 val soglia75 = maxNumOutDegreeEdgesPerNode * 0.75 val soglia100 = maxNumOutDegreeEdgesPerNode.toDouble // 给每个顶点标记集群名称,仅扫描一次RDD val labeledVertices = completeUserNodesOutDegreesRdd.map { case (vid, outDeg) => val cluster = outDeg match { case d if d <= soglia25 => "lowSAUsers" case d if d <= soglia50 => "mediumLowSAUsers" case d if d <= soglia75 => "mediumHighSAUsers" case d if d <= soglia100 => "highSAUsers" case _ => "unknown" // 理论上不会触发 } (cluster, (vid, outDeg)) } // 按集群分组,得到每个集群的顶点集合RDD val groupedClusters = labeledVertices.groupByKey() // 收集到Driver端(大数据量谨慎)并组装成容器 val clustersContainer = groupedClusters.collect().map { case (clusterName, vertices) => (clusterName, vertices.toListBuffer) }.toListBuffer
关键优势
- 真正并行: 所有过滤/分组操作都在Spark集群的多个节点上同时执行,充分利用集群资源,速度比串行快几个数量级。
- 避免Driver内存溢出: 不需要把整个RDD的数据拉到Driver端,只有当你主动调用
collect()时才会拉取(非必要可以不用)。 - 代码更简洁: 去掉了冗余的类型转换和循环逻辑,用Spark原生API实现,更符合Spark的编程范式。
内容的提问来源于stack exchange,提问作者Fobi
相关产品推荐
相关产品推荐

