围绕性能和工作spark-sql GROUP BY的内存问题

时间:2015-05-19 09:49:09

标签: apache-spark apache-spark-sql

考虑以下运行具有相对大量聚合和相对大量组的GROUP BY的示例:

import org.apache.spark.sql.hive.HiveContext
import org.apache.spark.SparkContext._
val h = new HiveContext(sc)
import h.implicits._

val num_columns = 3e3.toInt
val num_rows = 1e6.toInt
val num_groups = 1e5.toInt

case class Data(A: Long = (math.random*num_groups).toLong)

val table = (1 to num_rows).map(i => Data()).toDF

val aggregations = (1 to num_columns).map(i => s"count(1) as agg_$i")
table.registerTempTable("table")
val result = h.sql(s"select a, ${aggregations.mkString(",")} from table group by a")

// Write the result to make sure everyting is executed
result.save(s"result_${num_columns}_${num_rows}_${num_groups}.parquet", "parquet")

这个作业的输入只有8MB,输出大约2.4GB,我在一个集群上运行它,每个工作机器有61GB内存。结果:所有工作程序都因OutOfMemory异常而崩溃。 即使num_columns的值较低,由于GC开销,作业也会变得非常慢。

我们尝试过的事情包括:

  • 减少分区大小(减少内存占用但增加了簿记开销)
  • 在进行聚合之前使用HashPartitioner对数据进行预分区(减少内存消耗但需要在任何实际工作发生之前进行完全重新洗牌)

有没有更好的方法来达到预期的效果?

2 个答案:

答案 0 :(得分:3)

一般来说,像这样的问题几乎是通用的解决方案是将分区大小保持在合理的大小。虽然"合理"是有点主观的,可以根据具体情况而有所不同100-200MB看起来是个好地方。

我可以轻松地汇总您在单个工作人员上提供的示例数据,保留默认spark.executor.memory(1GB)并将总可用资源限制为8个内核和8GB RAM。所有这些通过使用50个分区并保持聚合时间大约3秒而没有任何特殊的调整(这在1.5.2到2.0.0之间或多或少是一致的)。

总结一下:如果可能的话,在创建spark.default.parallelism时增加DataFrame或明确设置分区数。对于像这样的小数据集,默认spark.sql.shuffle.partitions应该足够了。

答案 1 :(得分:-1)

由于我不确定您使用的聚合函数是什么,因此很难说背景中的火花是什么。在任何情况下,为了对每个聚合函数进行更多控制,我将为基本RDD本身的每个聚合函数运行一个reduceByKey转换。然后,您可以根据需要简单地加入结果。通过这种方式,您可以更好地控制并且可以看到哪个聚合“花费”最多,而且您可以通过操作来避免该组,除了改组之外,还可能导致内存问题(由于整个组的移动)将数据转换为单个分区)。以下是一个简短的插图,其中aggrigationFunctions是您的聚合函数列表及其ID和实际函数(元组列表)。

val aggrigationResults = aggrigationFunctions.map( 
   f => {
     val aggRes = baseRdd
                     .map(x => (x.[the field to group by], x.[the value to aggrigate]))
                     .reduceByKey(f.func)
     (f.id, aggRes)
   }
)