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

时间:2017-12-25 12:55:52

标签: python python-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()

从代码中可以看出。

我故意让子进程在同一时刻(启动后5s)写日志以增加冲突的可能性。但是根本没有冲突。

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

3 个答案:

答案 0 :(得分:19)

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

根据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)

答案 1 :(得分:16)

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

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

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

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

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

更新:Flask和多处理

  

如果由uwsgi或nginx托管,那么像flask这样的Web框架将在多个worker中运行。在这种情况下,多个进程可以写入一个日志文件。会有问题吗?

烧瓶中的错误处理是通过stdout / stderr完成的,然后由网络服务器(uwsgi,nginx等)完成,这需要注意以正确的方式写入日志(参见例如[此烧瓶+ nginx示例] ])(http://flaviusim.com/blog/Deploying-Flask-with-nginx-uWSGI-and-Supervisor/),可能还添加了流程信息,因此您可以将错误行与流程相关联。来自flasks doc

  

默认情况下,从Flask 0.11开始,错误会自动记录到您的网络服务器日志中。但警告不是。

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

答案 2 :(得分:1)

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

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)

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