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
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.py
process.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.
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.
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
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.