是,多线程下 rotatingfilehandler 在 rollover 边界处大概率丢失或错乱日志,因 dorollover 缺乏原子锁保护;修复方案是继承并用实例级 threading.lock 串行化 dorollover。

多线程下直接用 RotatingFileHandler 会丢日志吗
会,但不是“一定丢”,而是**在 rollover 边界处大概率丢失或错乱**。原因在于 RotatingFileHandler.doRollover() 内部先 rename 原文件、再 open 新文件,这两个操作之间没有原子锁保护。当多个线程几乎同时触发 rollover(比如日志量突增、maxBytes 设得过小),就可能出现:一个线程刚 rename 完,另一个线程又试图 rename 同一文件,导致 OSError 或覆盖失败;或者新文件被多个线程同时 write,内容交错。
为什么 logging 模块本身线程安全却仍出问题
logging 的线程安全仅保证单次 logger.info() 调用内部不被其他线程打断——即 Logger 和 Handler.emit() 入口有锁。但 RotatingFileHandler.doRollover() 是个复合操作:检查大小 → rename → open → write,它内部的文件系统调用(os.rename()、open())本身不带跨线程同步语义。所以锁只包住了 emit 的开头和结尾,没包住 rollover 的全过程。
最简可靠的修复方式:用 threading.Lock 包住 doRollover
不需要重写整个 Handler,只需继承并重载 doRollover(),用实例级 threading.Lock 串行化轮转动作。注意锁对象必须是 Handler 实例属性,不能是类属性或全局变量。
import logging
from logging.handlers import RotatingFileHandler
import threading
class SafeRotatingFileHandler(RotatingFileHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._rollover_lock = threading.Lock()
def doRollover(self):
with self._rollover_lock:
super().doRollover()
使用时照常配置:
handler = SafeRotatingFileHandler('app.log', maxBytes=10_000_000, backupCount=5)
logger.addHandler(handler)
- 这个方案兼容所有 Python 版本,不依赖 fcntl 或 multiprocessing
- 锁粒度刚好卡在 rollover 关键区,不影响正常日志写入的并发性
- backupCount 大于 0 时,旧日志归档也受保护,不会出现 “xxx.log.1” 被多个线程反复 rename
别踩的坑:Lock 加在 emit 里 vs 加在 doRollover 里
网上有些实现把 threading.Lock 加在 emit() 全局入口,这会导致所有日志写入串行化,严重拖慢性能,尤其在高吞吐场景。而真正需要互斥的只有 doRollover() 这几十毫秒的操作。另外,不要用 logging.handlers.QueueHandler 来“绕开”这个问题——它只是把日志推到队列,最终消费端仍要调用 RotatingFileHandler.emit(),轮转竞争依然存在。
真正容易被忽略的是:如果你用了多个 SafeRotatingFileHandler 实例指向同一个 baseFilename(比如不同 logger 各配一个 handler 写同一文件),那每个实例都有自己的 _rollover_lock,还是可能冲突。这种场景必须统一用一个 handler 实例,或改用进程安全方案(如 concurrent_log_handler 库)。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











