在Scala中按大小拆分JSON-s的RDD

时间:2017-06-26 13:43:32

标签: json scala apache-spark

假设我们在HDFS中有很多JSON-s,但是对于原型,我们将一些JSON-s本地加载到Spark中:

val eachJson = sc.textFile("JSON_Folder/*.json")

我想编写一个遍历eachJson RDD [String]的作业,并计算每个JSON的大小。然后将大小添加到累加器,并将相应的JSON添加到StringBuilder。但是当连接的JSON-s的大小超过阈值时,我们开始将其他JSON-s存储在新的StringBuilder中。

例如,如果我们有100个JSON-s,并且我们开始逐个计算它们的大小,我们观察到从第32个元素开始,连接的JSON-s的大小超过了阈值,那么我们将它们组合在一起只有前31个JSON-s。之后我们从第32个元素开始。

到目前为止我设法做的是获取我们必须根据以下代码拆分RDD的索引:

eachJson.collect()
  .map(_.getBytes("UTF-8").length)
  .scanLeft(0){_ + _}
  .takeWhile(_ < 20000) //threshold = 20000
  .length-1

我也试过了:

val accum = sc.accumulator(0, "My Accumulator")
val buf = new StringBuilder
while(accum.value < 20000)
  {
    for(i <- eachJson)
      {
        accum.add(i.getBytes("UTF-8").length)
        buf ++= i
      }    
  }

但是我收到以下错误: org.apache.spark.SparkException: Task not serializable

我如何通过Scala在Spark中执行此操作? 我使用Spark 1.6.0和Scala 2.10.6

2 个答案:

答案 0 :(得分:1)

不是答案;只是为了指出正确的方向。你得到&#34;任务不可序列化&#34;异常,因为您的val buf = new StringBuilder在RDD&{39} foreachfor(i <- eachJson))中使用。 Spark无法分发您的buf变量,因为StringBuilder本身不可序列化。此外,你不应该直接访问可变状态。因此,建议您将所需的所有数据都放到Accumulator,而不仅仅是尺寸:

case class MyAccumulator(size: Int, result: String)

并使用rdd.aggregaterdd.fold

之类的内容
eachJson.fold(MyAccumulator(0, ""))(...)

//or

eachJson.fold(List.empty[MyAccumulator])(...)

或者只需将scanLeftcollect一起使用。

请注意,这不可扩展(与StringBuilder / collect解决方案相同)。为了使其可扩展 - 使用mapPartitions

更新。 mapPartitions将为您提供部分聚合JSON的能力,因为您将获得&#34; local&#34; iterator(partition)作为输入 - 您可以将其作为常规scala集合进行操作。如果你没有连接一些小百分比的JSON,那就足够了。

 eachJson.mapPartitions{ localCollection =>
    ... //compression logic here
 }

答案 1 :(得分:1)

Spark的编程模型对于你想要达到的目标并不理想,如果我们采用&#34;聚合元素的一般问题取决于只能通过检查前面的元素来识别的东西&#34;,有两个原因:

  1. 一般来说,Spark不会对数据进行排序(但它可以做到这一点)
  2. Sparks处理分区中的数据,并且分区的大小通常不是(例如默认情况下)取决于数据的内容,而是取决于默认分区,其作用是将数据均匀地划分为分区。
  3. 所以它不是一个可能的问题(它是),而是一个问题&#34;它的成本是多少&#34; (CPU /内存/时间),它为你买的东西。

    精确解决方案草案

    如果我要拍摄一个精确的解决方案(确切地说,我的意思是:保留元素顺序,例如由JSON中的时间戳定义,并将确切连续的输入分组到接近边界的最大量),我会:

    1. 对RDD进行排序(有一个sortBy函数,这样做):这是一个完整的数据随机播放,所以它很昂贵。
    2. 在排序之后为每一行提供一个id(有一个RDD版本的zipWithIndex,它尊重RDD上的排序,如果存在的话。还有一个更快的数据帧等价物,它会创建单调增加的索引,尽管不是连续的索引)。
    3. 收集计算大小边界所必需的结果部分(边界是步骤2中定义的ID),就像您一样。这再次是对数据的完整传递。
    4. 创建一个尊重这些边界的数据分区器(例如,确保单个边界的每个元素都在同一个分区中),并将此分区器应用于在步骤2中获得的RDD(数据上的另一个完整的shuffle) 。您只需拥有逻辑上等同于您期望的分区,例如:大小总和低于一定限度的元素组。但是,在重新分区过程中,每个分区内的排序可能已丢失。所以你还没有结束!
    5. 然后我会将这个结果的mapPartitions转到:
       5.1。将数据本地传递给每个分区,
       5.2。我需要对数据结构中的组项进行排序
    6. 其中一个关键是不要在步骤4和步骤5之间应用任何与分区混淆的东西。 只要&#34;分区映射&#34;适合驾驶员的记忆,这几乎是一个实用的解决方案,但成本非常高。

      更简单的版本(放宽约束)

      如果组不能达到最佳大小,那么解决方案变得更加简单(并且如果你已经设置了一个,它会尊重RDD的顺序):如果没有,你几乎可以编码Spark,只是一个JSON文件的迭代器。

      Personnaly,我定义了一个递归累加器函数(没有任何火花相关的)就像这样(我想你可以使用takeWhile编写更短,更高效的版本):

        /**
          * Aggregate recursively the contents of an iterator into a Seq[Seq[]]
          * @param remainingJSONs the remaining original JSON contents to be aggregated
          * @param currentAccSize the size of the active accumulation
          * @param currentAcc the current aggregation of json strings
          * @param resultAccumulation the result of aggregated JSON strings
          */
        @tailrec
        def acc(remainingJSONs: Iterator[String], currentAccSize: Int, currentAcc: Seq[String], resultAccumulation: Seq[Seq[String]]): Seq[Seq[String]] = {
          // IF there is nothing more in the current partition
          if (remainingJSONs.isEmpty) {
            // And were not in the process of acumulating
            if (currentAccSize == 0)
              // Then return what was accumulated before
              resultAccumulation
            else
              // Return what was accumulated before, and what was in the process of being accumulated
              resultAccumulation :+ currentAcc
          } else {
            // We still have JSON items to process
            val itemToAggregate = remainingJSONs.next()
            // Is this item too large for the current accumulation ?
            if (currentAccSize + itemToAggregate.size > MAX_SIZE) {
              // Finish the current aggregation, and proceed with a fresh one
              acc(remainingJSONs, itemToAggregate.size, Seq(itemToAggregate), resultAccumulation :+ currentAcc)
            } else {
              // Accumulate the current item on top of the current aggregation
              acc(remainingJSONs, currentAccSize + itemToAggregate.size, currentAcc :+ itemToAggregate, resultAccumulation)
            }
          }
        }
      

      不,你拿这个累积代码,让它为spark的数据帧的每个分区运行:

      val jsonRDD = ...
      val groupedJSONs = jsonRDD.mapPartitions(aPartition => {
        acc(aPartition, 0, Seq(), Seq()).iterator
      })
      

      这会将RDD[String]变为RDD[Seq[String]],其中每个Seq[String]由连续的RDD元素组成(如果RDD已经排序,则可以预测,否则可能无法预测),其总长度低于阈值。 可能是什么&#34;次优&#34;就是说,在每个分区的末尾,可能只有几个(可能是单个)JSON的Seq[String],而在下一个分区的开头,创建了一个完整的JSON。