Модуль ведения журнала Python — многопроцессное ведение журнала

Python

1. Описание проблемы2. Анализ2.1 Модуль логирования реализует откат лога2.2 Безопасный вывод мультипроцессных журналов в одну и ту же файловую схему3. Решения3.1 Использование пакета ConcurrentRotatingFileHandler3.2 пакет concurrent-log-handler3.3 Блокировка вывода журнала3.4 Переопределение класса FileHandler3.5 За регистрацию событий отвечает отдельный процесс3.6 Схема логирования.SocketHandler4. Ссылки

1. Описание проблемы

проект, использованиеRotatingFileHandlerРазделите журнал в соответствии с размером файла журнала. файл настроекMaxBytesза1GB,backupCountразмер 5.

После проверки обнаруживается, что размер лог-файла меньше10MB, и время записи каждого файла журнала отката также относительно близко.

2. Анализ

Если лог-файл слишком мал, то предполагается, что проблема с кодом или содержимое файла утеряно, время записи лога близко к предположению, что это проблема одновременной записи.

После осмотра проблем с кодом нет, и эта причина исключена. Учитывайте текущее использованиеgunicornБольшинство программ запуска с несколькими процессами вызваны одновременной записью нескольких процессов в один и тот же файл, что приводит к потере файла журнала.

loggingМодули потокобезопасны, но не процессобезопасны.

Как решить эту проблему? сначала пройти черезPythonизloggingКонкретный метод реализации модуля обработки отката журнала.

2.1 Модуль логирования реализует откат лога

loggingсерединаRotatingFileHandlerкласс иTimedRotatingFileHandlerКласс реализует разделение файлов в соответствии с размером файла журнала и временем файла журнала, которые унаследованы отBaseRotatingHandlerсвоего рода.

BaseRotatingHandlerЗапуск и выполнение сегментации файла реализовано в классе, конкретный процесс выглядит следующим образом:

 1def emit(self, record):
2    """
3        Emit a record.
4        Output the record to the file, catering for rollover as described
5        in doRollover().
6        """
7    try:
8        if self.shouldRollover(record):
9            self.doRollover()
10        logging.FileHandler.emit(self, record)
11    except Exception:
12        self.handleError(record)

конкретный процесс исполненияshouldRollover(record)иdoRollover()функция находится вRotatingFileHandlerкласс иTimedRotatingFileHandlerреализованы в классе.

отRotatingFileHandlerкласс, например,doRollover()Поток функций выглядит следующим образом:

 1def doRollover(self):
2    if self.stream:
3        self.stream.close()
4        self.stream = None
5    if self.backupCount > 0:
6        for i in range(self.backupCount - 1, 0, -1): # 从backupCount,依次到1
7            sfn = self.rotation_filename("%s.%d" % (self.baseFilename, i))
8            dfn = self.rotation_filename("%s.%d" % (self.baseFilename,
9                                                        i + 1))
10            if os.path.exists(sfn):
11                if os.path.exists(dfn):
12                    os.remove(dfn)
13                os.rename(sfn, dfn) # 实现将xx.log.i->xx.log.i+1
14        dfn = self.rotation_filename(self.baseFilename + ".1")
15        # ---------start-----------
16        if os.path.exists(dfn): # 判断如果xx.log.1存在,则删除xx.log.1
17            os.remove(dfn)
18        self.rotate(self.baseFilename, dfn) # 将xx.log->xx.log.1
19        # ----------end------------
20    if not self.delay:
21        self.stream = self._open() # 执行新的xx.log

Анализируя вышеописанный процесс, все шаги таковы:

  1. Обрабатываемый в данный момент файл журнала называетсяself.baseFilename, Значениеself.baseFilename = os.path.abspath(filename)- это абсолютный путь к установленному файлу журнала, при условии, чтоbaseFilenameзаerror.log.
  2. При откате файлаerror.log.iпереименован вerror.log.i+1.
  3. судитьerror.log.1Существует ли он, если он существует, удалите его и сохраните текущий файл журнала.error.logпереименован вerror.log.1.
  4. self.streamперенаправить на новыйerror.logдокумент.

Когда программа запускает несколько процессов, каждый процесс выполняетсяdoRolloverпроцесс, если более одного процесса входит в критическую секцию, это вызоветdfnМногократное удаление и другие запутанные операции.

2.2 Безопасный вывод мультипроцессных журналов в одну и ту же файловую схему

Соответствующий обходной путь:

  1. Отправьте лог тому же процессу, который отвечает за вывод в файл (используяQueueиQueueHandlerотправить все события журнала в один процесс)
  2. Заблокируйте вывод журнала, и каждый процесс сначала получает блокировку при выполнении вывода журнала (используяLockкласс для сериализации доступа к файлу процесса)
  3. все процессы регистрируются вSocketHandler, а затем используйте отдельный процесс, который реализует сервер сокетов для чтения из сокета и записи в файл (Pythonуказано в мануале)

3. Решения

3.1 Использование пакета ConcurrentRotatingFileHandler

Этот метод относится к блокировочной схеме.

ConcurrentLogHandlerЖурналы можно безопасно записывать в один и тот же файл в многопроцессорной среде, а файлы журналов можно разделять, когда файл журнала достигает определенного размера (Поддержка разделения по размеру файла). ноConcurrentLogHandlerРазделение файлов журнала по времени не поддерживается.

ConcurrentLogHandlerМодуль использует блокировку файлов, поэтому несколько процессов одновременно регистрируются в одном файле, не повреждая события журнала. Этот модуль обеспечиваетRotatingFileHandlerАналогичная схема ротации файлов.

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

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

ConcurrentRotatingFileLogHandlerclass — это стандартный обработчик журнала Python.RotatingFileHandlerпрямая замена.

Этот пакет в комплектеportalockerдля обработки блокировки файлов. из-за использованияportalockerмодуль, который в настоящее время поддерживает только“nt”и“posix”Платформа.

Установить:

1pip install ConcurrentLogHandler

Этот модуль поддерживаетPython2.6и более поздние версии. Текущая последняя версия0.9.1

ConcurrentLogHandlerкак пользоваться и прочееhandlerкласс, соответствующийRotatingFileHandlerиспользуется таким же образом.

Функция и параметры инициализации:

1class ConcurrentRotatingFileHandler(BaseRotatingHandler):
2    """
3    Handler for logging to a set of files, which switches from one file to the
4    next when the current file reaches a certain size. Multiple processes can
5    write to the log file concurrently, but this may mean that the file will
6    exceed the given size.
7    """
8    def __init__(self, filename, mode='a', maxBytes=0, backupCount=0,
9                 encoding=None, debug=True, delay=0):

Параметры имеют тот же смыслPythonвстроенныйRotatingFileHandlerКласс тот же, пожалуйста, обратитесь к предыдущему сообщению в блоге для деталей. также унаследовано отBaseRotatingHandlerсвоего рода.

Простой пример:

1import logging
2from cloghandler import ConcurrentRotatingFileHandler
3
4logger = logging.getLogger()
5rotateHandler = ConcurrentRotatingFileHandler('./logs/my_logfile.log', "a", 1024*1024, 5)
6logger.addHandler(rotateHandler)
7logger.setLevel(logging.DEBUG)
8
9logger.info('This is a info message.')

соответствовать нетConcurrentRotatingFileHandlerВ случае пакетов добавьте резервное использованиеRotatingFileHandlerкод:

1try:
2    from cloghandler import ConcurrentRotatingFileHandler as RFHandler
3except ImportError:
4    from warning import warn
5    warn('ConcurrentRotatingFileHandler package not installed, Using builtin log handler')
6    from logging.handlers import RotatingFileHandler as RFHandler

После запуска можно обнаружить, что.lockфайл, безопасно записывайте файлы журнала, блокируя.

Примечания: Библиотека не обновлялась с 2013 года. Если есть какие-либо проблемы, вы можете использовать ее.3.2в разделеconcurrent-log-handlerупаковка.


не используется отдельноpythonПри написании сценария обратите внимание на то, как вы его используете:

 1# 不建议使用方式
2from cloghandler import ConcurrentRotatingFileHandler
3
4.......
5'handlers':{
6        "error_file": {
7            "class": "ConcurrentRotatingFileHandler",
8            "maxBytes": 100*1024*1024,# 日志的大小
9            "backupCount": 3,
10# 建议写完整
11import cloghandler
12'handlers':{
13        "error_file": {
14            "class": "cloghandler.ConcurrentRotatingFileHandler",
15            "maxBytes": 100*1024*1024,# 日志的大小
16            "backupCount": 3,

В противном случае появится следующая ошибка:

1Error: Unable to configure handler 'access_file': Cannot resolve 'ConcurrentRotatingFileHandler': No module named 'ConcurrentRotatingFileHandler'

3.2 пакет concurrent-log-handler

Этот модуль также предоставляет дополнительные обработчики журналов для стандартного программного обеспечения Python для ведения журналов. То есть события журнала записываются в файл журнала.Когда файл достигает определенного размера, файл журнала будет ротироваться по очереди, и несколько процессов могут безопасно записывать в один и тот же файл журнала, а также его можно сжимать (открывать) .WindowsиPOSIXсистемы поддерживаются.

это можно рассматривать как старую версиюcloghandlerпрямая замена основногоcloghandlerизменить наconcurrent_log_handler.

Его характеристики и описаниеcloghandlerпоследовательный, конкретный3.1подраздел.

Установить:

1pip install concurrent-log-handler

При установке из исходников выполните следующую команду:

1python setup.py install

Пример использования:

1import logging
2from concurrent_log_handler import ConcurrentRotatingFileHandler
3
4logger = logging.getLogger()
5rotateHandler = ConcurrentRotatingFileHandler('./logs/mylogfile.log', 'a', 512*1024, 5)
6logger.addHandler(rotateHandler)
7logger.setLevel(logging.DEBUG)
8
9logger.info('This is a info message.')

Точно так же, если вы хотите распространять код, не уверены, что все установленоconcurrent_log_handlerупаковать, сделатьPythonМожно легко вернуться к встроенномуRotatingFileHandler. Вот пример:

 1import logging
2try:
3    from concurrent_log_handler import ConcurrentRotatingFileHandler as RFHandler
4except ImportError:
5    # 下面两行可选
6    from warnings import warn
7    warn('concurrent_log_handler package not installed. Using builtin log handler')
8    from logging.handlers import RotatingFileHandler as RFHandler
9
10logger = logging.getLogger()
11rotateHandler = RFHandler('./logs/mylogfile.log', 'a', 1024*1024, 5)
12logger.addHandler(rotateHandler)

Точно так же рекомендуется импортировать напрямуюconcurrent_log_handler,использоватьconcurrent_log_handler.ConcurrentRotatingFileHandlerСпособ.

3.3 Блокировка вывода журнала

TimedRotatingFileHandlerсвоего родаdoRolloverОсновная часть функции выглядит следующим образом:

 1def doRollover(self):
2    ....
3    dfn = self.rotation_filename(self.baseFilename + "." +
4                                     time.strftime(self.suffix, timeTuple))
5    # -------begin-------
6    if os.path.exists(dfn): # 判断如果存在dfn,则删除
7            os.remove(dfn)
8    self.rotate(self.baseFilename, dfn) # 将当前日志文件重命名为dfn
9    # --------end--------
10    if self.backupCount > 0:
11        for s in self.getFilesToDelete():
12            os.remove(s)
13    if not self.delay:
14        self.stream = self._open()
15    ....

Идеи модификации:

судитьdfnСуществует ли уже файл, если да, значит, он былrenameпройден; если нет, разрешен только один процессrename, другим процессам нужно подождать.

Создайте новый класс, который наследуется отTimeRotatingFileHandler,ИсправлятьdoRolloverфункции, просто обработайте закомментированную часть кода выше. следующее:

 1class MPTimeRotatingFileHandler(TimeRotatingFileHandler):
2    def doRollover(self):
3        ....
4        dfn = self.rotation_filename(self.baseFilename + "." +
5                                     time.strftime(self.suffix, timeTuple))
6        # ----modify start----
7        if not os.path.exists(dfn):
8            f = open(self.baseFilename, 'a')
9            fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
10            if os.path.exists(self.baseFilename): # 判断baseFilename是否存在
11                self.rotate(self.baseFilename, dfn)
12        # ----modify end-----
13        if self.backupCount > 0:
14        for s in self.getFilesToDelete():
15            os.remove(s)
16        ....

3.4 Переопределение класса FileHandler

logging.handlers.pyОтношения наследования различных типов показаны на следующем рисунке:

image-20200106151225654
image-20200106151225654

TimeRotatingFileHandlerКласс наследуется от этого класса, вFileHandlerДобавьте некоторую обработку в класс.

Для получения дополнительной информации, пожалуйста, обратитесь к следующим сообщениям в блоге:

  1. модуль ведения журнала python и многопроцессное ведение журнала | блог doudou0o

  2. Многопроцессорность Python решает проблему путаницы в логах — блог qq_20690231 — блог CSDN


существуетPythonВ официальном мануале предусмотрен метод логирования в один файл в нескольких процессах.

loggingявляется потокобезопасным и поддерживает регистрацию нескольких потоков в одном процессе в один файл. Ведение журнала из нескольких процессов в один файл не поддерживается, поскольку вPythonСтандартной схемы сериализации доступа к одному файлу для нескольких процессов не существует.

Существует несколько вариантов записи нескольких внутрипроцессных журналов в один файл:

  1. все процессы регистрируются вSocketHandler, а затем используйте отдельный процесс, который реализует сервер сокетов для чтения из сокета и записи в файл.
  2. использоватьQueueиQueueHandlerОтправляйте все события журнала в один процесс в многопроцессном приложении.

3.5 За регистрацию событий отвечает отдельный процесс

Один процесс-слушатель отвечает за прослушивание событий журнала других процессов и ведение журнала в соответствии со своей собственной конфигурацией.

Пример:

 1import logging
2import logging.handlers
3import multiprocessing
4
5from random import choice, random
6import time
7
8def listener_configurer():
9    root = logging.getLogger()
10    h = logging.handlers.RotatingFileHandler('test.log', 'a', 300,10) # rotate file设置的很小,以便于查看结果
11    f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
12    h.setFormatter(f)
13    root.addHandler(h)
14
15def listenser_process(queue, configurer):
16    configurer()
17    while True:
18        try:
19            record = queue.get()
20            if record is None:
21                break
22            logger = logging.getLogger(record.name)
23            logger.handle(record)
24        except Exception:
25            import sys, traceback
26            print('Whoops! Problem:', file=sys.stderr)
27            trackback.print_exc(file=sys.stderr)
28
29LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING,
30          logging.ERROR, logging.CRITICAL]
31
32LOGGERS = ['a.b.c', 'd.e.f']
33
34MESSAGES = [
35    'Random message #1',
36    'Random message #2',
37    'Random message #3',
38]
39
40def worker_configurer(queue):
41    h = logging.handlers.QueueHandler(queue)
42    root = logging.getLogger()
43    root.addHandler(h)
44    root.setLevel(logging.DEBUG)
45
46# 该循环仅记录10个事件,这些事件具有随机的介入延迟,然后终止
47def worker_process(queue, configurer):
48    configurer(queue)
49    name = multiprocessing.current_process().name
50    print('Worker started:%s'%name)
51    for i in range(10):
52        time.sleep(random())
53        logger = logging.getLogger(choice(LOGGERS))
54        level = choice(LEVELS)
55        message = choice(MESSAGES)
56        logger.log(level, message)
57# 创建队列,创建并启动监听器,创建十个工作进程并启动它们,等待它们完成,然后将None发送到队列以通知监听器完成
58def main():
59    queue = multiprocessing.Queue(-1)
60    listener = multiprocessing.Process(target=listener_process,
61                                      args=(queue, listener_configurer))
62    listener.start()
63    workers = []
64    for i in range(10):
65        worker = multiprocessing.Process(target=worker_process,
66                                        args=(queue, listener_configurer))
67        workers.append(worker)
68        worker.start()
69    for w in workers:
70        w.join()
71    queue.put_nowait(None)
72    listener.join()
73
74if __name__ == '__main__':
75    main()

Используйте отдельный поток в основном процессе для ведения журнала

В следующем фрагменте кода показано, как использовать определенную конфигурацию ведения журнала, напримерfooРегистратор использует специальный обработчик, которыйfooВсе события в подсистеме записываются в файлmplog-foo.log. Соответствующая конфигурация будет использоваться непосредственно в механизме ведения журнала главного процесса (даже для событий журнала, генерируемых рабочими процессами).

 1import logging
2import logging.config
3import logging.handlers
4from multiprocessing import Process, Queue
5import random
6import threading
7import time
8
9def logger_thread(q):
10    while True:
11        record = q.get()
12        if record is None:
13            break
14        logger = logging.getLogger(record.name)
15        logger.handle(record)
16
17def worker_process(q):
18    qh = logging.handlers.QueueHandler(q)
19    root = logging.getLogger()
20    root.setLevel(logging.DEBUG)
21    root.addHandler(qh)
22    levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
23              logging.CRITICAL]
24    loggers = ['foo', 'foo.bar', 'foo.bar.baz', 'spam', 'spam.ham', 'spam.ham.eggs']
25
26    for i in range(100):
27        lv1=l = random.choice(levles)
28        logger = logging.getLogger(random.choice(loggers))
29        logger.log(lvl, 'Message no. %d', i)
30
31for __name__ == '__main__':
32    q = Queue()
33    d = {
34        'version': 1,
35        'formatters': {
36            'detailed': {
37                'class': 'logging.Formatter',
38                'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
39            }
40        },
41        'handlers': {
42            'console': {
43                'class': 'logging.StreamHandler',
44                'level': 'INFO',
45            },
46            'file': {
47                'class': 'logging.FileHandler',
48                'filename': 'mplog.log',
49                'mode': 'w',
50                'formatter': 'detailed',
51            },
52            'foofile': {
53                'class': 'logging.FileHandler',
54                'filename': 'mplog-foo.log',
55                'mode': 'w',
56                'formatter': 'detailed',
57            },
58            'errors': {
59                'class': 'logging.FileHandler',
60                'filename': 'mplog-errors.log',
61                'mode': 'w',
62                'level': 'ERROR',
63                'formatter': 'detailed',
64            },
65        },
66        'loggers': {
67            'foo': {
68                'handlers': ['foofile']
69            }
70        },
71        'root': {
72            'level': 'DEBUG',
73            'handlers': ['console', 'file', 'errors']
74        },
75    }
76    workers = []
77    for i in range(5):
78        wp = Process(target=worker_process, name='worker %d'%(i+1), args=(q,))
79        workers.append(wp)
80        wp.start()
81    logging.config.dictConfig(d)
82    lp = threading.Thread(target=logger_thread, args=(q,))
83    lp.start()
84
85    for wp in workers:
86        wp.join()
87    q.put(None)
88    lp.join()

3.6 Схема логирования.SocketHandler

Конкретное использование этого решения описано в следующей записи блога. Конкретную реализацию см. в следующем блоге:

В Python журналирование печатает журналы в многопроцессорной среде — VictoKu — Blog Park

4. Ссылки

  1. Напишите совместимый с несколькими процессами TimedRotatingFileHandler на Python