web-dev-qa-db-fra.com

Enregistrement PySpark de l'exécuteur

Quelle est la bonne façon d'accéder à l'enregistreur log4j de Spark à l'aide de pyspark sur un exécuteur?

Il est facile de le faire dans le pilote mais je n'arrive pas à comprendre comment accéder aux fonctionnalités de journalisation sur l'exécuteur afin que je puisse me connecter localement et laisser YARN collecter les journaux locaux.

Y a-t-il un moyen d'accéder à l'enregistreur local?

La procédure de journalisation standard ne suffit pas car je ne peux pas accéder au contexte de l'étincelle à partir de l'exécuteur.

11
Chobeat

Vous ne pouvez pas utiliser le journal local log4j sur les exécuteurs. Les travailleurs Python créés par les exécuteurs jvms n'a pas de connexion de rappel à Java, ils ne font que recevoir des commandes. Mais il existe un moyen de se connecter à partir d’exécuteurs en utilisant la journalisation standard Python et de les capturer par YARN. 

Sur votre HDFS, placez un fichier de module python qui configure la journalisation une fois par travailleur python et les fonctions de journalisation des mandataires (nommez-le logger.py):

import os
import logging
import sys

class YarnLogger:
    @staticmethod
    def setup_logger():
        if not 'LOG_DIRS' in os.environ:
            sys.stderr.write('Missing LOG_DIRS environment variable, pyspark logging disabled')
            return 

        file = os.environ['LOG_DIRS'].split(',')[0] + '/pyspark.log'
        logging.basicConfig(filename=file, level=logging.INFO, 
                format='%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s')

    def __getattr__(self, key):
        return getattr(logging, key)

YarnLogger.setup_logger()

Importez ensuite ce module dans votre application:

spark.sparkContext.addPyFile('hdfs:///path/to/logger.py')
import logger
logger = logger.YarnLogger()

Et vous pouvez utiliser à l'intérieur de votre pyspark des fonctions telles que la bibliothèque de journalisation normale:

def map_sth(s):
    logger.info("Mapping " + str(s))
    return s

spark.range(10).rdd.map(map_sth).count()

Le pyspark.log sera visible dans le gestionnaire de ressources et sera collecté à la fin de l'application. Vous pourrez donc accéder à ces journaux ultérieurement avec yarn logs -applicationId .....  enter image description here

17
Mariusz

Notez que la réponse de Mariusz renvoie un proxy au module de journalisation. Cela fonctionne (vote positif) lorsque vos demandes de journalisation sont très basiques. Une fois que vous êtes intéressé par des tâches telles que la configuration de plusieurs instances de consignateur ou l'utilisation de plusieurs gestionnaires, cela fait défaut. Par exemple. Si vous souhaitez exécuter un ensemble de codes plus volumineux lors du débogage, l'une des solutions consiste à vérifier la méthode isEnabledFor d'une instance de consignateur, comme suit:

logger = logging.getLogger(__name__)
if logger.isEnabledFor(logging.DEBUG):
    # do some heavy calculations and call `logger.debug` (or any other logging method, really)

Cela échouerait lorsque la méthode serait appelée sur le module de journalisation, comme dans la réponse de Mariusz, car le module de journalisation ne dispose pas d'un tel attribut.

Un moyen de résoudre ce problème consiste à créer un module spark_logging.py dans lequel vous configurez la journalisation et renvoyez une nouvelle instance de Logger. Le code ci-dessous montre un exemple de cela, qui configure la journalisation à l'aide de dictConfig. Il ajoute également un filtre afin que le nombre de répétitions de tous les nœuds de travail soit considérablement réduit lors de l'utilisation du consignateur racine (l'exemple de filtre provient de Christopher Dunn ( ref )).

# spark_logging.py
import logging
import logging.config
import os
import tempfile
from logging import *  # gives access to logging.DEBUG etc by aliasing this module for the standard logging module


class Unique(logging.Filter):
    """Messages are allowed through just once.
    The 'message' includes substitutions, but is not formatted by the
    handler. If it were, then practically all messages would be unique!
    """
    def __init__(self, name=""):
        logging.Filter.__init__(self, name)
        self.reset()

    def reset(self):
        """Act as if nothing has happened."""
        self.__logged = {}

    def filter(self, rec):
        """logging.Filter.filter performs an extra filter on the name."""
        return logging.Filter.filter(self, rec) and self.__is_first_time(rec)

    def __is_first_time(self, rec):
        """Emit a message only once."""
        msg = rec.msg %(rec.args)
        if msg in self.__logged:
            self.__logged[msg] += 1
            return False
        else:
            self.__logged[msg] = 1
            return True


def getLogger(name, logfile="pyspark.log"):
    """Replaces getLogger from logging to ensure each worker configures
    logging locally."""

    try:
        logfile = os.path.join(os.environ['LOG_DIRS'].split(',')[0], logfile)
    except (KeyError, IndexError):
        tmpdir = tempfile.gettempdir()
        logfile = os.path.join(tmpdir, logfile)
        rootlogger = logging.getLogger("")
        rootlogger.addFilter(Unique())
        rootlogger.warning(
            "LOG_DIRS not in environment variables or is empty. Will log to {}."
            .format(logfile))

    # Alternatively, load log settings from YAML or use JSON.
    log_settings = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'file': {
                'class': 'logging.FileHandler',
                'level': 'DEBUG',
                'formatter': 'detailed',
                'filename': logfile
            },
            'default': {
                'level': 'INFO',
                'class': 'logging.StreamHandler',
            },
        },
        'formatters': {
            'detailed': {
                'format': ("%(asctime)s.%(msecs)03d %(levelname)s %(module)s - "
                           "%(funcName)s: %(message)s"),
            },
        },
        'loggers': {
            'driver': {
                'level': 'INFO',
                'handlers': ['file', ]
            },
            'executor': {
                'level': 'DEBUG',
                'handlers': ['file', ]
            },
        }
    }

    logging.config.dictConfig(log_settings)
    return logging.getLogger(name)

Vous pouvez ensuite importer ce module et l’aliaser pour logging lui-même:

from pyspark.sql import SparkSession

spark = SparkSession \
    .builder \
    .appName("Test logging") \
    .getOrCreate()

try:
    spark.sparkContext.addPyFile('s3://YOUR_BUCKET/spark_logging.py')
except:
    # Probably running this locally. Make sure to have spark_logging in the PYTHONPATH
    pass
finally:
    import spark_logging as logging

def map_sth(s):
    log3 = logging.getLogger("executor")
    log3.info("Logging from executor")

    if log3.isEnabledFor(logging.DEBUG):
        log3.debug("This statement is only logged when DEBUG is configured.")

    return s

def main():
    log2 = logging.getLogger("driver")
    log2.info("Logging from within module function on driver")
    spark.range(100).rdd.map(map_sth).count()

if __== "__main__":
    log1 = logging.getLogger("driver")
    log1.info("logging from module level")
    main()

Comme avec réponse de Mariusz , les journaux seront accessibles à l'aide du gestionnaire de ressources (ou déchargés dans votre dossier temporaire lorsque LOG_DIRS ne figure pas dans vos variables d'environnement) . La gestion des erreurs effectuée en haut de ce script est ajouté pour que vous puissiez exécuter ce script localement. 

Cette approche offre plus de liberté: vous pouvez demander aux exécuteurs de se connecter à un fichier et à tous les types d’agrégation de compter sur le lecteur dans un autre fichier.

Notez qu'il y a un peu plus de travail à faire dans ce cas, par rapport à l'utilisation d'une classe en tant que proxy pour le module de journalisation intégré, car chaque fois que vous demandez un consignateur sur les instances de l'exécuteur, il devra être configuré. Ce ne sera probablement pas votre principal ennemi du temps lors de l'analyse de données volumineuses. ;-) 

6
Oliver W.

J'ai encore une autre approche pour résoudre le problème de journalisation dans PySpark. L'idée est la suivante:

  • Utiliser le service de gestion de journaux à distance (par exemple, Loggly, CloudWatch sur AWS, Application Insights sur Azure, etc.)
  • Configurez le module de journalisation dans les nœuds maître et de travail en utilisant la même configuration pour envoyer les journaux aux services ci-dessus.

C'est une bonne approche si vous utilisez déjà des services de cloud, car nombre d'entre eux disposent également de services de collecte et de gestion de journaux.

J'ai un exemple de compte de mots simple sur Github pour illustrer cette approche https://github.com/chhantyal/wordcount

Cette application Spark envoie des journaux à Loggly à l'aide du module standard logging à partir du pilote (nœud principal) ainsi que des exécuteurs (nœuds de travail).

0
chhantyal