TimedRotatingFileHandler 多进程下必然出错,因其无跨进程同步能力;各进程独立执行 doRollover 导致 rename/remove 冲突,引发 OSError 或日志丢失;唯一健壮方案是 QueueHandler + QueueListener 由主进程统一轮转。

TimedRotatingFileHandler 在多进程下直接使用必然出错,不是配置问题,是设计机制冲突——它没有跨进程同步能力。
为什么 TimedRotatingFileHandler 多进程下会丢日志或报 OSError
每个进程独立调用 doRollover(),执行顺序不可控,典型错误链路如下:
- 进程 A 检查
app.log存在 → 关闭流 → 重命名为app.log.2026-09-08 - 进程 B 同时检查到
app.log存在 → 关闭流(此时 A 已关)→os.rename(app.log, ...)失败,抛OSError: [Errno 2] No such file or directory - 或更糟:B 先删掉
app.log.2026-09-08(因认为该归档文件已存在),A 的归档被清除
根本原因:文件系统操作(os.rename、os.remove)不是原子的,且无跨进程协调。即使加 multiprocessing.Lock,也仅能串行化写入,无法保证 doRollover 整个流程的原子性(比如 rename 前另一进程已创建新文件)。
用 multiprocessing.Queue + QueueListener 是唯一健壮方案
它把“旋转”和“落盘”收归主进程单一线程,子进程只发消息,彻底规避文件竞争。
- 主进程中创建
multiprocessing.Queue(),再用它初始化QueueListener,绑定一个TimedRotatingFileHandler(注意:必须是主进程创建的 handler 实例) - 调用
queue_listener.start()—— 这是后台线程,不是进程,别误用Process - 每个子进程里:
logging.getLogger().handlers.clear(),然后添加QueueHandler(queue) - 子进程调用
logging.info()时,LogRecord自动序列化进队列;监听端反序列化后交由TimedRotatingFileHandler.emit()执行,此时所有旋转逻辑都在单一线程内完成 - 主进程退出前必须调用
queue_listener.stop(),否则子进程可能卡在queue.put()上
别碰 filelock / fcntl 加锁的 TimedRotatingFileHandler
网上很多自定义 emit() 加锁的方案看似简单,实际隐患极深:
-
fcntl.flock在 Windows 下完全不可用;threading.Lock在多进程下无效,必须用multiprocessing.Lock - 锁只能保护写入段,但
doRollover中的os.rename和os.remove仍可能被其他进程干扰——锁的粒度不够 -
TimedRotatingFileHandler的轮转逻辑依赖self.stream状态,多进程共享同一 handler 实例会导致状态混乱,哪怕加锁也无法修复 - 一旦某个子进程崩溃未释放锁,后续所有进程永久阻塞,运维上几乎无法定位
这类方案只适合临时调试、进程数 ≤ 2、日志量极低的场景,生产环境请直接放弃。
如果非得用按时间切割,又不想改架构,就分文件
最务实的降级方案:放弃“同一个文件”,改为每个进程写独立带 PID 的日志,再靠外部工具聚合。
- 子进程中配置
TimedRotatingFileHandler(filename=f"worker_{os.getpid()}.log", when="midnight") - 必须在子进程内重新调用
logging.basicConfig()或显式addHandler(),不能复用父进程 logger - 用
logrotate(Linux)或newsyslog(macOS)统一管理这些文件:按时间切割、压缩、过期清理,完全绕过 Python 层并发问题 - 查日志时用
grep -h "ERROR" worker_*.log | sort -k1,2补时间排序,或用journalctl(若用 systemd)替代
真正难的不是让日志“看起来像一个文件”,而是让排查“不比单进程慢”。分文件 + 外部轮转,是多数中大型服务的实际选择——它不优雅,但稳定、可观察、易运维。


















