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

python logging performance comparison and options

我正在研究 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 仅次于此,但仍比非标准手动线程慢 3 倍左右。
  3. std_manual_threading 选项将消息转储到线程安全队列中,并在单独的线程中使用标准日志记录模块。这比选项 2 高出约 25%,可能是由于上下文切换成本。
  4. 最后,使用 "logutils" 的 QueueHandler 的选项是最昂贵的。我将 logutils/queue.py 的 _monitor 方法的代码调整为在处理 500 条消息后休眠 10 毫秒,只要队列中的消息少于 100K。这使运行时间从 7 秒减少到 5 秒(可能是由于避免了上下文切换成本)。

我的问题是,为什么日志记录模块的性能开销如此之大,是否有任何替代方案?作为一个性能敏感的应用程序,使用日志记录模块是否有意义?

p.s.: 我分析了不同的场景,看起来 LogRecord 创建很昂贵。

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

如果日志记录占用的处理时间超过 1%,则可能是您使用不当,这不是日志记录错误。

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

stdlib logging 包为开发人员/devops/支持人员提供了很多灵活性和功能,显然,这种灵活性是有代价的。如果对性能的需求胜过对灵活性的需求,那么您需要采取其他措施。您是否采取了 in the docs 所述的优化步骤?在合理的硬件上,典型的日志记录调用需要 几十微秒 的数量级,这似乎并不过分。但是,很少建议在紧密循环中登录,因为生成的信息量可能会花费太多时间。

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

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

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

还有其他优化方法。例如,为了线程安全,日志记录中的标准处理程序会在 emit() 左右锁定 - 如果在您控制的特定场景中没有对处理程序的争用,您可以有一个处理程序子类,它不会对锁获取进行操作,并且发布。等等。

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

无论日志消息的结果是否被使用或丢弃,所有评估日志消息参数的工作都已完成。正如其他回复中提到的。 然而,遗漏的(以及你问题的多线程部分出现的地方)是,虽然将大量数据写入磁盘可能会很慢,因为现代计算机有很多内核,但将输出写入文件的过程将被分包给另一个核心,而解释器移动到另一个 python “线程”。操作系统将完成异步磁盘写入,几乎没有时间会浪费在实际的磁盘写入上。

只要解释器​​总是有另一个线程切换到几乎没有时间会浪费在写入上。如果所有 python 个“线程”都在 I/O 上被阻塞,解释器实际上只会浪费时间。除非你真的淹没了你的磁盘,否则情况不太可能。