You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python多进程+QueueHandler日志循环问题及配置咨询

背景

我正在学习Python多进程相关知识,我的应用需要将所有进程的日志输出到单个文件中。我参考了相关教程以及Python官方日志指南中的多进程日志示例。

我的代码包含一个主进程,它会启动另一个进程监听日志。为了将主进程和所有子进程的日志发送到同一位置,我使用了multiprocessing.Queue。

代码

主进程代码

主进程初始化日志队列并启动监听进程:

def main():
    
    log_queue = multiprocessing.Queue() 
    logger = logging.getLogger('my_app')
    logger.addHandler(logging.handlers.QueueHandler(log_queue))
    logger.setLevel(logging.DEBUG)

 
    listener = multiprocessing.Process(target=logger_process, args=(log_queue,), daemon=True)
    listener.start()

    # First log
    logger.info(f'Number of cpus {multiprocessing.cpu_count()}')

    time.sleep(2)
    print(f'Queue size is {log_queue.qsize()}')
    ....

if __name__ == '__main__':
    main()

监听进程代码

从队列获取日志的监听进程代码:

def logger_process(queue):
    
    logger = logging.getLogger('my_app')
    f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
    h = logging.FileHandler(filename='ptest.log', mode='a')
    h.setFormatter(f)
    logger.addHandler(h)
    logger.setLevel(logging.DEBUG) # log all messages debug and up

    print('started logger process')
   
    while True:
        print(f'logger before: Queue size is {queue.qsize()}')
        msg = queue.get()
        print(f'logger after: Queue size is {queue.qsize()}')
        if msg is None:  # We send this as a sentinel to tell the listener to quit.
            pass #break # Shutdown
            traceback.print_stack()
        else:
            print(f'Log message is {msg}')
            logger.handle(msg)  # No level or filter logic applied - just do it!
            msg = None
现象

打印信息显示同一条日志重复输出,队列大小反复在0→1→0→1之间变化:

Queue size is 0
started logger process
logger before: Queue size is 1
logger after: Queue size is 0
Log message is <LogRecord: my_app, 20, ./multiproc.py, 235, "Number of cpus 16">
logger before: Queue size is 1
logger after: Queue size is 0
Log message is <LogRecord: my_app, 20, ./multiproc.py, 235, "Number of cpus 16">
logger before: Queue size is 1
logger after: Queue size is 0
Log message is <LogRecord: my_app, 20, ./multiproc.py, 235, "Number of cpus 16">
排查过程

(抱歉逐步排查的过程有点冗长,希望能帮助到其他像我一样的Python新手)

我曾怀疑是fork()启动方式复制了父进程上下文导致问题(我在Ubuntu上运行代码),但改为spawn()后问题依旧。

随后我尝试注释监听进程中的代码以缩小问题范围,发现注释掉logger.handle()后,循环停止了。

在Stack Overflow上进一步搜索后,我重新审视了日志记录器的初始化逻辑,得到以下结果:

def logger_process(queue):
    # Configure logging infra
    #logger = logging.getLogger('my_app') # <------ causing a loop
    logger = logging.getLogger()          # <------ works normally
    print(vars(logger))
    listloggers()

当监听进程使用getLogger()(即根日志记录器)时的输出:

{'filters': [], 'name': 'root', 'level': 30, 'parent': None, 'propagate': True, 'handlers': [], 'disabled': False, '_cache': {}}
<RootLogger root (DEBUG)>
     <FileHandler /home/ptest.log (NOTSET)>
+ [my_app              ] <Logger my_app (DEBUG)>
     <QueueHandler (NOTSET)>

当监听进程使用getLogger('my_app')时的输出:

{'filters': [], 'name': 'my_app', 'level': 10, 'parent': <RootLogger root (WARNING)>, 'propagate': True, 'handlers': [<QueueHandler (NOTSET)>], 'disabled': False, '_cache': {}, 'manager': <logging.Manager object at 0x7f89aa763ee0>}
<RootLogger root (WARNING)>
+ [my_app              ] <Logger my_app (DEBUG)>
     <QueueHandler (NOTSET)>
     <FileHandler /home/ptest.log (NOTSET)>
技术咨询问题
  1. 我能看出propagate属性在导致循环中起到了作用,但能否有人详细解释具体的运行机制?
  2. 如果我创建更多子进程,它们的getLogger调用应该如何编写?创建命名/非命名日志记录器的最佳实践是什么?
  3. 日志指南示例中,worker_configurer将队列处理器添加到根日志记录器,这是否是因为多进程队列支持多生产者/消费者,需要在日志处理器中注册队列的两端?
  4. 在同一个示例中,worker_process()每次日志都使用logging.getLogger(choice(LOGGERS)),这是否意味着每次发送日志都会初始化新的日志记录器?另外,有10个工作进程但只有2个LOGGERS,这是否意味着同一个日志记录器会存在于不同进程中?这在日志记录器的继承传播层级中是如何运作的?
问题解答

1. 日志循环的运行机制

核心逻辑是这样的:

  • 主进程的my_app日志器绑定了QueueHandler,所有日志会被发送到log_queue。
  • 监听进程启动后,获取了同名的my_app日志器并添加了FileHandler,但该日志器同时还保留了QueueHandler(多进程启动模式导致配置被继承或重复初始化),且propagate属性为默认的True。
  • 当监听进程调用logger.handle(msg)处理队列中的LogRecord时,my_app日志器会先通过FileHandler输出日志到文件,然后因为QueueHandler的存在,会再次将这条日志发送回log_queue。
  • 队列中新增的日志又会被监听进程的循环取出、处理,再次发送回队列,形成无限循环。
  • 改用根日志器处理时,根日志器没有绑定QueueHandler,处理完FileHandler后没有后续传播动作,循环自然终止。

2. 多进程下getLogger的最佳实践

  • 进程日志配置完全隔离:每个进程启动后独立配置日志器,不要依赖主进程的配置(多进程间日志器实例完全独立,各自有专属的日志管理器)。
  • 生产者进程(主进程+业务子进程):使用命名日志器(如my_app、my_app.worker),仅绑定QueueHandler,将所有日志发送到共享队列,不要添加本地输出handler(如FileHandler),避免资源竞争或重复输出。
  • 监听进程:使用根日志器或专门的监听日志器,绑定文件/控制台等输出handler,同时设置logger.propagate = False,彻底阻断日志传播回队列的可能。
  • 命名日志器规范:按模块或功能命名(如my_app.data、my_app.api),方便区分日志来源;尽量避免直接使用根日志器,防止不同模块日志混淆。

3. 示例中将队列处理器加到根日志器的原因

和队列注册无关,核心是简化配置:

  • 根日志器是所有日志器的父节点,将QueueHandler加到根日志器后,所有子日志器的日志会通过传播机制自动进入队列,无需给每个命名日志器单独添加QueueHandler。
  • 多进程队列本身天然支持多生产者多消费者,只要所有生产者进程拿到同一个队列实例(通过进程参数传递),就能正常发送日志,不需要额外注册操作。

4. 关于示例中logging.getLogger(choice(LOGGERS))的疑问

  • 不会重复初始化日志器:logging.getLogger(name)是单例模式,同一个进程内多次调用同一名称的getLogger,会返回同一个日志器实例,不会重复创建。
  • 同名日志器会存在于不同进程:是的,但每个进程中的日志器是完全独立的实例,进程之间没有共享。比如10个工作进程中,每个进程调用getLogger('my_app.worker')都会得到自己进程内的专属实例,它们的配置(handler、级别等)互不影响。
  • 继承传播仅在当前进程内生效:每个进程的日志器继承关系是独立的,比如进程A中my_app.worker的父节点是进程A内的my_app日志器,my_app的父节点是进程A内的根日志器,和其他进程的日志器层级毫无关联,日志传播只在当前进程的层级内发生。

内容的提问来源于stack exchange,提问作者Panda142308

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 03:41:31