PySpark从执行程序登录

时间:2016-11-25 13:31:43

标签: python apache-spark log4j pyspark

在执行程序上使用pyspark访问Spark的log4j记录器的正确方法是什么?

在驱动程序中这样做很容易,但我似乎无法理解如何访问执行程序上的日志功能,以便我可以在本地登录并让YARN收集本地日志。

有没有办法访问本地记录器?

标准的日志记录过程是不够的,因为我无法从执行程序访问spark上下文。

3 个答案:

答案 0 :(得分:20)

您不能在执行程序上使用本地log4j记录器。执行者生成的Python工作者jvms没有与java的“回调”连接,他们只接收命令。但是有一种方法可以使用标准的python日志记录从执行程序登录并通过YARN捕获它们。

在你的HDFS上放置python模块文件,它为每个python worker和proxies日志函数配置一次日志记录(命名为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()

然后在您的应用程序中导入此模块:

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

您可以在普通日志库中使用pyspark函数:

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

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

pyspark.log将在资源管理器上显示,并将在应用程序完成时收集,因此您可以稍后使用yarn logs -applicationId ....访问这些日志。 enter image description here

答案 1 :(得分:6)

请注意,Mariusz的答案会将代理返回给日志记录模块。当您的日志记录需求非常基本时,这可以工作(upvoted)。一旦您对配置多个记录器实例或使用多个处理程序这样的事情感兴趣,它就会缺乏。例如。如果你有一组更大的代码,你只想在调试时运行,solutions之一就是检查记录器实例的isEnabledFor方法,如下所示:

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

当在日志​​记录模块上调用该方法时,这会失败,就像在Mariusz的回答中一样,因为日志记录模块没有这样的属性。

解决此问题的一种方法是创建一个spark_logging.py模块,您可以在其中配置日志记录并返回Logger的新实例。下面的代码显示了一个示例,它使用dictConfig配置日志记录。它还添加了一个过滤器,以便在使用根记录器时大大减少来自所有工作节点的重复次数(过滤器示例来自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)

然后,您可以导入此模块并为logging本身设置别名:

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 __name__ == "__main__":
    log1 = logging.getLogger("driver")
    log1.info("logging from module level")
    main()

Mariusz's answer一样,可以使用资源管理器访问日志(当LOG_DIRS不在您的环境变量中时,可以转储到临时文件夹中)。 添加在此脚本顶部完成的错误处理,以便您可以在本地运行此脚本。

这种方法允许更多自由:您可以让执行程序登录到一个文件,并在另一个文件中的驱动器上进行各种聚合计数。

请注意,与使用类作为内置日志记录模块的代理相比,在这种情况下还有更多工作要做,因为每次在执行程序实例上请求记录器时,都必须是配置。在进行大数据分析时,这可能不是您的主要时间。 ;-)

答案 2 :(得分:0)

我还有另一种方法可以解决PySpark中的日志记录问题。思路如下:

  • 使用远程日志管理服务(例如Loggly,AWS上的CloudWatch,Azure上的Application Insights等)
  • 使用相同的配置在主节点和工作节点中配置日志记录模块,以将日志发送到上述服务

如果您已经在使用云服务,这是一个很好的方法,因为其中许多还具有日志收集/管理服务。

我在Github上有一个简单的单词计数示例来演示这种方法https://github.com/chhantyal/wordcount

此Spark应用程序使用标准logging模块从驱动程序(主节点)和执行程序(工作节点)向Loggly发送日志。