Поддерживает ли протоколы python многопроцессорность?
Мне сказали, что запись не может использоваться в Multiprocessing. Вы должны сделать элемент управления concurrency в случае, если многопроцессорность испортила журнал.
Но я сделал несколько тестов, кажется, что нет проблем с использованием протоколирования многопроцессорности
import time
import logging
from multiprocessing import Process, current_process, pool
# setup log
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='/tmp/test.log',
filemode='w')
def func(the_time, logger):
proc = current_process()
while True:
if time.time() >= the_time:
logger.info('proc name %s id %s' % (proc.name, proc.pid))
return
if __name__ == '__main__':
the_time = time.time() + 5
for x in xrange(1, 10):
proc = Process(target=func, name=x, args=(the_time, logger))
proc.start()
Как вы можете видеть из кода.
Я намеренно позволяю записывать журнал подпроцесса в тот же момент (5 с после старта), чтобы увеличить вероятность конфликта. Но конфликта вообще нет.
Итак, мой вопрос: можем ли мы использовать протоколирование в многопроцессорности?
Почему так много сообщений говорят, что мы не можем?
Ответы
Ответ 1
Как правильно объяснил Matino: регистрация в многопроцессорной установке небезопасна, поскольку несколько процессов (которые теперь знают что-либо о других существующих) записывают в один и тот же файл, потенциально вмешиваясь друг с другом.
Теперь случается, что каждый процесс содержит дескриптор открытого файла и выполняет "добавление записи" в этот файл. Вопрос заключается в том, при каких обстоятельствах запись append является "атомарной" (то есть не может быть прервана, например, другим процессом, записывающим в тот же файл и перемежающим его вывод). Эта проблема применима к каждому языку программирования, так как в конце они будут использовать syscall для ядра. Этот ответ отвечает, при каких обстоятельствах общий файл журнала в порядке.
Это сводится к проверке размера буфера вашего буфера, на linux, который определен в /usr/include/linux/limits.h
и составляет 4096 байт. Для других ОС вы найдете здесь хороший список.
Это означает: если ваша строка журнала меньше 4'096 байт (если в Linux), то приложение безопасно, если диск подключен напрямую (т.е. нет сети между ними). Но для более подробной информации, пожалуйста, проверьте первую ссылку в моем ответе. Чтобы проверить это, вы можете сделать logger.info('proc name %s id %s %s' % (proc.name, proc.pid, str(proc.name)*5000))
с разными значениями длины. Например, с 5000 я уже перепутал строки журнала в /tmp/test.log
.
В этом вопросе уже существует немало решений, поэтому я не буду добавлять свое решение здесь.
Обновление: Колба и многопроцессорная обработка
Веб-фреймворки, такие как фляжка, будут выполняться несколькими сотрудниками, если они будут размещены с помощью uwsgi или nginx. В этом случае несколько процессов могут записываться в один файл журнала
Обработка ошибок в колбе выполняется через stdout/stderr, который затем обрабатывается веб-сервером (uwsgi, nginx и т.д.), который должен заботиться о том, чтобы журналы записывались корректно (см., например, этот фляж + пример nginx, возможно, также добавление информации о процессе, чтобы вы могли связать строки ошибок с процессами. колбы doc:
По умолчанию с Flask 0.11 ошибки записываются в журнал ваших веб-серверов автоматически. Однако предупреждений нет.
Таким образом, у вас все еще будет проблема с перемешанными файлами журнала, если вы используете warn
, и сообщение превышает размер буфера канала.
Ответ 2
Нельзя записывать в один файл из нескольких процессов.
Согласно https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
Хотя ведение журнала является потокобезопасным и протоколирование в один файл из поддерживается несколько потоков в одном процессе, вход в один файл из нескольких процессов не поддерживается, потому что нет стандартный способ сериализации доступа к одному файлу через несколько процессов в Python.
Одним из возможных решений было бы, чтобы каждый процесс записывал в свой собственный файл. Вы можете добиться этого, написав собственный обработчик, который добавляет процесс pid в конец файла:
import logging.handlers
import os
class PIDFileHandler(logging.handlers.WatchedFileHandler):
def __init__(self, filename, mode='a', encoding=None, delay=0):
filename = self._append_pid_to_filename(filename)
super(PIDFileHandler, self).__init__(filename, mode, encoding, delay)
def _append_pid_to_filename(self, filename):
pid = os.getpid()
path, extension = os.path.splitext(filename)
return '{0}-{1}{2}'.format(path, pid, extension)
Тогда вам просто нужно вызвать addHandler
:
logger = logging.getLogger('foo')
fh = PIDFileHandler('bar.log')
logger.addHandler(fh)
Ответ 3
Использовать очередь для правильной обработки concurrency, одновременно восстанавливая ошибки, подавая все в родительский процесс через канал.
from logging.handlers import RotatingFileHandler
import multiprocessing, threading, logging, sys, traceback
class MultiProcessingLog(logging.Handler):
def __init__(self, name, mode, maxsize, rotate):
logging.Handler.__init__(self)
self._handler = RotatingFileHandler(name, mode, maxsize, rotate)
self.queue = multiprocessing.Queue(-1)
t = threading.Thread(target=self.receive)
t.daemon = True
t.start()
def setFormatter(self, fmt):
logging.Handler.setFormatter(self, fmt)
self._handler.setFormatter(fmt)
def receive(self):
while True:
try:
record = self.queue.get()
self._handler.emit(record)
except (KeyboardInterrupt, SystemExit):
raise
except EOFError:
break
except:
traceback.print_exc(file=sys.stderr)
def send(self, s):
self.queue.put_nowait(s)
def _format_record(self, record):
# ensure that exc_info and args
# have been stringified. Removes any chance of
# unpickleable things inside and possibly reduces
# message size sent over the pipe
if record.args:
record.msg = record.msg % record.args
record.args = None
if record.exc_info:
dummy = self.format(record)
record.exc_info = None
return record
def emit(self, record):
try:
s = self._format_record(record)
self.send(s)
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
def close(self):
self._handler.close()
logging.Handler.close(self)
Обработчик выполняет все записи файла из родительского процесса и использует только один поток для приема сообщений, переданных из дочерних процессов