做服务端开发的对日志都有一种又爱又恨的复杂感情,尤其是当“排障时发现日志文件已经涨到 4GB”或者“按日期归档的文件白白占了一整块磁盘”成为家常便饭,你就知道光靠默认配置扛不住真实的运行压力。这个 Spring Boot 项目上线后运行了大概两个月,日志把一块 20G 的磁盘塞到只剩百分之几,我决定给 logback 配上“按大小切割+按日期归档”双维度滚动,结果配置上去之后发现:文件没有按大小切、归档互相覆盖、甚至控制台警告一堆。折腾了几天,把 logback 滚动机制的来龙去脉摸了个底朝天。这篇就把我踩过的坑和最终验证可行的方案一起写出来。
日志滚动分割这件事,表面上看只是写一段 XML 配置,实际背后藏着 logback 的两套机制,理解机制之后再配置,才不会出现“明明写了 maxFileSize 却不生效”这种问题。
1. 理解日志滚动机制:你要切的到底是什么
1.1 为什么“按日期+按大小”双维度滚动是刚需
先说我为什么坚持要用双维度滚动,而不是随便选一种。单按日期滚动,配置最简单,但遇到业务高峰,一天产生几个 GB 的日志,第二天运维打电话说磁盘快满了,你去翻日志时一个文件就有好几 GB,grep 一次要等半分钟,排障效率非常低。单按大小滚动,文件虽然被切成了 100MB 一个,但归档文件没有明确的日期信息,你想查“上周三下午那段时间的报错”,得凭感觉打开一堆文件翻,同样难受。
双维度滚动的意义在于:目录里的归档文件既能按日期组织,又能把单文件控制在合理大小。比如某个服务一天产生 2GB 日志,配成“按天归档、每 100MB 切一个文件”,当天就有 20 个带序号的归档文件,文件名形如 app.2025-03-11.0.log.gz,想查哪个时间段的日志,先按日期定位目录,再按序号查,非常直观。这是我在生产环境验证过的做法,对排障体验的提升是实打实的。
1.2 三个关键角色:Appender、RollingPolicy、TriggeringPolicy
在动手配置之前,有必要把 logback 里的三个角色理清楚。写日志的是RollingFileAppender,它负责把日志写到当前文件;决定“滚动成什么文件名、保留多少份”的是RollingPolicy;决定“什么时候触发滚动”的是TriggeringPolicy。
而SizeAndTimeBasedRollingPolicy这个类比较特殊,它在 logback 1.1.0 版本之后被官方引入,把日期滚动策略和大小触发策略合并成了一个类,也就是我们这次的主角。它的原理说起来不复杂:正常情况下日志写在当前文件里,比如 app.log;当时间跨天或者当前文件达到maxFileSize时,logback 先把当前文件按fileNamePattern重命名成归档文件,比如 app.2025-03-11.0.log.gz,然后新建一个空的 app.log 继续写。整个过程几乎是原子操作,应用侧无感知。
搞清楚这个流程之后,你再去审视自己的配置文件,就会发现很多“失效”问题其实是某个环节的配置和另外一个环节打架了。
2. 症状自查:你的日志到底卡在哪一步
2.1 症状一:文件一直涨,永远不切分
最典型的现象是 log 文件从 10MB 涨到 1GB,也不见目录里出现归档文件。这种情况下我通常先检查有没有把触发策略写进去。很多人从网上复制的老配置长这样:
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy>这个写法其实是老版本的经典用法,在部分 logback 版本里有效。但很容易出现两个结果:一是 logback 版本较新时,内部已经迁移到SizeAndTimeBasedRollingPolicy,老写法的某个标签被忽略;二是maxFileSize的单位没写对。我见过一位同事配置<maxFileSize>100</maxFileSize>,他以为是 100MB,实际 logback 默认单位是字节,于是日志文件写到 100 字节就切一次,目录里瞬间生成上万个归档文件。
2.2 症状二:归档文件互相覆盖,日志“越滚越少”
这个比不滚动更隐蔽。如果你配置的是SizeAndTimeBasedRollingPolicy,但fileNamePattern里只有日期没有序号%i,就会出问题。%i是“同一周期内递增的序号”,缺少它时,同一时间段内多次滚动会生成相同的归档文件名,后滚动的文件会覆盖前面的归档,给人感觉日志内容少了。最直接的表现是:目录里永远只有 app.2025-03-11.log.gz 一个文件,但里面的内容远少于当天应该产生的日志量。
还有一类情况是fileNamePattern写成了logs/current.log.%d{yyyy-MM-dd}.%i.log,同时<file>指向logs/current.log。滚动前 logback 会把 current.log 改名成 current.log.2025-03-11.0.log,然后新建 current.log。问题出在下次滚动时,当前文件 current.log 又一次被改名,但根据你的 pattern 计算出来的归档名和第一次相同,同样会覆盖。
2.3 症状三:滚动出来的归档文件是 0 字节
我排查过一类“滚动成功但文件是空”的问题。滚动本身触发了,归档文件也生成了,但解压出来是 0 字节。这通常不是滚动策略的问题,而是日志写入链路的问题。项目里为了不阻塞业务线程,经常套一层AsyncAppender,如果异步队列没来得及刷盘,进程就终止了,那当前日志内容就会丢失。另外,RollingFileAppender默认immediateFlush=true,每次写入都会刷盘,如果你手贱改成 false,又遇到进程被强杀,最后那批日志就没了。
2.4 症状四:配置文件改了,重启后还是老样子
这类问题最气人。你检查了代码、检查了语法,配置看起来完全正确,但重启后行为没变。在我的经验里,绝大多数原因是 classpath 下同时存在多个 logback 配置文件。比如项目里有logback.xml和logback-spring.xml,Spring Boot 的加载优先级是logback-spring.xml优先于logback.xml,你改了前者,实际生效的却是后者;或者你改了src/main/resources下的配置,但打包时 Maven 资源过滤把这个文件给污染了,变量没替换成功。
3. 一份能同时按日期和大小滚动的完整配置
3.1 推荐姿势:使用 SizeAndTimeBasedRollingPolicy
直接给一份我在生产环境验证过、可以直接抄的配置。在src/main/resources/logback-spring.xml中这样写:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 让 Spring Boot 的 logging.file.path 配置能传进来 --> <springProperty scope="context" name="logPath" source="logging.file.path" defaultValue="./logs"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 当前正在写入的文件 --> <file>${logPath}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 归档文件命名规则:日期 + 序号 + 压缩格式 --> <fileNamePattern>${logPath}/history/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <!-- 每个文件达到 100MB 时触发滚动 --> <maxFileSize>100MB</maxFileSize> <!-- 最多保留 15 天的归档文件 --> <maxHistory>15</maxHistory> <!-- 所有归档文件总大小上限 5GB --> <totalSizeCap>5GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="INFO"> <appender-ref ref="FILE"/> </root> </configuration>这份配置里我加了<springProperty>标签,让日志目录可以从application.yml的logging.file.path读取,方便不同环境切换。如果你不想依赖 Spring Boot 属性,直接把<file>和<fileNamePattern>写成绝对路径也可以。我习惯再加一层 history 子目录存放归档文件,这样日志目录不会显得杂乱。
3.2 参数逐个拆解:为什么是这些值
maxFileSize决定单个文件触顶阈值。这里要特别注意单位:支持 KB、MB、GB,不写单位就是字节。我建议用 100MB 或 256MB,因为它同时影响归档文件数量和排障时打开文件的速度。如果太小,比如 10MB,归档文件会很多,维护成本增加;如果太大,比如 1GB,又回到了“单文件过大”的问题。
maxHistory按日期周期保留归档。配合%d{yyyy-MM-dd}使用,意思是只保留最近 15 天的归档,更早的自动删除。注意它不一定按自然日算,而是按归档文件名里的日期段计算的。
totalSizeCap是总容量上限。它是 logback 对“磁盘被日志塞满”问题的最终兜底。即使 15 天内的归档没超过maxHistory,但所有归档加起来超过 5GB,logback 也会从最老的开始删除。生产环境我强烈建议maxHistory和totalSizeCap两个都配,双保险。
%i是序号占位符。没有它,同一天产生的多个归档文件会互相顶掉。%d{yyyy-MM-dd}决定按天为周期。如果希望按小时切,就写%d{yyyy-MM-dd-HH}。.gz后缀是 gzip 压缩,能让磁盘占用大幅下降。如果不想压缩就不加后缀,但生产环境我建议压缩,归档文件占用能减少 70% 以上。
3.3 Spring Boot 的简化配置方式与边界
Spring Boot 2.x 起提供了一组内置的 rolling policy 配置项,可以在application.yml里这样写:
logging: file: name: ./logs/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 15 total-size-cap: 5GB file-name-pattern: ./logs/history/app.%d{yyyy-MM-dd}.%i.log.gz这种方式的优点是少写 XML,适合只想快速调整参数的场景。但它的局限也很明显:不支持更复杂的自定义滚动策略,比如需要按多个 appender 写不同文件、需要给某个 logger 单独设置滚动策略时,还是要回到logback-spring.xml。而且,如果项目里同时存在logback-spring.xml,Spring Boot 会优先使用 XML 配置,YAML 里的rollingpolicy不一定会生效。这个坑我踩过,当时在 YAML 里改了max-file-size,而项目里一份旧的 XML 还躺着,结果怎么改都没反应。
4. 排查技巧:让 logback 自己交代问题
4.1 开启 logback 内部状态输出
遇到“配置不生效”问题时,不要靠猜。logback 自己提供了诊断开关,在logback-spring.xml根节点加一个 listener:
<configuration> <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener"/> <!-- 其余配置省略 --> </configuration>或者在启动命令里加 JVM 参数-Dlogback.statusListenerClass=ch.qos.logback.core.status.OnConsoleStatusListener。启动时控制台会打印 logback 内部初始化过程,包括它找到了哪个配置文件、加载了哪些 appender、有没有解析出${logPath}变量。我解决过一次“日志不写入自定义目录”的问题,就是因为变量解析失败,logback 打印了 WARN,而当时没开 status,靠猜浪费了半天。
4.2 观察启动日志与目录结构
启动后,先看日志文件有没有生成,再确认目录结构:
ls -l logs/ ls -l logs/history/如果logs/history下出现了带日期和序号的.gz文件,说明滚动策略已经被接受。然后可以手动制造一次滚动来验证。比如临时把maxFileSize改成1KB,重启后写几行日志,观察是否切出新文件。验证完再改回正式值。这个方法虽然粗暴,但能最快确认整个链路是通的。
4.3 最小化复现,排除“叠加因素”
如果一切看起来都正常,但还是没滚动,我建议搞一个最小配置,只保留一个 appender、一个 root,去掉自定义 encoder、异步 appender 等叠加因素,再测。很多时候问题不在滚动策略本身,而是异步 appender 把日志动作吞了,或者某处的 logger 配置级别过高导致根本没写文件。最小化复现能帮你快速定位是哪一层出了问题。
5. 常见问题与避坑速查表
5.1 一张表搞清楚所有典型故障
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 文件不切分,一直增大 | fileNamePattern缺少%i或触发策略未生效 | 改用SizeAndTimeBasedRollingPolicy,确认maxFileSize |
| 归档文件互相覆盖 | fileNamePattern没有%i | 加上.%i.,例如app.%d{yyyy-MM-dd}.%i.log.gz |
| 归档文件 0 字节 | 异步写日志未刷盘,或程序强杀 | 检查AsyncAppender配置,尽量保留immediateFlush=true |
| 配置改了没效果 | classpath 下存在多个 logback 配置文件 | 检查项目里是否有logback.xml和logback-spring.xml共存 |
maxFileSize无效 | 单位写错或放到了不支持该属性的类下 | 使用 KB/MB/GB 明确单位,放在SizeAndTimeBasedRollingPolicy内 |
| 磁盘还是被占满 | 没有配置totalSizeCap或maxHistory过大 | 两个兜底参数都配上,定期检查磁盘 |
| 日志目录不存在 | 变量${logPath}没解析出来 | 使用<springProperty>,确认application.yml有对应配置 |
| 启动报错无法写入 | 文件被其他进程占用,Windows 平台常见 | 确认没有其他服务或预览程序锁住 log 文件 |
5.2 关于 AsyncAppender 的一笔重要提醒
很多项目为了性能给日志加了异步 appender,我强烈建议在生产环境保持immediateFlush=true,并且把AsyncAppender的queueSize调大。默认队列是 256,如果业务量很大,队列满了之后,logback 会启动“丢弃策略”,直接丢弃一部分日志来保护应用,结果就是滚动本身正常,但日志内容不完整。排查这类问题时,表面现象很像滚动配置坏了,实际是日志在进入 appender 之前就被扔掉了。这属于“配置不生效”问题里最容易被误判的种类。
5.3 时区和文件名格式的隐藏坑
%d{yyyy-MM-dd}默认使用服务器的本地时区。如果服务器时区是 UTC,而你的业务和排障都在东八区,你会发现归档文件的日期“差一天”。这个问题不影响滚动功能,但会让按日期查找日志变得很别扭。建议在配置里显式指定时区:%d{yyyy-MM-dd, GMT+8},或者更稳妥的做法是在部署服务器时统一时区。
5.4 老版本 logback 的兼容性问题
如果你项目的 Spring Boot 版本比较老,内置的 logback 还很旧,那么totalSizeCap这个参数可能不支持。SizeAndTimeBasedRollingPolicy是在 logback 1.1.0 之后才出现的,totalSizeCap更是后面几个版本才补齐。遇到参数被忽略时,先看一下实际生效的 logback 版本:
java -jar app.jar --debug | grep -i logback或者直接在依赖树里查:
mvn dependency:tree | grep logback如果版本太老,优先升级logback-classic和logback-core到 1.2.x 或更高,再重新验证。
6. 我的一点实操体会
日志滚动分割这个需求,看起来只是几十行配置,但真正在实际项目里跑稳,关键是理解RollingFileAppender、RollingPolicy、TriggeringPolicy三者之间的协作关系。我这回踩的坑里,最“阴险”的就是fileNamePattern里的%i缺失和maxFileSize单位错写,前者让文件被覆盖,后者让文件疯狂分裂,两个问题都极难凭肉眼发现,全靠开 status 看内部日志才定位。
最后再分享一个小习惯:每次调整滚动配置后,我会清空旧的归档文件再重启,避免老文件干扰判断;然后先小规模验证,再上生产。这样能把“配置不生效”的风险控制在最小范围。希望这篇经验总结能帮你少走弯路,在 Spring Boot 里把日志滚动一次配好。