python 日志记录是否支持多处理?

Does python logging support multiprocessing?

有人告诉我日志记录不能用于多处理。您必须进行并发控制,以防多处理弄乱日志。

不过我做了一些测试,好像在multiprocessing中使用logging没有问题

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()

从代码中可以看出

我故意让子进程在同一时刻(启动后5s)写入日志,增加冲突的几率。但是完全没有冲突。

所以我的问题是我们可以在多处理中使用日志记录吗? 为什么这么多帖子说我们不能?

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

根据https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes

Although logging is thread-safe, and logging to a single file from multiple threads in a single process is supported, logging to a single file from multiple processes is not supported, because there is no standard way to serialize access to a single file across multiple processes in 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)

正如 Matino 正确解释的那样:登录多处理设置并不安全,因为多个进程(对现有的其他进程一无所知)正在写入同一个文件,可能会相互干扰。

现在发生的情况是每个进程都持有一个打开的文件句柄并对该文件执行 "append write"。问题是在什么情况下追加写入是 "atomic" (也就是说,不能被另一个写入同一个文件并混合他的输出的进程打断)。这个问题适用于每一种编程语言,因为最终它们会对内核进行系统调用。 This answer 回答在什么情况下共享日志文件是可以的。

它归结为检查您的管道缓冲区大小,linux 在 /usr/include/linux/limits.h 中定义并且是 4096 字节。对于其他操作系统,您会发现 here 一个很好的列表。

这意味着:如果您的日志行小于 4'096 字节(如果在 Linux 上),那么追加是安全的,如果磁盘是直接连接的(即两者之间没有网络)。但有关更多详细信息,请查看我的回答中的第一个 link。要对此进行测试,您可以使用不同的长度来执行 logger.info('proc name %s id %s %s' % (proc.name, proc.pid, str(proc.name)*5000))。以 5000 为例,我已经在 /tmp/test.log.

中混淆了日志行

this question中已经有很多解决方案,所以我不会在这里添加自己的解决方案。

更新:Flask 和多处理

Web frameworks like flask will be run in multiple workers if hosted by uwsgi or nginx. In that case, multiple processes may write into one log file. Will it have problems?

flask 中的错误处理是通过 stdout/stderr 完成的,然后由网络服务器(uwsgi、nginx 等)进行处理,需要注意以正确的方式写入日志(参见例如 [this flask+nginx 例子])(http://flaviusim.com/blog/Deploying-Flask-with-nginx-uWSGI-and-Supervisor/), probably also adding process information so you can associate error lines to processes. From flasks doc:

By default as of Flask 0.11, errors are logged to your webserver’s log automatically. Warnings however are not.

因此,如果您使用 warn 并且消息超过管道缓冲区大小,您仍然会遇到混合日志文件的问题。

使用队列正确处理并发,同时通过管道将所有内容提供给父进程,从而从错误中恢复。

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)

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