本文记录了作者通过查看spark UI中Executors记录,成功定位到性能较差代码的位置。进而进行代码优化后,将任务执行时间从4hour优化到30min。
背景
今天用spark执行了数据量并不算大的任务,但是点进Stages发现,任务执行很慢。猜测代码性能较差,请教了周围的大神,总结出如下定位性能较差代码位置的方法。
定位方法
第一步
进入Executors,找到Active Tasks为1的Executor ID,一般为蓝色,如图一。
第二步
找到Active Tasks为1的Executor ID后,点击Thread Dump进入,寻找Thread State 为 RUNNABLE 的 thread id,如图二、图三。
第三步
找到为 RUNNABLE 的 thread id 后,点击展开,仔细查看调用栈中自己所写程序的部分,如图四。发现问题出在83行,以及40行,找到对应代码位置进行检查。
代码优化
优化点一
import com.huaban.analysis.jieba.JiebaSegmenter.SegMode
import com.huaban.analysis.jieba.{JiebaSegmenter, SegToken}
val wordList:Array[String] = sc.textFile(path).collect()
val srcDF: DataFrame = hiveContext.sql(sql).mapPartitions(partition => {
val buffer = new ListBuffer[(String, String)]
val jiebaSegmenter = new JiebaSegmenter()
val it = partition
while (it.hasNext) {
val row = it.next()
val appid = row.getLong(0).toString
val text = jiebaSegmenter
.process(row.getString(1).toString, SegMode.SEARCH)
.toArray
.map(_.asInstanceOf[SegToken].word)
.filter(x => !wordList.contains(x)) // 这里即为83行
.mkString(" ")
buffer.append((appid, text))
}
buffer.iterator
})
优化后
import com.huaban.analysis.jieba.JiebaSegmenter.SegMode
import com.huaban.analysis.jieba.{JiebaSegmenter, SegToken}
val wordList = sc.textFile(path).collect().toSet // 将Array转换为Set
val wordList_bc = sc.broadcast(frequencyFilterWordList) // 使用广播变量
val srcDF: DataFrame = hiveContext.sql(sql).mapPartitions(partition => {
val buffer = new ListBuffer[(String, String)]
val jiebaSegmenter = new JiebaSegmenter()
val it = partition
while (it.hasNext) {
val row = it.next()
val appid = row.getLong(0).toString
val text = jiebaSegmenter
.process(row.getString(1).toString, SegMode.SEARCH)
.toArray
.map(_.asInstanceOf[SegToken].word)
.filter(x => !wordList_bc.value.contains(x)) // 使用Set类型的广播变量
.mkString(" ")
buffer.append((appid, text))
}
buffer.iterator
})
原因分析:
- Set的contains操作时间复杂度低于Array。
- 通过使用广播变量,节省在不同task之间的拷贝开销。
优化点二
import org.apache.spark.mllib.linalg.{SparseVector, Vector, Vectors}
def cosinSimilarity(v1: SparseVector, v2: SparseVector): Double = {
val indices2: Array[Int] = v2.indices
var sum = 0.0
for (index <- v1.indices) { // 这里即为40行
if (indices2.contains(index)) {
sum += v1(index) * v2(index)
}
}
sum / (Vectors.norm(v1, 2) * Vectors.norm(v2, 2))
}
优化后
import org.apache.spark.mllib.linalg.{SparseVector, Vector, Vectors}
def cosinSimilarity(v1: SparseVector, v2: SparseVector): Double = {
val indices2: Set[Int] = v2.indices.toSet // 将indices2由Array转换为Set
val indices1: Set[Int] = v1.indices.toSet
var sum = 0.0
for (index <- indices1) { // 将v1.indices改写为Set类型的indices1
if (indices2.contains(index)) {
sum += v1(index) * v2(index)
}
}
sum / (Vectors.norm(v1, 2) * Vectors.norm(v2, 2))
}
原因分析:
- Set的contains操作时间复杂度低于Array。
- 避免每次循环都进行v1.indices操作。如果向量的长度很大,就能节省很多indices的计算开销。
效果
优化前4小时都不出结果,优化后不到30分钟即可。