web-dev-qa-db-fra.com

Comment puis-je supprimer une SparkSession et en créer un nouveau dans une seule application?

J'ai un programme pyspark avec plusieurs modules indépendants qui peuvent chacun traiter indépendamment les données pour répondre à mes divers besoins. Mais ils peuvent également être chaînés pour traiter des données dans un pipeline. Chacun de ces modules construit une SparkSession et s’exécute à la perfection. 

Cependant, lorsque j'essaie de les exécuter en série dans le même processus Python, je rencontre des problèmes. Au moment où le deuxième module du pipeline s'exécute, spark se plaint que le SparkContext que je tente d'utiliser a été arrêté:

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

Chacun de ces modules crée une SparkSession au début de l'exécution et arrête le sparkContext à la fin de son processus. Je construis et arrête des sessions/contextes comme ceci:

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

Selon la documentation officielle , getOrCreate "obtient une SparkSession existante ou, s'il n'en existe pas, en crée une nouvelle en fonction des options définies dans ce générateur." Mais je ne veux pas de ce comportement (ce comportement où le processus tente d’obtenir une session existante). Je ne trouve aucun moyen de le désactiver ni de détruire la session. Je sais seulement comment arrêter le SparkContext associé.

Comment puis-je construire de nouvelles SparkSessions dans des modules indépendants et les exécuter de manière séquentielle dans le même processus Python sans que des sessions précédentes n'interfèrent avec celles nouvellement créées? 

Voici un exemple de la structure du projet:

main.py

import collect
import process

if __== '__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
10
vaer-k

En bref, Spark (y compris PySpark) n'est pas conçu pour gérer plusieurs contextes dans une seule application. Si vous êtes intéressé par le côté JVM de l'histoire, je vous recommanderais de lire SPARK-2243 (résolu comme ne corrigera pas).

Un certain nombre de décisions de conception ont été prises dans PySpark, ce qui inclut notamment, mais sans s'y limiter, une passerelle Pyleton 4J singleton . Effectivement vous ne pouvez pas avoir plusieurs SparkContexts dans une même application . SparkSession est non seulement lié à SparkContext, mais pose également des problèmes qui lui sont propres, tels que la gestion du métastore Hive local (autonome), le cas échéant. De plus, il existe des fonctions qui utilisent SparkSession.builder.getOrCreate en interne et dépendent du comportement que vous voyez actuellement. Un exemple notable est l'enregistrement UDF. D'autres fonctions peuvent présenter un comportement inattendu si plusieurs contextes SQL sont présents (par exemple, RDD.toDF).

Les contextes multiples ne sont pas seulement non pris en charge, mais violent également, selon mon opinion personnelle, le principe de responsabilité unique. Votre logique métier ne doit pas concerner tous les détails de la configuration, du nettoyage et de la configuration.

Mes recommandations personnelles sont les suivantes:

  • Si l'application est composée de plusieurs modules cohérents pouvant être composés et bénéficiant d'un environnement d'exécution unique avec mise en cache et métastore commun, initialisez tous les contextes requis dans le point d'entrée de l'application et transmettez-les à des pipelines individuels si nécessaire:

    • main.py:

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

      from pyspark.sql import SparkSession
      
      def execute(spark: SparkSession, data=None):
          ...
      
  • Sinon (cela semble être le cas ici en fonction de votre description), je concevrais entrypoint pour exécuter un pipeline unique et utiliser un gestionnaire de Worfklow externe (comme Apache Airflow ou Toil ) pour gérer le exécution.

    Il est non seulement plus propre, mais permet également une récupération et une planification des erreurs beaucoup plus souples.

    La même chose peut être faite avec les constructeurs mais comme un personne intelligente a déjà dit: _/Explicit est meilleur qu'implicite.

    • main.py

      import argparse
      
      from pyspark.sql import SparkSession
      
      import collect
      import process
      
      pipelines = {"collect": collect, "process": process}
      
      if __== "__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 comme dans le point précédent.

D'une manière ou d'une autre, je garderais un seul et même endroit où le contexte est défini et un seul et unique endroit où il est démontable. 

5
user6910411

Voici une solution de contournement, mais pas une solution:

J'ai découvert que la classe SparkSession du code source contient le __init__ suivant (j'ai supprimé les lignes de code non pertinentes de l'affichage ici):

_instantiatedContext = None

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

Par conséquent, je peux contourner mon problème en définissant l'attribut _instantiatedContext de la session sur None après avoir appelé session.stop(). Lorsque le module suivant s'exécute, il appelle getOrCreate() et ne trouve pas le _instantiatedContext précédent. Il affecte donc une nouvelle sparkContext.

Ce n'est pas une solution très satisfaisante mais elle sert de solution de contournement pour répondre à mes besoins actuels. Je ne sais pas si cette approche de démarrage de sessions indépendantes est anti-modèle ou tout simplement inhabituelle.

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

vous pouvez créer une nouvelle session à partir de la session en cours

1
Veera Marni

Pourquoi ne pas transmettre la même instance de session d'allumage aux multiples étapes de votre pipeline? Vous pouvez utiliser un modèle de générateur. Il me semble que vous collectez des jeux de résultats à la fin de chaque étape, puis que vous transmettez ces données à l’étape suivante. Envisagez de laisser les données du cluster dans la même session et de transmettre la référence de session et la référence de résultat étape par étape jusqu'à la fin de votre application.

En d'autres termes, mettez le 

session = SparkSession.builder...

... dans votre main.

0
ThatDataGuy