python日志记录性能比较和选项

时间:2016-02-20 06:53:57

标签: python multithreading performance logging python-multithreading

我正在研究Python中的高性能日志记录,到目前为止,我们已经对python标准日志记录模块的性能感到失望 - 但似乎没有其他选择。下面是一段性能测试代码,用于测试4种不同的日志记录方式:

import logging
import timeit
import time
import datetime
from logutils.queue import QueueListener, QueueHandler
import Queue
import threading

tmpq = Queue.Queue()

def std_manual_threading():
    start = datetime.datetime.now()
    logger = logging.getLogger()
    hdlr = logging.FileHandler('std_manual.out', 'w')
    logger.addHandler(hdlr)
    logger.setLevel(logging.DEBUG)
    def logger_thread(f):
        while True:
            item = tmpq.get(0.1)
            if item == None:
                break
            logging.info(item)
    f = open('manual.out', 'w')
    lt = threading.Thread(target=logger_thread, args=(f,))
    lt.start()
    for i in range(100000):
        tmpq.put("msg:%d" % i)
    tmpq.put(None)
    lt.join()
    print datetime.datetime.now() - start

def nonstd_manual_threading():
    start = datetime.datetime.now()
    def logger_thread(f):
        while True:
            item = tmpq.get(0.1)
            if item == None:
                break
            f.write(item+"\n")
    f = open('manual.out', 'w')
    lt = threading.Thread(target=logger_thread, args=(f,))
    lt.start()
    for i in range(100000):
        tmpq.put("msg:%d" % i)
    tmpq.put(None)
    lt.join()
    print datetime.datetime.now() - start


def std_logging_queue_handler():
    start = datetime.datetime.now()
    q = Queue.Queue(-1)

    logger = logging.getLogger()
    hdlr = logging.FileHandler('qtest.out', 'w')
    ql = QueueListener(q, hdlr)


    # Create log and set handler to queue handle
    root = logging.getLogger()
    root.setLevel(logging.DEBUG) # Log level = DEBUG
    qh = QueueHandler(q)
    root.addHandler(qh)

    ql.start()

    for i in range(100000):
        logging.info("msg:%d" % i)
    ql.stop()
    print datetime.datetime.now() - start

def std_logging_single_thread():
    start = datetime.datetime.now()
    logger = logging.getLogger()
    hdlr = logging.FileHandler('test.out', 'w')
    logger.addHandler(hdlr)
    logger.setLevel(logging.DEBUG)
    for i in range(100000):
        logging.info("msg:%d" % i)
    print datetime.datetime.now() - start

if __name__ == "__main__":
    """
    Conclusion: std logging about 3 times slower so for 100K lines simple file write is ~1 sec while std
    logging ~3. If threads are introduced some overhead causes to go to ~4 and if QueueListener and events
    are used with enhancement for thread sleeping that goes to ~5 (probably because log records are being
    inserted into queue).
    """
    print "Testing"
    #std_logging_single_thread() # 3.4
    std_logging_queue_handler() # 7, 6, 7 (5 seconds with sleep optimization)
    #nonstd_manual_threading() # 1.08
    #std_manual_threading() # 4.3
  1. nonstd_manual_threading选项效果最好,因为日志模块没有开销,但显然你错过了很多功能,例如格式化程序,过滤器和漂亮的界面
  2. 单个线程中的std_logging是下一个最好的东西,但仍然比nonstd手动线程慢大约3倍。
  3. std_manual_threading选项将消息转储到线程安全队列中,并在单独的线程中使用标准日志记录模块。这比选项2高约25%,可能是由于环境转换成本。
  4. 最后,使用" logutils" QueueHandler的选项最贵。只要队列中的消息少于100K,我就会在处理500条消息后调整logutils / queue.py的_monitor方法的代码来休眠10毫秒。这使运行时间从7秒减少到5秒(可能是因为避免了上下文切换成本)。
  5. 我的问题是,为什么日志模块会有如此多的性能开销,还有其他选择吗?作为性能敏感的应用程序,使用日志记录模块甚至是有意义的吗?

    p.s。:我已经描述了不同的场景,看起来LogRecord创建很昂贵。

3 个答案:

答案 0 :(得分:7)

stdlib logging包为开发人员/ devops /支持人员提供了很多灵活性和功能,显然这种灵活性需要付出一些代价。如果对性能的需求胜过灵活性的需要,那么你需要选择别的东西。您是否采取了优化描述in the docs的步骤?典型的日志记录调用在合理的硬件上需要数十微秒,这几乎看不出来。但是,如果只是因为生成的信息量可能需要花费太多时间才能完成,那么很少需要记录紧密循环。

找到来电者的代码可能非常昂贵,但如果您需要,则需要记录调用的文件名和行号。

QueueHandler适用于日志记录I / O占用大量时间且无法在带内完成的情况。例如,需要通过电子邮件向站点管理员发送日志的Web应用程序不能直接使用SMTPHandler,因为电子邮件握手可能很慢。

不要忘记Python中的线程上下文切换很慢。你试过SocketHandler了吗?有一个合适的起始点in the docs用于单独的接收器进程,它可以对文件,电子邮件等进行实际I / O操作。因此,您的进程只进行套接字I / O而不进行仅用于日志记录的上下文切换。使用域套接字或UDP可能会更快,尽管后者当然是有损的。

还有其他优化方法。例如,日志记录中的标准处理程序会在emit()周围锁定,以确保线程安全 - 如果在您控制下的特定方案中没有对处理程序的争用,则可以使用一个处理程序子类来禁止锁定获取和发布。等等。

答案 1 :(得分:3)

如果您想要更好的答案,请尝试更详细地描述您的问题,为什么您需要这么大的问题 要记录的消息数量?记录旨在记录重要信息,尤其是警告和错误,而不是您执行的每一行。

如果日志记录占用处理时间的1%以上,可能是您错误地使用了它并且没有记录错误。

其次,与性能相关:在将消息发送到日志记录模块之前不要构建消息(用format命令params替换格式%params)。这是因为日志记录会为您执行此操作,但速度要快得多。

答案 2 :(得分:0)

在传统意义上,Python不是真正的多线程。每当线程执行时,它都必须拥有gil(全局解释器锁)。每当它们调用系统或必须等待IO时,“线程”就会屈服。这允许解释器线程运行其他python“线程”。这等同于异步I / O。

无论是否使用或丢弃日志消息的结果,评估日志消息的参数的所有工作都已完成。如其他回应中所述。 但是,遗漏的(以及您问题的多线程部分出现的地方)是,由于现代计算机具有许多核心,因此向磁盘写入大量数据可能会很慢,而将输出写入文件的过程将被移植到解释器移至另一个python“线程”时移至另一个核心。操作系统将完成异步磁盘写操作,几乎没有时间将丢失磁盘写操作。

只要解释器​​始终有另一个线程可以切换到,几乎不会浪费任何时间进行写操作。如果所有python“线程”在I / O上被阻塞,则解释器实际上只会浪费时间。除非您确实要淹没磁盘,否则可能是这种情况。