Сравнение производительности и возможностей протоколирования python

Я изучаю высокопроизводительные протоколирования в 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
  • Опция nonstd_manual_threading работает лучше всего, так как нет накладных расходов на модуль регистрации, но, очевидно, вы пропустите множество функций, таких как форматирование, фильтры и приятный интерфейс.
  • std_logging в одном потоке - это следующая лучшая вещь, но все же примерно в 3 раза медленнее, чем ручная нарезка nonstd.
  • Параметр std_manual_threading отправляет сообщения в очередь потокобезопасности, а в отдельном потоке использует стандартный модуль ведения журнала. Это выходит примерно на 25% выше, чем вариант 2, возможно, из-за затрат на коммутацию контекста.
  • Наконец, опция, использующая "logutils" QueueHandler, оказывается самой дорогой. Я изменил код метода logutils/queue.py _monitor для сна в течение 10 миллисекунд после обработки 500 сообщений, если в очереди осталось менее 100K сообщений. Это сокращает время выполнения от 7 секунд до 5 секунд (возможно, из-за отсутствия затрат на коммутацию контекста).

Мой вопрос: почему в модуле регистрации есть так много служебных накладных расходов и есть ли альтернативы? Как приложение, чувствительное к производительности, имеет смысл использовать модуль регистрации?

p.s.: Я профилировал различные сценарии и, похоже, создание LogRecord дорого.

Ответ 1

Пакет stdlib logging обеспечивает большую гибкость и функциональность для разработчиков/разработчиков/службы поддержки, и эта гибкость, безусловно, требует определенных затрат. Если потребность в производительности превосходит потребность в гибкости, вам нужно заняться чем-то другим. Вы предприняли шаги для оптимизации, описанной в документации? Типичный вызов регистрации занимает порядка десятков микросекунд на разумном оборудовании, что вряд ли кажется чрезмерным. Однако вход в узкие циклы редко рекомендуется, хотя бы потому, что объем создаваемой информации может занять слишком много времени.

Код для поиска абонента может быть довольно дорогим, но он необходим, если вы хотите, например, имя файла и номер строки, где был сделан вызов журнала.

QueueHandler предназначен для сценариев, где ввод-вывод регистрации занимает значительное время и не может быть выполнен внутриполосным. Например, веб-приложение, журналы которого необходимо отправить по электронной почте администраторам сайта, не может рисковать, используя SMTPHandler напрямую, поскольку рукопожатие по электронной почте может быть медленным.

Не забывайте, что переключение контекста потока в Python происходит медленно. Вы пробовали SocketHandler? Существует подходящая отправная точка в документации для отдельного процесса получателя, который выполняет фактический ввод-вывод в файл, электронную почту и т.д. Таким образом, ваш процесс выполняет только ввод-вывод через сокет, а не переключение контекста только для протоколирование. И использование доменных сокетов или UDP может быть быстрее, хотя последнее, конечно, с потерями.

Есть и другие способы оптимизации. Например, стандартные обработчики в ведении журналов блокируют emit(), чтобы обеспечить безопасность потоков - если в конкретном сценарии, находящемся под вашим контролем, отсутствует конфликт для обработчика, вы можете иметь подкласс обработчика, который не блокирует получение и снятие блокировки. И так далее.

Ответ 2

Если вы хотите, чтобы лучший ответ попытался более подробно описать вашу проблему, почему вам нужен такой огромный количество сообщений для регистрации? Регистрация была разработана для записи важной информации, особенно предупреждений и ошибок, а не каждой строки, которую вы выполняете.

Если регистрация занимает более 1% времени обработки, возможно, вы используете ее неправильно и не регистрируете ошибку.

Во-вторых, это связано с производительностью: не создавайте сообщение перед его отправкой в ​​модуль протоколирования (замените формат% params параметрами команды форматирования). Это связано с тем, что ведение журнала делает это для вас, но гораздо быстрее.

Ответ 3

Python не является действительно многопоточным в традиционном смысле. Всякий раз, когда поток выполняется, он должен иметь gil (глобальную блокировку интерпретатора). "потоки" выдают всякий раз, когда они вызывают в систему или вынуждены ждать ввода-вывода. Это позволяет потоку интерпретатора запускать другие "потоки" Python. Это приравнивается к асинхронному вводу/выводу.

Независимо от того, используется ли результат сообщения регистрации или отброшен, вся работа по оценке аргументов для сообщения регистрации выполняется. Как уже упоминалось в других ответах. Однако, что упущено (и в чем заключается многопоточная часть вашего вопроса), так это то, что хотя запись большого количества на диск может быть медленной, поскольку современные компьютеры имеют много ядер, процесс записи вывода в файл будет обработан для другое ядро, в то время как интерпретатор переходит к другому потоку Python. Операционная система завершит асинхронную запись на диск, и на запись на диск будет потрачено совсем немного времени.

Пока у интерпретатора всегда есть другой поток, на который можно переключиться, практически не будет времени на запись. Интерпретатор фактически потеряет время только в том случае, если все "потоки" Python заблокированы на вводе/выводе. Что, вероятно, имеет место, если вы действительно не забиваете свой диск.