10

我正在尝试在多处理服务器中实现日志记录。根据文档,“不支持从多个进程记录到单个文件”。我创建了一个小程序来检查这个语句:

import logging
import multiprocessing
import os

log = logging.getLogger()


def setup_logger():
    formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')

    fileHandler = logging.FileHandler('test.log')
    fileHandler.setFormatter(formatter)

    log.setLevel(logging.DEBUG)
    log.addHandler(fileHandler)


def write_log_entries(identifier, start_event):
    start_event.wait()
    for i in range(100):
        s = ''.join(str(identifier) for k in range(30))
        log.info('[{}, {}] --- {}'.format(os.getpid(), identifier, s))


if __name__ == '__main__':
    setup_logger()
    procs = []
    start_event = multiprocessing.Event()
    for i in range(100, 300):
        p = multiprocessing.Process(target=write_log_entries, args=(i, start_event))
        procs.append(p)

    for p in procs:
        p.start()

    start_event.set()

    for p in procs:
        p.join()

执行上述代码后,我预计会在“test.log”中看到一团糟,但一切似乎都很好(当然,时间戳除外,它们不是按顺序排列的)。

任何人都可以解释为什么当多个进程同时写入日志文件时日志条目不重叠?在这种情况下 log.info() 可以被认为是原子的吗?

4

1 回答 1

10

简短的回答:内核锁定对 的单个调用write,因此只要消息很小,您就可以了,因此它们会在单个write中刷新,这样就可以write一次成功地写入所有内容。没有一般保证是这种情况,这就是为什么文档不承诺这会起作用的原因。

长答案:每次调用都会log.info刷新日志输出。这是必要的,否则您将无法在文件中看到最新的日志条目。在 Python/libc 级别,flush 被实现为对 write(2) 系统调用的调用,调用该系统调用以写出文件缓冲区的内容(如果有)。在您的情况下,缓冲区内容是您的日志消息。因此,Python 或 libc,取决于正在使用的文件,最终会调用操作系统调用,例如:

write(fd, buf, buflen);

...其中fd是日志文件的系统级文件描述符,buf是缓冲写入的内存,buflen是消息的长度。(如果您使用诸如straceLinux 之类的工具跟踪您的 Python 进程,您可以看到这些调用。)write返回成功写入的字符数,并且内核不会将这些字符与文件同一区域中的其他写入交错。如果文件以 O_APPEND 模式打开,甚至可以保证写入在文件末尾,至少在 Unix 上是这样。所以,如果buflen它很小,就像正常的日志消息一样,一切都很好。但至少有两件事会出错。

首先,不能保证所有内容buflen都会写在一个write. write可以被信号中断,fd可以指向接受固定大小写入的设备,或者您的日志消息可能足够大,以至于内核无法在单个块中接受它。通常,这不会是一个问题——正确编写write总是作为循环实现的。但在你的情况下,这将是一场灾难,因为不同的调用write 与其他进程交错。

其次,如果您的日志消息足够大以至于无法放入 stdio 缓冲区(8K 左右),那么它将在到达内核之前被分割成块。在记录回溯或将日志格式化为 XML 等详细格式时,很容易发生这种情况。

于 2012-09-02T19:25:39.403 回答