如何在一个应用程序中拆除SparkSession并创建一个新的?

时间:2017-01-05 18:18:18

标签: python apache-spark pyspark

我有一个包含多个独立模块的pyspark程序,每个模块都可以独立处理数据以满足我的各种需求。但它们也可以链接在一起以处理管道中的数据。这些模块中的每一个都构建了一个SparkSession并且可以自己完美地执行。

然而,当我尝试在同一个python进程中连续运行它们时,我遇到了问题。在管道中的第二个模块执行时,sp​​ark抱怨我尝试使用的SparkContext已经停止:

py4j.protocol.Py4JJavaError: An error occurred while calling o149.parquet.
: java.lang.IllegalStateException: Cannot call methods on a stopped SparkContext.

这些模块中的每一个都在执行开始时构建SparkSession,并在其进程结束时停止sparkContext。我建立并停止会话/上下文:

session = SparkSession.builder.appName("myApp").getOrCreate()
session.stop()

根据official documentationgetOrCreate"获取现有的SparkSession,或者,如果没有现有的SparkSession,则根据此构建器中设置的选项创建一个新的SparkSession。"但我不想要这种行为(此过程尝试获取现有会话)。我无法找到任何方法来禁用它,我无法弄清楚如何销毁会话 - 我只知道如何停止其关联的SparkContext。

如何在独立模块中构建新的SparkSession,并在同一个Python进程中按顺序执行它们,而以前的会话不会干扰新创建的?

以下是项目结构的示例:

main.py

import collect
import process

if __name__ == '__main__':
    data = collect.execute()
    process.execute(data)

collect.py

import datagetter

def execute(data=None):
    session = SparkSession.builder.appName("myApp").getOrCreate()

    data = data if data else datagetter.get()
    rdd = session.sparkContext.parallelize(data)
    [... do some work here ...]
    result = rdd.collect()
    session.stop()
    return result

process.py

import datagetter

def execute(data=None):
    session = SparkSession.builder.appName("myApp").getOrCreate()
    data = data if data else datagetter.get()
    rdd = session.sparkContext.parallelize(data)
    [... do some work here ...]
    result = rdd.collect()
    session.stop()
    return result

4 个答案:

答案 0 :(得分:7)

长话短说,Spark(包括PySpark)并不是设计用于在单个应用程序中处理多个上下文。如果您对故事的JVM方面感兴趣,我建议您阅读SPARK-2243(解析为无法修复)。

在PySpark中做出了许多设计决策,反映了这些决定包括但不限于a singleton Py4J gateway。有效you cannot have multiple SparkContexts in a single applicationSparkSession不仅绑定到SparkContext,而且还引入了自己的问题,如处理本地(独立)Hive Metastore(如果使用的话)。此外,内部使用SparkSession.builder.getOrCreate的函数取决于您现在看到的行为。一个值得注意的例子是UDF注册。如果存在多个SQL上下文,则其他函数可能会出现意外行为(例如RDD.toDF)。

多个上下文不仅不受支持,而且在我个人看来,违反了单一责任原则。您的业​​务逻辑不应该关注所有设置,清理和配置细节。

我的个人建议如下:

  • 如果应用程序由多个连贯的模块组成,这些模块可以组成并受益于具有缓存的单个执行环境,并且常见的Metastore会初始化应用程序入口点中的所有必需上下文,并在必要时将这些上下文传递给各个管道:

    • main.py

      from pyspark.sql import SparkSession
      
      import collect
      import process
      
      if __name__ == "__main__":
          spark: SparkSession = ...
      
          # Pass data between modules
          collected = collect.execute(spark)
          processed = process.execute(spark, data=collected)
          ...
          spark.stop()
      
    • collect.py / process.py

      from pyspark.sql import SparkSession
      
      def execute(spark: SparkSession, data=None):
          ...
      
  • 否则(根据您的描述似乎就是这种情况)我会设计入口点来执行单个管道并使用外部worfklow管理器(如Apache AirflowToil)来处理执行。

    它不仅更清洁,而且还允许更灵活的故障恢复和调度。

    同样的事情当然可以通过构建器来完成,但就像smart person曾经说过的那样:显式优于隐式。

    • main.py

      import argparse
      
      from pyspark.sql import SparkSession
      
      import collect
      import process
      
      pipelines = {"collect": collect, "process": process}
      
      if __name__ == "__main__":
          parser = argparse.ArgumentParser()
          parser.add_argument('--pipeline')
          args = parser.parse_args()
      
          spark: SparkSession = ...
      
          # Execute a single pipeline only for side effects
          pipelines[args.pipeline].execute(spark)
          spark.stop()
      
    • 与上一点一样,
    • collect.py / process.py

不管怎样,我会保留一个且只有一个地方设置上下文,而且只有一个地方被拆除。

答案 1 :(得分:3)

这是一种解决方法,但不是解决方案:

我发现source code中的SparkSession类包含以下__init__(我已删除了此处显示的不相关的代码行):

_instantiatedContext = None

def __init__(self, sparkContext, jsparkSession=None):
    self._sc = sparkContext
    if SparkSession._instantiatedContext is None:
        SparkSession._instantiatedContext = self

因此,我可以在调用_instantiatedContext后将会话的None属性设置为session.stop()来解决我的问题。当下一个模块执行时,它会调用getOrCreate()并找不到之前的_instantiatedContext,因此会分配一个新的sparkContext

这不是一个非常令人满意的解决方案,但它可以作为满足我当前需求的解决方法。我不确定开始独立会话的整个方法是否是反模式或只是不寻常。

答案 2 :(得分:1)

spark_current_session = SparkSession. \
                             builder. \
                             appName('APP'). \
                             config(conf=SparkConf()). \
                             getOrCreate()
spark_current_session.newSession()

您可以从当前会话创建新会话

答案 3 :(得分:0)

为什么不将相同的spark会话实例传递到管道的多个阶段?您可以使用构建器模式。听起来像你在每个阶段结束时收集结果集,然后将数据传递到下一个阶段。考虑将数据保留在同一会话中的集群中,并将会话引用和结果引用从阶段传递到阶段,直到应用程序完成。

换句话说,把

session = SparkSession.builder...

...在你的主人家。