You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

应用停机期间日志未滚动归档的问题处理咨询

问题描述

我在应用中配置了如下Appender,用于执行每小时日志滚动并压缩日志文件:

<appender name="log-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
  ..
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>log.json.%d{yyyy-MM-dd-HH}.gz</fileNamePattern>
    <maxHistory>36</maxHistory>
    <totalSizeCap>1GB</totalSizeCap>
  </rollingPolicy>
</appender>

生成的文件如下:

log.json.2026-04-14-08.gz
log.json.2026-04-14-09.gz
log.json.2026-04-14-10      <-- 当前活跃文件

该配置运行良好,但存在以下场景问题:

  • 假设应用因部署等原因在10:58至11:02期间停机
  • log.json.2026-04-14-10文件未执行滚动操作,无法被压缩为.gz格式
  • 后续执行归档清理时,该文件因不匹配.gz后缀不会被处理
  • 这会产生孤儿文件,在logback无内置清理机制的情况下逐渐占用磁盘空间

使用cleanHistoryOnStart也无济于事,因为它仅检查fileNamePattern中指定的.gz文件。


解决方案

1. 自定义Logback扩展,启动时清理未压缩旧日志

实现一个自定义的ContextListener,在应用启动时扫描日志目录,处理未压缩的过期日志:

  • 筛选出符合log.json.yyyy-MM-dd-HH格式但无.gz后缀的文件
  • 校验文件时间是否早于当前时间1小时(确保是过期的活跃文件)
  • 对符合条件的文件执行GZIP压缩,完成后删除原文件
  • 手动触发Logback的历史清理逻辑,确保压缩后的文件纳入管理

核心代码示例:

public class UncompressedLogCleaner implements ContextListener {
    @Override
    public void start(Context context) {
        File logDir = new File("/path/to/your/logs");
        File[] targetFiles = logDir.listFiles((dir, name) -> name.matches("log.json.\\d{4}-\\d{2}-\\d{2}-\\d{2}"));
        if (targetFiles == null) return;

        long oneHourAgo = System.currentTimeMillis() - 3600 * 1000;
        for (File file : targetFiles) {
            if (file.lastModified() < oneHourAgo) {
                compressFile(file);
                file.delete();
            }
        }

        // 触发Logback内置清理
        RollingFileAppender<?> appender = (RollingFileAppender<?>) context.getAppender("log-file");
        if (appender != null) {
            TimeBasedRollingPolicy<?> policy = (TimeBasedRollingPolicy<?>) appender.getRollingPolicy();
            policy.cleanHistory();
        }
    }

    private void compressFile(File source) {
        try (FileInputStream fis = new FileInputStream(source);
             GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(source.getAbsolutePath() + ".gz"))) {
            byte[] buffer = new byte[4096];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                gzos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            // 记录压缩异常日志,避免影响应用启动
        }
    }

    @Override
    public void stop() {}

    @Override
    public void setContext(Context context) {}
}

在logback配置中注册该监听器:

<configuration>
    <contextListener class="com.your.package.UncompressedLogCleaner"/>
    <!-- 原有Appender配置 -->
</configuration>

2. 注册Shutdown钩子,主动触发日志滚动

在应用关闭时主动调用Logback的滚动方法,避免未压缩的活跃文件遗留:

  • 注册JVM shutdown钩子,在应用正常退出时触发日志滚动
  • 确保即使是部署停机场景,只要JVM正常退出,就能完成当前日志的压缩

代码示例:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
    RollingFileAppender<?> appender = (RollingFileAppender<?>) context.getAppender("log-file");
    if (appender != null) {
        try {
            appender.getRollingPolicy().rollover();
        } catch (RolloverFailure e) {
            // 记录滚动失败日志
        }
    }
}));

3. 外部定时脚本清理

若不想修改应用代码,可借助操作系统定时任务处理:

  • 编写Shell脚本,扫描日志目录找出未压缩的旧日志,执行压缩后删除原文件
  • 同时清理超过maxHistory(36小时)的压缩日志

Shell脚本示例:

#!/bin/bash
LOG_DIR="/path/to/your/logs"
# 计算1小时前的时间戳(秒)
ONE_HOUR_AGO=$(date -d '1 hour ago' +%s)
# 36小时=2160分钟
MAX_HISTORY_MIN=2160

# 压缩过期的未压缩日志
for file in "$LOG_DIR"/log.json.*; do
    if [[ ! $file =~ \.gz$ ]]; then
        FILE_TIMESTAMP=$(stat -c %Y "$file")
        if (( FILE_TIMESTAMP < ONE_HOUR_AGO )); then
            gzip "$file"
        fi
    fi
done

# 清理过期的压缩日志
find "$LOG_DIR" -name "log.json.*.gz" -mmin +$MAX_HISTORY_MIN -delete

在Linux系统中配置cron每小时执行一次:

0 * * * * /path/to/your/clean_logs.sh

内容的提问来源于stack exchange,提问作者Markus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 15:04:49