Python Tornado API日志轮转后多线程写入异常求助
这个问题我维护Tornado服务时也碰到过,踩过不少坑,本质是文件句柄没有被正确更新导致的——TimedRotatingFileHandler轮转时只是重命名了原文件,但部分线程持有的还是旧文件的句柄,所以会继续往重命名后的旧日志里写。
问题根源
当TimedRotatingFileHandler在0点执行轮转时,它仅完成了「重命名现有api.log为api.log.yyyy-mm-dd」+「创建新api.log」这两步,但Tornado的子线程(比如处理请求的IOLoop线程、自定义后台线程)之前已经打开了旧api.log的文件句柄,这些线程不会自动感知文件被重命名,依然会通过旧句柄写入数据,导致新日志跑到了旧文件里。
具体解决方案
1. 重写Handler强制刷新句柄
继承TimedRotatingFileHandler重写doRollover方法,在完成轮转后主动刷新所有日志记录器的文件句柄:
import logging.handlers import os class FixedTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler): def doRollover(self): # 先执行原生轮转逻辑 super().doRollover() # 遍历所有日志记录器,刷新文件句柄 for logger_name in logging.Logger.manager.loggerDict: logger = logging.getLogger(logger_name) for handler in logger.handlers: if isinstance(handler, logging.FileHandler): # 关闭旧句柄并重新打开新文件 handler.close() handler.stream = open(handler.baseFilename, handler.mode, encoding=handler.encoding)
之后在Tornado启动前的日志配置中,用这个自定义Handler替换原生的TimedRotatingFileHandler即可。
2. 确保全局共享日志实例
如果你的代码里存在子线程独立初始化日志记录器的情况,会导致部分线程没拿到更新后的Handler。建议在Tornado启动前就完成全局日志配置,所有线程统一使用logging.getLogger()获取实例,不要在子线程内重复创建Handler。
3. Linux环境下用WatchedFileHandler配合logrotate
如果服务跑在Linux系统上,logging.handlers.WatchedFileHandler是更省心的选择——它会监听文件的inode变化,当文件被重命名/替换时自动重新打开新句柄。不过它本身不做轮转,需要配合系统的logrotate工具:
- 日志配置代码:
handler = logging.handlers.WatchedFileHandler('api.log') handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logging.getLogger().addHandler(handler)
- 配置logrotate规则(比如在
/etc/logrotate.d/api_log中添加):
/path/to/your/api.log { daily rotate 30 missingok notifempty compress delaycompress create 644 root root }
这样logrotate负责每日轮转,WatchedFileHandler自动处理句柄更新,完美解决问题。
4. 禁止子线程手动操作日志文件
如果你的代码里有自定义线程直接open('api.log', 'a')写入日志(而非通过logging模块),那肯定会出现句柄失效的问题。强制要求所有日志操作都通过Python标准logging模块完成,不要手动打开日志文件。
验证方法
修改后可以手动测试:把系统时间调到23:59:59,等待一分钟触发轮转,然后发送几个请求,检查新的api.log是否有日志写入,旧的轮转文件是否不再新增内容。
内容的提问来源于stack exchange,提问作者falrus

