繁体   English   中英

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

[英]Split RDD of JSON-s by size in Scala

假设我们在HDFS中有很多JSON,但是对于一个原型,我们使用以下代码在本地将一些JSON加载到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

没有答案; 只是为了指出正确的方向。 您会收到“任务不可序列化”异常,因为在RDD的foreach使用了val buf = new StringBuilderfor(i <- eachJson) )。 由于StringBuilder本身不可序列化,因此Spark无法分发您的buf变量。 此外,您不应该直接访问可变状态。 因此,建议将所需的所有数据放入Accumulator ,而不仅仅是大小:

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

并使用rdd.aggregaterdd.fold类的东西:

eachJson.fold(MyAccumulator(0, ""))(...)

//or

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

或在collect将其与scanLeft一起使用。

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


更新。 mapPartitions使您能够部分聚合JSON,因为您将获得“本地”迭代器(分区)作为输入-您可以将其作为常规的scala集合进行操作。 如果您可以确定没有串联一些小的JSON,那可能就足够了。

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

如果我们采用“汇总元素取决于只能通过检查以前的元素才能知道的内容”这一普遍问题,Spark的编程模型对于您要实现的目标不是理想的,这有两个原因:

  1. 一般来说,Spark不会对数据强加排序(但是可以做到)
  2. Sparks处理分区中的数据,并且分区的大小通常不(例如,默认情况下)取决于数据的内容,而是由默认的分区器负责,该角色的作用是将数据均匀地划分为多个分区。

因此,这实际上不是一个可能的问题,而是“购买成本”(CPU /内存/时间)(“多少钱”)的问题。

精确解决方案的草稿

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

  1. 在RDD上强加一个排序(有一个sortBy函数可以做到这一点):这是一个完整的数据混洗,因此很昂贵。
  2. 在排序之后,为每一行提供一个ID(如果存在zipWithIndex,则有一个RDD版本,该版本尊重RDD上的排序。还有一个等效的数据帧,它可以创建单数递增的索引,尽管不是连续的)。
  3. 收集与计算尺寸边界(边界是在第2步中定义的id)所必需的结果的一部分,与您所做的差不多。 这又是一次完整的数据传递。
  4. 创建一个尊重这些边界的数据分区器(例如,确保单个边界的每个元素都在同一个分区中),然后将此分区器应用于在步骤2中获得的RDD(对数据进行另一次完全混洗)。 您得到的分区在逻辑上与期望的分区相等,例如,大小总和在一定限制内的元素组。 但是每个分区内的顺序可能在重新分区过程中丢失了。 所以你还没有结束!
  5. 然后,我将基于此结果的MapPartitions映射到:
    5.1。 将数据本地化到每个分区,
    5.2。 排序后需要的数据结构中的分组项

关键之一是不要应用任何在步骤4和5之间混乱的分区。只要“分区图”适合驱动程序的内存,这几乎是一种实用的解决方案,但代价却很高。

较简单的版本(约束宽松)

如果组不能达到最佳大小是可以的,那么解决方案将变得更加简单(并且如果您设置了RDD,它会遵循RDD的顺序):如果根本没有Spark,则几乎可以编写代码,只是JSON文件的迭代器。

就个人而言,我将像这样定义一个递归累加器函数(与火花无关)(我想您可以使用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进行了排序,这是可以预测的,否则可能无法预测),它们的总长度低于阈值。 所谓“次优”的意思是,在每个分区的末尾,可能存在一个Seq[String]其中只有几个(可能是单个)JSON,而在下一个分区的开头,则是一个完整的创建。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM