Apache Spark:一个地图任务中的多个输出

时间:2016-02-02 17:32:54

标签: scala apache-spark

TL; DR:我有一个大文件,我迭代三次以获得三组不同的计数。有没有办法在数据的一次传递中获得三张地图?

更多细节:

我试图计算大文件中列出的字词和功能之间的PMI。我的管道看起来像这样:

val wordFeatureCounts = sc.textFile(inputFile).flatMap(line => {
  val word = getWordFromLine(line)
  val features = getFeaturesFromLine(line)
  for (feature <- features) yield ((word, feature), 1)
})

然后我重复这个以分别获取字数和特征计数:

val wordCounts = sc.textFile(inputFile).flatMap(line => {
  val word = getWordFromLine(line)
  val features = getFeaturesFromLine(line)
  for (feature <- features) yield (word, 1)
})

val featureCounts = sc.textFile(inputFile).flatMap(line => {
  val word = getWordFromLine(line)
  val features = getFeaturesFromLine(line)
  for (feature <- features) yield (feature, 1)
})

(我知道我可以迭代wordFeatureCounts来获取wordCountsfeatureCounts,但这并不能回答我的问题,并且在实践中查看运行时间我和#39;我不确定以这种方式实现它的速度实际上更快。还要注意,在计算出的计数未显示之后,还有一些reduceByKey操作以及我对此做的其他事情,因为它们与问题无关。)

我真正想做的是这样的事情:

val (wordFeatureCounts, wordCounts, featureCounts) = sc.textFile(inputFile).flatMap(line => {
  val word = getWordFromLine(line)
  val features = getFeaturesFromLine(line)
  val wfCounts = for (feature <- features) yield ((word, feature), 1)
  val wCounts = for (feature <- features) yield (word, 1)
  val fCounts = for (feature <- features) yield (feature, 1)
  ??.setOutput1(wfCounts)
  ??.setOutput2(wCounts)
  ??.setOutput3(fCounts)
})

有没有办法用火花做到这一点?在查找如何执行此操作时,我已经在将结果保存到磁盘时看到了有关多个输出的问题(没有帮助),而且我已经看到了一些关于累加器的信息(其中包括#39) ;看起来像我需要的那样),但那就是它。

另请注意,我无法将所有这些结果放在一个大清单中,因为我需要三个单独的地图。如果有一种有效的方法可以在事后分割组合的RDD,那可能会有效,但我能想到的唯一方法是最终迭代数据四次,而不是我目前做的三次(一次创建组合地图,然后三次将其过滤到我真正想要的地图中)。

3 个答案:

答案 0 :(得分:0)

无法将RDD拆分为多个RDD。如果你想一想这将如何在幕后工作,这是可以理解的。假设您将RDD x = sc.textFile("x")拆分为a = x.filter(_.head == 'A')b = x.filter(_.head == 'B')。到目前为止没有任何事情发生,因为RDD是懒惰的。但现在你打印a.count。因此Spark打开文件,并遍历这些行。如果该行以A开头,则会对其进行计数。但是我们如何处理以B开头的行?将来是否会打电话给b.count?或者它可能是b.saveAsTextFile("b")我们应该把这些线写在某个地方?我们现在还不知道。使用Spark API无法拆分RDD。

但是如果你知道自己想要什么,没有什么能阻止你实施某些东西。如果您想同时获得a.countb.count,则可以将以A开头的行映射到(1, 0),将带有B的行映射到(0, 1),然后在reduce中总结元组元素。如果您想在使用B计算行数时将A行保存到文件中,则可以在map之前使用filter(_.head == 'B').saveAsTextFile中的聚合器。

唯一的通用解决方案是将中间数据存储在某处。一种选择是只缓存输入(x.cache)。另一种方法是在单次传递中将内容写入单独的目录,然后将它们作为单独的RDD读回。 (参见Write to multiple outputs by key Spark - one Spark job。)我们在制作中这样做,效果很好。

答案 1 :(得分:0)

与传统的map-reduce编程相比,这是Spark的主要缺点之一。可以将一个RDD / DF / DS转换为另一个RDD / DF / DS,但是您不能将一个RDD映射到多个输出中。为了避免重新计算,您需要将结果缓存到某个中间的RDD中,然后运行多个映射操作以生成多个输出。如果您要处理合理的大小数据,则缓存解决方案将起作用。但是,如果与可用内存相比,数据量很大,则中间输出将溢出到磁盘,并且缓存的优势不会那么大。在此处查看讨论-https://issues.apache.org/jira/browse/SPARK-1476。这是一个古老的吉拉,但很重要。查看Mridul Muralidharan的评论。

Spark需要提供一种解决方案,其中映射操作可以产生多个输出而无需缓存。从函数式编程的角度来看,这可能并不优雅,但我认为,这是实现更好性能的一个很好的折衷方案。

答案 2 :(得分:0)

我也很失望地看到这是 Spark 相对于经典 MapReduce 的一个硬限制。我最终通过使用多个连续的地图来解决这个问题,在这些地图中我过滤掉了我需要的数据。

这是一个示意性玩具示例,它对数字 0 到 49 执行不同的计算并将两者写入不同的输出文件。

from functools import partial
import os
from pyspark import SparkContext

# Generate mock data
def generate_data():
    for i in range(50):
        yield 'output_square', i * i
        yield 'output_cube', i * i * i

# Map function to siphon data to a specific output
def save_partition_to_output(part_index, part, filter_key, output_dir):
    # Initialise output file handle lazily to avoid creating empty output files
    file = None
    
    try:
        for key, data in part:
            if key != filter_key:
                # Pass through non-matching rows and skip
                yield key, data
                continue

            if file is None:
                file = open(os.path.join(output_dir, '{}-part{:05d}.txt'.format(filter_key, part_index)), 'w')

            # Consume data
            file.write(str(data) + '\n')

        yield from []

    finally:
        if file is not None:
            file.close()

def main():
    sc = SparkContext()
    rdd = sc.parallelize(generate_data())

    # Repartition to number of outputs
    # (not strictly required, but reduces number of output files).
    #
    # To split partitions further, use repartition() instead or
    # partition by another key (not the output name).
    rdd = rdd.partitionBy(numPartitions=2)

    # Map and filter to first output.
    rdd = rdd.mapPartitionsWithIndex(partial(save_partition_to_output, filter_key='output_square', output_dir='.'))

    # Map and filter to second output.
    rdd = rdd.mapPartitionsWithIndex(partial(save_partition_to_output, filter_key='output_cube', output_dir='.'))

    # Trigger execution.
    rdd.count()

if __name__ == '__main__':
    main()

这将创建两个输出文件 output_square-part00000.txtoutput_cube-part00000.txt,具有所需的输出拆分。