Spark优化那些事(2)-graphx实现介数估算踩坑总结
来源:互联网 发布:淘宝卖家信用贷款 编辑:程序博客网 时间:2024/06/03 18:42
背景
最近一段时间在使用spark graphx实现介数估算。介数(betweenness)可分为点介数和边介数,在图中衡量一个点或边的重要程度,后面的讨论均是针对点介数,边介数实现方式类似。这个指标虽然好,但是计算开销非常之大,如果没有优化,纯粹按照定义计算,复杂度为O(n3)O(n3)(n是节点数),定义如下:
其中σstσst是s,t之间最短路径的数量,σst(v)σst(v)是s,t最短路径中包含节点v的数量。
根据 Brandes的快速介数计算 ,可以将复杂度降到O((m+n)n)O((m+n)n)(m,n分别是边数和节点数)。但是,即使是spark,面对此计算量计算上百万节点的图仍然不太现实。所以,只能采取折中的方法,也就是估算每个点的介数。最后,仍然是采用 Brandes的介数估算算法 ,才使复杂度降到可接受的O(m+n)O(m+n)。虽然牺牲了部分精度,但是在样本达到一定数据量时,误差可以降到可接受的范围。
本文主要介绍实现该算法时,遇到的性能问题以及解决方法,不会涉及过多算法细节,需要详细了解的读者可以参考文章最后的 参考资料 中的相关论文。
算法框架
首先需要从精确计算介数谈起,首先定义一个变量,称之为dependency,简单来讲,在原始定义中,将源点s固定,v和t变化,形式的定义如下:
Brandes的快速介数计算算法中,每计算一轮会将所有v针对固定s的dependency计算出来,计算n轮,然后针对每个v,合并所有dependency,就可以计算每个点介数值。每一轮的计算量包括:一个单源最短路径,一个前置节点搜集,一个依赖合并过程,需要三个pregel过程和一个vertexLeftJoin过程,每一轮的复杂度为O(m+n)O(m+n)。介数估算的方法是随机抽取k节点,计算对应的dependency,然后计算每个v的平均dependency并乘以n,得到最终估算结果,理论依据是 霍夫丁不等式 (有届随机变量样本均值与均值期望的误差可以控制在一定范围内)。由于k≪mk≪m,所以最终复杂度不变。以上就是计算介数估算的大体框架,具体细节请参考 Brades介数估算论文 ,里面有估算的误差估计证明以及实验效果。
性能瓶颈
算法过程虽然不复杂,但是在实现时却遇到了性能瓶颈。经过排查,每一轮dependency计算开销相对稳定,主要不稳定的开销在最后按点聚合dependency的过程中。在百万点,千万边的无标度图中,该过程有时候只需要 几分钟 ,但是大多数时候却需要 几小时甚至更多 !所以,主要的优化点就是如何提高聚合这块的性能。此过程经过了几轮改造,最后将此步骤的耗时稳定在几分钟,下面分享此过程的演进过程。
版本1:逐步聚合 vs 合并后聚合
聚合的第一个版本,可以用下面简化代码演示,
// 计算依赖def dependency(id:Int, graph:Graph[Double, Double]):RDD[(Long, Double)] = { ... } val data:Graph[Double, Double] = ... // loading from hdfs// 随机抽样val idList = data.vertices.takeSample(10)// 方案1:全部聚合,然后统一reduceByKey,只有一次shuffleval rst1 = idList.map(id => dependency(id, data)) .reduce(_ union _).reduceByKey(_+_) // 方案2:每一步都聚合,减少内存使用,但有多次shuffleval rst2 = idList.map(id => dependency(id, data)) .reduce((l,r) => (l union r).reduceByKey(_+_))
在第一个版本中,尝试过上面两个方案:1)先全部union,然后一次shuffle,这样在聚合的时会对内存要求过高;2)逐步聚合,虽然有多次shuffle,但是减少内存使用。两个方案可以理解为空间与实践的置换,前置空间换时间,后者用时间换空间。但很不幸,两方案均有性能瓶颈!后来经过分析与网上求助,最终找到问题所在:两个方案都使血缘(lineage)变长了。比如第一个方案,第一个dependency的结果在最后的结果中,会经过9次血缘,第二个dependency经过了8此,以此类推。而第二个方案,更加可怕,第一个经过了18次,第二个是16次。这么长的血缘,出错的几率是非常大的,所以需要大量的重跑,这也是导致最后shuffle不稳定的原因,有大量出错导致重计算。网上有个类似问题,可以参考 Stackoverflow due to long RDD Lineage 。
版本2:批量union,减少血缘
spark context对象提供一个union方法,用于批量聚合若干个rdd,并且没有血缘叠加效果,最终,将代码改为如下:
// 计算依赖def dependency(id:Int, graph:Graph[Double, Double]):RDD[(Long, Double)] = { ... } val data:Graph[Double, Double] = ... // loading from hdfs// 随机抽样val idList = data.vertices.takeSample(10)val rst1 = sc.union(idList.map(id => dependency(id, data))).reduceByKey(_+_)
上面的代码虽然减少了每个dependency RDD的血缘,但是shuffle还是很慢且不稳定。shuffle阶段有时需要几分钟,有时需要数个小时。不知道是不是由于需要同时容纳数个rdd,占据了大量内存,然后不断出现错误,进而导致重新计算,最终shuffle变得不稳定,这一点还有待证实。
版本3:RDD合并取代graphx leftJoin合并
版本2的问题是需要在内存中保存过多的中间节点,导致内存被拜拜浪费掉。所以,版本3在每一轮都有一个按点合并结果的过程。一开始使用graph对象的joinVertices方法合并中间结果,但是会导致每一轮的时间莫名奇妙得增加,而且是呈指数增长(这个坑后面可能还会遇到)。改用RDD规避,避免了这个问题。简化代码如下:
// 计算依赖def dependency(id:Int, g:Graph[Double, Double]):RDD[(VertexId, Double)] = { ... } val data:Graph[Double, Double] = ... // loading from hdfsval sampleVertices = data.vertices.takeSample(10)var rst: RDD[(VertexId, Double)] = nullfor(source <- sampleVertices) { val newDep = dependency(source, data).persist(cacheLevel)if (null == rst) {rst = newDep} else {rst = (rst union newDep).reduceByKey(_ + _).persist(cacheLevel)}}val finalRst = rst.mapVertices((_, attr) => attr * verticesNum / sampleSize) // 合并
计算效果
经过上面几轮优化后,效果有了大幅度提升。还有一些优化的小技巧,这里捎带提一下,比如使用EdgePartition2D的边分区策略可以进一步提高执行效率,合理的释放和缓存图对象也可以减少血缘,减少重算时间。现在使用100个executor,2核,每个executor 14G内存,对450,000,000点,1,253,792,736 边的图,随机迭代5轮,需要一千多分钟,每轮平均200分钟。
spark优化总结
spark底层api优化其实就两点 :
- 减少血缘 合理仔细的利用persisit,checkpoint和unpersisit,缓存中间变量并去掉无用的对象,避免过长的血缘重计算与合理的利用内存。但是,如果不适当的释放内存,可能导致没有缓存对象,仍然导致过长的血缘,这一点可以参考 Spark优化那些事(1)-请在action之后unpersisit! 。
- 减少shuffling shuffling需要网络开销,能少就少,能不用就不用。
上面的迭代过程其实就遵循上面两个原则进行的,最后得到了不错的效果。graphx的api使用起来还是有一定的坑,后面还需要多注意,并且研究graphx底层实现细节,希望可以发现版本3中的那个坑。
参考资料
- Brandes的快速介数计算
- Brandes的介数估算算法
- Stackoverflow due to long RDD Lineage
- Spark优化那些事(2)-graphx实现介数估算踩坑总结
- Spark GraphX实现PageRank
- Spark GraphX下强连通子图和社团发现算法在1T TPC-DS数据集下执行方法、优化和性能估算
- Spark学习笔记-GraphX-2
- Spark GraphX
- spark graphx
- Spark GraphX
- Spark Graphx
- Spark GraphX
- Spark中文手册9:spark GraphX编程指南(2)
- Spark中文手册9:spark GraphX编程指南(2)
- Spark中文手册9:spark GraphX编程指南(2)
- Spark MLlib LDA 基于GraphX实现原理及源码分析
- Spark GraphX实现Bron–Kerbosch算法-极大团问题
- Spark中的GraphX
- Spark GraphX相关使用方法
- Spark GraphX(一)
- spark graphx文章整理
- __autoload,spl_autoload_register与自动加载
- 图片选择器selector
- UVA1203 Argus (优先队列priority_queue)
- 神经网络快速入手
- 我的work list: Done, Doing, Push Doing, To Do
- Spark优化那些事(2)-graphx实现介数估算踩坑总结
- 内网和外网之间的通信
- 模式识别学习——遗传算法
- Win10安装Ubuntu16.04的常见问题
- RGB与YUV之间的转换
- 0101到高低电平原理
- Spark机器学习库(MLlib)指南
- 为什么0101二进制可以变为高低电平
- JSP小知识点总结