有没有办法将结果传递给驱动程序而无需等待所有分区完成执行?

时间:2017-01-23 15:35:57

标签: python apache-spark pyspark

有没有办法将结果传递给驱动程序而无需等待所有分区完成执行?

我是Spark的新手,所以如果有更好的方法,请指出我正确的方向。我想并行执行大量分区并使用spark来处理分发/重启等。操作完成后,我想将结果收集到驱动程序中的单个存档中。

使用toLocalIterator()

我能够使用toLocalIterator()执行此操作,according to the docs限制了驱动程序所需的资源。所以它基本上有效。

问题是toLocalIterator()不仅一次将驱动程序限制为一个分区,而且似乎每次都执行一个分区。这对我没用。下面的演示代码演示了这种行为。

使用persist() + count() + toLocalIterator()

我发现通过持久化然后用count()触发并行执行,我可以解决这个问题。之后,toLocalIterator()能够快速提取预先计算的结果。

这个问题是我有大量的分区(大约10 ^ 3或10 ^ 4),每个分区的大小约为15分钟。这最终会持续存在大量数据(并不是什么大不了的事)但更糟糕的是,一旦整体工作持续太久,它似乎就会失去持久性。分区最终会重新计算。我正在使用google dataproc和可抢占的工作人员,这可能与它有关,但我很确定它甚至会在固定工人上重新计算...我不确定究竟发生了什么。

在任何情况下,在访问第一个结果之前必须执行所有分区似乎并不理想。

下面的演示代码演示了当一切都持续良好并且迭代不会触发重新计算时的最佳情况。

??? - >迭代数据而不等待完全执行

有类似的吗?

复制/粘贴演示代码

import time
import pyspark.storagelevel

def slow_square(n):
    time.sleep(5)
    return n**2


with pyspark.SparkContext() as spark_context:
    numbers = spark_context.parallelize(range(4), 4)  # I think 4 is default executors locally
    squares = numbers.map(slow_square)

    # Use toLocalIterator()
    start = time.time()
    list(squares.toLocalIterator())
    print('toLocalIterator() took {:.1f} seconds (expected about 5)'.format(time.time() - start))
    # I get about 20s

    # Use count() to show that it's faster in parallel
    start = time.time()
    squares.count()
    print('count() took {:.1f} seconds (expected about 5)'.format(time.time() - start))
    # I get about 5s

    # Use persist() + count() + toLocalIterator()
    start = time.time()
    squares.persist(pyspark.storagelevel.StorageLevel.MEMORY_AND_DISK)
    squares.count()
    list(squares.toLocalIterator())
    print('persisted toLocalIterator() took {:.1f} seconds (expected about 5)'.format(time.time() - start))
    # I get about 5s

1 个答案:

答案 0 :(得分:2)

一般来说,这不是你通常在Spark中做的事情。通常,我们会尝试将通过驱动程序传递的数据量限制到最小。有两个主要原因:

  • 将数据传递给Spark驱动程序很容易成为应用程序的瓶颈。
  • 驱动程序实际上是批处理应用程序中的单点故障。

在正常情况下,您只需让工作继续,写入持久存储并最终对结果应用进一步的处理步骤。

如果您希望能够迭代访问结果,您可以选择以下几种方法:

  • 使用Spark Streaming。创建一个简单的过程,将数据推送到集群,然后收集每个批次。它简单,可靠,经过测试,并且不需要任何其他基础设施。
  • 使用foreach / foreachPartition处理数据,并在生成数据时将数据推送到外部邮件系统,并使用其他进程进行使用和编写。这需要额外的组件,但在概念上可以更容易(您可以使用背压,缓冲结果,将合并逻辑与驱动程序分开,以最大限度地降低应用程序失败的风险)。
  • Hack Spark累加器。任务完成后更新Spark累加器,以便您以离散批次处理累积的即将到来的数据。

    警告以下代码只是一个概念验证。它没有经过适当的测试,很可能是非常不可靠的

    示例AccumulatorParam使用RXPy

    # results_param.py
    
    from rx.subjects import Subject
    from pyspark import AccumulatorParam, TaskContext
    
    class ResultsParam(AccumulatorParam, Subject):
        """An observable accumulator which collects task results"""
        def zero(self, v):
            return []
    
        def addInPlace(self, acc1, acc2):
            # This is executed on the workers so we have to
            # merge the results
            if (TaskContext.get() is not None and 
                    TaskContext().get().partitionId() is not None):
                acc1.extend(acc2)
                return acc1
            else:
                # This is executed on the driver so we discard the results
                # and publish to self instead
                for x in acc2:
                    self.on_next(x)
                return []
    

    Simple Spark应用程序(Python 3.x):

    # main.py
    
    import time
    from pyspark import SparkContext, TaskContext
    
    sc = SparkContext(master="local[4]")
    sc.addPyFile("results_param.py")
    
    from results_param import ResultsParam
    
    # Define accumulator
    acc = sc.accumulator([], ResultsParam())
    
    # Dummy subscriber 
    acc.accum_param.subscribe(print)
    
    def process(x):
        """Identity proccess"""
        result = x
        acc.add([result])
    
        # Add some delay
        time.sleep(5)
    
        return result
    
    sc.parallelize(range(32), 8).foreach(process)
    

    这相对简单,但如果多个任务同时完成,则存在驱动程序压倒性风险,因此您必须显着超额订阅驱动程序资源(与并行度级别和任务结果的预期大小成比例)。

  • 直接使用Scala runJob(不支持Python)。

    Spark实际上异步获取结果,只要您不关心订单,就不需要等待处理所有数据。您可以看到例如the implementation Scala reduce

    应该可以使用这种机制将分区推送到Python进程,但我还没有尝试过。