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)>
技术咨询问题
- 我能看出
propagate属性在导致循环中起到了作用,但能否有人详细解释具体的运行机制? - 如果我创建更多子进程,它们的
getLogger调用应该如何编写?创建命名/非命名日志记录器的最佳实践是什么? - 日志指南示例中,
worker_configurer将队列处理器添加到根日志记录器,这是否是因为多进程队列支持多生产者/消费者,需要在日志处理器中注册队列的两端? - 在同一个示例中,
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
相关产品推荐
相关产品推荐

