日志文件管理:文件滚动策略与日志分割归档
日志文件如果不加管理,一天就能撑爆磁盘。我见过最夸张的一次,一个 Java 应用一天写了 12GB 日志,直接把测试环境的根分区塞满了。嗯,从那以后,日志滚动和归档就成了我项目里的标配。
说白了,日志滚动就是「写到一定大小或时间,自动换一个新文件写」。归档则是把旧日志打包压缩,省空间、方便查。这两件事在 Python 里用 RotatingFileHandler 就能搞定。
RotatingFileHandler 的核心机制
Python 的 logging.handlers.RotatingFileHandler 支持两种滚动触发条件:
- 按文件大小滚动:单个日志文件达到指定字节数,就切分
- 按备份数量保留:保留最近 N 个文件,旧的自动删除
举个例子,我习惯这样配置:
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
filename='app.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
logger = logging.getLogger('my_app')
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
这段代码的意思是:每个日志文件最大 10MB,超过就切。保留最近 5 个备份文件,最旧的那个会被自动删掉。你想想看,如果日志每天写 50MB,那磁盘上最多也就保留 60MB 左右的数据,非常可控。
滚动后的文件命名规则:
原始文件始终叫 app.log。第一次滚动后,app.log 被重命名为 app.log.1,新内容写入新的 app.log。第二次滚动后,app.log.1 变成 app.log.2,app.log 变成 app.log.1,依此类推。数字越大,文件越旧。
TimedRotatingFileHandler:按时间滚动
有些场景下,按大小滚动不够用。比如合规要求日志必须按天归档,或者你想在凌晨低峰期统一切分。这时候用 TimedRotatingFileHandler:
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
filename='app.log',
when='midnight', # 每天午夜切分
interval=1,
backupCount=30, # 保留最近30天
encoding='utf-8'
)
when 参数支持多种粒度:
| 参数值 | 含义 | 典型场景 |
|---|---|---|
'S' |
每秒 | 调试、性能测试 |
'M' |
每分钟 | 短时压力测试 |
'H' |
每小时 | 高频交易日志 |
'D' |
每天 | 通用业务日志 |
'W0' |
每周一 | 周报统计日志 |
'midnight' |
每天午夜 | 合规归档 |
我的经验:生产环境我一般用 when='midnight' 配合 backupCount=30。这样每天一个文件,查某一天的日志直接看文件名就知道是哪天,不用猜。而且凌晨切分不影响业务高峰。
日志归档:压缩与清理
光滚动还不够。日志文件如果不压缩,几十个文件堆在那也挺占地方。我个人习惯在滚动后做两件事:
- 压缩旧日志:用 gzip 把已滚出的文件打包
- 定期清理:删除超过保留期限的归档文件
Python 标准库没有直接提供压缩归档的 Handler,但我们可以自己写一个简单的包装:
import gzip
import os
import shutil
from logging.handlers import RotatingFileHandler
class CompressedRotatingHandler(RotatingFileHandler):
def doRollover(self):
# 先执行父类的滚动逻辑
super().doRollover()
# 然后压缩刚滚出的文件
for i in range(self.backupCount, 0, -1):
src = f"{self.baseFilename}.{i}"
dst = f"{src}.gz"
if os.path.exists(src) and not os.path.exists(dst):
with open(src, 'rb') as f_in:
with gzip.open(dst, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(src)
这段代码在每次滚动后,把滚出的 .log.1、.log.2 等文件压缩成 .log.1.gz。压缩比通常在 5:1 到 10:1 之间,效果很明显。
我曾经踩过的坑:压缩操作本身会消耗 CPU 和磁盘 IO。如果日志量特别大(比如每秒几百条),滚动时再压缩可能导致日志写入短暂阻塞。我的建议是:把压缩放到单独的线程或定时任务里做,别在 Handler 的 doRollover 里同步执行。
日志分割与归档的完整流程
下面这张图展示了从日志写入到最终归档的完整链路:
生产环境配置建议
我在多个线上项目里总结了一套比较稳的配置参数,分享给你参考:
| 场景 | 滚动策略 | maxBytes / interval | backupCount | 归档方式 |
|---|---|---|---|---|
| 开发调试 | 按大小 | 5MB | 3 | 不压缩 |
| 低流量业务 | 按天 | midnight | 30 | gzip 压缩 |
| 高流量业务 | 按大小 + 按天 | 100MB + midnight | 7 | gzip 压缩 + 冷存储 |
| 合规审计 | 按天 | midnight | 365 | gzip 压缩 + 加密归档 |
避坑指南:我曾经在线上把 backupCount 设成 0,结果日志文件滚动后直接删除了旧文件,没有保留任何备份。排查问题时发现历史日志全没了,只能靠监控曲线猜问题。所以 backupCount 至少设成 1,除非你确定不需要回溯。
多进程环境下的滚动问题
如果你的应用是多进程的(比如 Gunicorn、uWSGI),多个进程同时写同一个日志文件,滚动时可能会出问题。Python 的 RotatingFileHandler 不是线程安全的,更不是进程安全的。
我推荐两种方案:
- 方案一:每个进程写独立的日志文件,文件名带上进程 ID 或序号。比如
app_worker_1.log、app_worker_2.log。这样各滚各的,互不干扰。 - 方案二:使用
ConcurrentRotatingFileHandler(第三方库concurrent-log-handler),它通过文件锁保证多进程安全滚动。
# 方案一示例:按进程 ID 分文件
import os
import logging
worker_id = os.getpid()
handler = RotatingFileHandler(
filename=f'app_worker_{worker_id}.log',
maxBytes=10 * 1024 * 1024,
backupCount=3
)
特别注意:多进程共用同一个日志文件时,千万不要在滚动时做「先删旧文件再重命名」的操作。两个进程同时滚动,可能一个进程把另一个进程刚滚出的文件删了。我遇到过这种问题,排查了整整一天才发现是滚动冲突。
日志归档的冷存储策略
对于需要长期保留的日志(比如合规要求保留 180 天),滚动到本地的文件不可能一直存着。我的做法是:
- 本地保留最近 7 天的日志(按天滚动,backupCount=7)
- 每天凌晨用 cron 任务把前一天的日志压缩后上传到对象存储(S3、OSS 等)
- 对象存储上按年月日建目录,比如
logs/2025/03/21/app.log.gz - 设置对象存储的生命周期策略,180 天后自动删除
这样本地磁盘压力很小,历史日志又能随时从对象存储拉回来查。嗯,这个方案我在好几个项目里都用过,稳得很。