Python日志记录是否支持多进程?

35

我被告知在Multiprocessing中无法使用日志记录。您必须进行并发控制,以防止多处理程序干扰日志记录。

但是我进行了一些测试,似乎在Multiprocessing中使用日志记录没有问题。

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秒钟内同时写日志,以增加冲突的可能性。但是根本没有任何冲突。

所以我的问题是,我们能在多进程中使用logging吗?为什么有那么多帖子说我们不能?

4个回答

34

正如Matino正确解释的那样:在多进程设置中登录不安全,因为多个进程(彼此之间不知道其他进程的存在)正在写入同一个文件,可能会相互干扰。

现在的情况是每个进程都持有一个打开的文件句柄,并对该文件进行“追加写入”。问题是在什么情况下追加写入是“原子性”的(即,不能被其他进程写入相同的文件并混合输出所打断)。这个问题适用于每一种编程语言,因为最终它们都会对内核进行系统调用。这个答案回答了共享日志文件安全的条件。

关键是要检查管道缓冲区大小,在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中混合了日志行。

这个问题中,已经有相当多的解决方案了,所以我不会在这里添加自己的解决方案。

更新:Flask和多进程

如果由uwsgi或nginx托管,则类似flask的Web框架将在多个工作者中运行。 在这种情况下,多个进程可能会写入一个日志文件。 它会有问题吗?

Flask中的错误处理通过stdout / stderr完成,然后由Web服务器(uwsgi,nginx等)捕获,需要确保日志以正确的方式编写(例如此flask + nginx示例),还可以添加进程信息,以便您将错误行与进程关联。根据flasks doc的说法:

默认情况下,自Flask 0.11以来,错误会自动记录到您的Web服务器日志中。但警告不会。

因此,如果使用warn且消息超过管道缓冲区大小,则仍然存在混杂日志文件的问题。


还有一个问题。像Flask这样的Web框架如果由uWSGI或Nginx托管,将在多个工作进程中运行。在这种情况下,多个进程可能会写入同一个日志文件。这会有问题吗? - Kramer Li
@KramerLi:我在我的回答中新增了一个部分来回答你的问题。 - hansaplast

25

从多个进程向单个文件写入数据是不安全的。

根据https://docs.python.org/zh-cn/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)

嗯...如果我能让它工作,我会很感激,但是不确定...记录器应该在哪里定义(在哪个范围内),以及如何将其传递给各个子进程? - Peter Franek

3
使用队列来正确处理并发,同时通过将所有内容通过管道提供给父进程以从错误中恢复。
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)

处理程序由父进程完成所有文件写入,并使用一个线程接收从子进程传递的消息。

1
能够看到这个类的使用示例也是很好的。 - Liquidgenius
是的,您能否提供一个例子? - amralieg
1
这个答案似乎是从这个答案复制粘贴而来的。请注意,这在Windows上不起作用。 - Niko Pasanen

2

QueueHandler 是 Python 3.2+ 中自带的,可以安全地处理多进程日志。

Python 文档提供了两个完整的示例:从多个进程记录到单个文件

对于使用 Python < 3.2 的用户,只需从https://gist.github.com/vsajip/591589复制QueueHandler到你自己的代码中,或者导入logutils

每个进程(包括父进程)将其日志放在 Queue 中,然后一个listener线程或进程(每个示例都提供了一个)将它们全部捡起来并写入文件 - 没有损坏或混淆的风险。

注意:这个问题基本上是How should I log while using multiprocessing in Python?的重复,因此我复制了我的答案,因为我相信它目前是最好的解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接