小架构step系列09:日志量控制

1 概述

当业务运行出问题的时候,需要借助日志来定位问题,下面的情况可能会导致日志量比较大:
  • 很多开发人员对日志的作用并不是很理解,大概是把日志当单步调试用,想看看程序是否运行到哪一步就打印个日志,确定程序到哪一步远远不够的,还需要其它信息,这样日志就会越打越多。
  • 业务软件运行的时间越长,积攒的日志就越多。
日志的存储是要消耗存储空间的,如果不加以控制,量积累到一定程度就有可能会耗光有限的磁盘空间,甚至无法写入造成宕机,所以需要对日志量进行控制。

2 日志配置

2.1 配置

之前的配置文件里有以下的配置:
<property name="log.file.path" value="logs" /> <!-- 日志存放目录 -->
<property name="log.filename.pattern" value="${log.file.path}/app.%d{yyyy-MM-dd}.%i.log" /> <!-- 日志文件名格式 -->
<property name="log.file.maxsize" value="30MB" /> <!-- 每个日志文件最大的大小 -->
<property name="log.file.maxhistory" value="30" /> <!-- 日志文件的最大保留个数,默认为0(无限) -->
<property name="log.file.max.capacity" value="10GB" /> <!-- 所有日志文件总大小的上限,需要配了文件最大保留个数才有效,默认为0(无限) -->

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${log.file.path}/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${log.filename.pattern}</fileNamePattern>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>${log.file.maxsize}</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
        <maxHistory>${log.file.maxhistory}</maxHistory>
        <totalSizeCap>${log.file.max.capacity}</totalSizeCap>
    </rollingPolicy>
</appender>
  • 首先,要指定日志存储的位置,也就是存储在哪个目录下。
  • 然后是一个日志文件不能过大,否则需要用的时候,想打开都比较困难,影响日志查看,需要设置一个单文件的最大量限制;既然一个文件不能过大,就必须对日志文件进行拆分,拆分后的文件名用什么格式命名;文件日志过多也不方便,在linux中一个目录下的文件个数也是有限的,需要对文件格式进行限制;
  • 最后一个是对日志总量的限制,不管每个文件的大小和文件数量多少,总量不能超过指定的数量。

2.2 原理

2.2.1 根据文件名pattern初始化拆分文件的时间周期

在配置文件中,文件名的pattern配置如 logs/app.%d{yyyy-MM-dd}.%i.log 样式,它是希望日志文件能够按天进行拆分,如果一天有多个,则加后缀“.i”。其中yyyy-MM-dd 是datePattern,这datePattern可以配置成很多种形式,这里只是其中一种:按天的类型,如何确定datePattern也是个问题,也就是要确定日志文件是按天、按分钟还是按月等类型来拆分。
// 在加载loback.xml配置文件时,会初始化Policy类(TimeBasedRollingPolicy),并调start()方法
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
    // 1. 把配置中的文件名pattern赋值给fileNamePattern变量
    if (fileNamePatternStr != null) {
        fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
        determineCompressionMode();
    }
    
    // 2. 初始化timeBasedFileNamingAndTriggeringPolicy并调用start()方法,例子中配的是SizeAndTimeBasedFNATP
    if (timeBasedFileNamingAndTriggeringPolicy == null) {
        timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
    }
    timeBasedFileNamingAndTriggeringPolicy.setContext(context);
    timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
    timeBasedFileNamingAndTriggeringPolicy.start();
    // 省略其它代码
}

// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public void start() {
    // 3. 调用父类TimeBasedFileNamingAndTriggeringPolicyBase的start()
    super.start();
    // 省略其它代码
}

// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
public void start() {
    DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
    if (dtc == null) {
        throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
    }

    // 4. 初始化RollingCalendar,用于计算拆分文件的时间点
    //    比如:当文件名pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,dtc.getDatePattern()为yyyy-MM-dd
    if (dtc.getTimeZone() != null) {
        rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
    } else {
        rc = new RollingCalendar(dtc.getDatePattern());
    }
    addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '" + tbrp.fileNamePattern.getPattern() + "'.");
    rc.printPeriodicity(this);

    if (!rc.isCollisionFree()) {
        addError("The date format in FileNamePattern will result in collisions in the names of archived log files.");
        addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
        withErrors();
        return;
    }

    setDateInCurrentPeriod(new Date(getCurrentTime()));
    if (tbrp.getParentsRawFileProperty() != null) {
        File currentFile = new File(tbrp.getParentsRawFileProperty());
        if (currentFile.exists() && currentFile.canRead()) {
            setDateInCurrentPeriod(new Date(currentFile.lastModified()));
        }
    }
    addInfo("Setting initial period to " + dateInCurrentPeriod);
    
    computeNextCheck();
}

// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public RollingCalendar(String datePattern) {
    super();
    // 记住日期pattern
    this.datePattern = datePattern;
    // 5. 根据pattern计算文件拆分周期类型,是按天还是按时分秒
    this.periodicityType = computePeriodicityType();
}
public PeriodicityType computePeriodicityType() {
    GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
    Date epoch = new Date(0); // 设置初始值为1970-01-01 00:00:00 GMT

    if (datePattern != null) {
        // PeriodicityType.VALID_ORDERED_LIST里提供了从毫秒到月这7种粒度的类型
        //   TOP_OF_MILLISECOND 毫秒
        //   PeriodicityType.TOP_OF_SECOND 秒
        //   PeriodicityType.TOP_OF_MINUTE 分钟
        //   PeriodicityType.TOP_OF_HOUR 小时
        //   PeriodicityType.TOP_OF_DAY 天
        //   PeriodicityType.TOP_OF_WEEK 周
        //   PeriodicityType.TOP_OF_MONTH 月
        for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
            simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
            
            // 6. epoch是一个1970-01-01 00:00:00的时间(到毫秒),用datePattern去把这个时间格式化成一个字符串
            //    由于不确定datePattern配置为哪一种(上面7中类型的一种),所以需要确定是哪一种
            String r0 = simpleDateFormat.format(epoch);

            // 7. 下一个时间同样也用datePattern格式化为字符串,方便比较,这里变化只有i,是7种时间粒度类型的一种
            //    即循环7种时间粒度类型来比较,看看配置的是哪一种时间类型
            Date next = innerGetEndOfThisPeriod(calendar, i, epoch);
            
            String r1 = simpleDateFormat.format(next);
            if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
                return i;
            }
        }
    }
    return PeriodicityType.ERRONEOUS;
}
static private Date innerGetEndOfThisPeriod(Calendar cal, PeriodicityType periodicityType, Date now) {
    // 8. periodicityType是当前的周期类型(遍历7种类型),now是1970-01-01 00:00:00,提供加1的参数
    return innerGetEndOfNextNthPeriod(cal, periodicityType, now, 1);
}
static private Date innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Date now, int numPeriods) {
    // 9. 按周期类型给指定时间now加上指定粒度的1,如果时间粒度是天则加上1天,如果时间粒度是毫秒则加上1毫秒等
    cal.setTime(now);
    switch (periodicityType) {
    case TOP_OF_MILLISECOND:
        cal.add(Calendar.MILLISECOND, numPeriods);
        break;

    case TOP_OF_SECOND:
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.SECOND, numPeriods);
        break;

    case TOP_OF_MINUTE:
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.MINUTE, numPeriods);
        break;

    case TOP_OF_HOUR:
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.HOUR_OF_DAY, numPeriods);
        break;

    case TOP_OF_DAY:
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.DATE, numPeriods);
        break;

    case TOP_OF_WEEK:
        cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
        break;

    case TOP_OF_MONTH:
        cal.set(Calendar.DATE, 1);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.MONTH, numPeriods);
        break;

    default:
        throw new IllegalStateException("Unknown periodicity type.");
    }

    return cal.getTime();
}

// 回到RollingCalendar继续处理加上了1时间粒度的时间
// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public PeriodicityType computePeriodicityType() {
    GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
    Date epoch = new Date(0); // 设置初始值为1970-01-01 00:00:00 GMT

    if (datePattern != null) {
        // PeriodicityType.VALID_ORDERED_LIST里提供了从毫秒到月这7种粒度的类型
        //   TOP_OF_MILLISECOND 毫秒
        //   PeriodicityType.TOP_OF_SECOND 秒
        //   PeriodicityType.TOP_OF_MINUTE 分钟
        //   PeriodicityType.TOP_OF_HOUR 小时
        //   PeriodicityType.TOP_OF_DAY 天
        //   PeriodicityType.TOP_OF_WEEK 周
        //   PeriodicityType.TOP_OF_MONTH 月
        for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
            simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
            
            // 6. epoch是一个1970-01-01 00:00:00的时间(到毫秒),用datePattern去把这个时间格式化成一个字符串
            //    由于不确定datePattern配置为哪一种(上面7中类型的一种),所以需要确定是哪一种
            String r0 = simpleDateFormat.format(epoch);

            // 7. 下一个时间同样也用datePattern格式化为字符串,方便比较
            Date next = innerGetEndOfThisPeriod(calendar, i, epoch);
            String r1 = simpleDateFormat.format(next);

            // 10. r0和r1都是时间格式化来的,r1是在1970-01-01 00:00:00这个时间点上按类型加1个时间粒度得来的
            //     比如:以时间粒度为天作为例子,datePattern是yyyy-MM-dd,此时r0=1970-01-01,
            //          遍历上面7种类型,第一种是毫秒,那么就在毫秒上加1,加1后用datePattern格式化,
            //          此时r1=1970-01-01,由于格式化后的值是到天的,所以毫秒加1就不影响,
            //          所以r0=r1,说明配置的datePattern并不是毫秒的类型,然后遍历下一种类型;
            //          当遍历到天类型时,天加1后格式化,此时r1=1970-01-02,r0 != r1,说明是天类型,
            //          从而找到了datePattern的类型,返回此类型,RollingCalendar初始化完成
            if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
                return i;
            }
        }
    }
    return PeriodicityType.ERRONEOUS;
}

// 回到TimeBasedFileNamingAndTriggeringPolicyBase的start()方法,使用初始化好的RollingCalendar
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
public void start() {
    DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
    if (dtc == null) {
        throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
    }

    // 4. 初始化RollingCalendar,用于计算拆分文件的时间点
    //    比如:当pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,dtc.getDatePattern()为yyyy-MM-dd
    if (dtc.getTimeZone() != null) {
        rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
    } else {
        rc = new RollingCalendar(dtc.getDatePattern());
    }
    addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '" + tbrp.fileNamePattern.getPattern() + "'.");
    rc.printPeriodicity(this);

    if (!rc.isCollisionFree()) {
        addError("The date format in FileNamePattern will result in collisions in the names of archived log files.");
        addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
        withErrors();
        return;
    }

    // 11. 设置当前的时间点
    //     先把当前时间作为默认值,如果历史上还没有打印过日志,那么这个时间就是当前时间点;
    //     然后从已有的日志文件中找出最后一次修改的时间作为当前时间点,如果中间很长时间没打印日志,那么这个时间可以是很久以前的;
    setDateInCurrentPeriod(new Date(getCurrentTime()));
    // 当pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,tbrp.getParentsRawFileProperty()为logs/app.log
    if (tbrp.getParentsRawFileProperty() != null) {
        File currentFile = new File(tbrp.getParentsRawFileProperty());
        if (currentFile.exists() && currentFile.canRead()) {
            setDateInCurrentPeriod(new Date(currentFile.lastModified())); // 文件上次修改的时间
        }
    }
    addInfo("Setting initial period to " + dateInCurrentPeriod);
    
    // 12. 计算下一个日志文件要拆分的时间点
    computeNextCheck();
}
public void setDateInCurrentPeriod(Date _dateInCurrentPeriod) {
    this.dateInCurrentPeriod = _dateInCurrentPeriod; // 记录当前时间点
}

// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
protected void computeNextCheck() {
    // 13. rc为上面初始化好的RollingCalendar,调用其getNextTriggeringDate方法,参数是记录好的当前时间点
    this.nextCheck = this.rc.getNextTriggeringDate(this.dateInCurrentPeriod).getTime();
}

// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public Date getNextTriggeringDate(Date now) {
    // 14. 按时间粒度类型,获取下一个日志要拆分的时间点,多提供参数1(表示一个时间粒度)
    return getEndOfNextNthPeriod(now, 1);
}
public Date getEndOfNextNthPeriod(Date now, int periods) {
    // 15. 计算下一个拆分日志文件的时间点,多提供参数当前类型(该类型为之前在computePeriodicityType()中计算出来的datePattern类型)
    //     计算方式参考上面步骤9,比如时间粒度类型为天,那下一个要拆分的时间点应该是当前时间点的天加上1天
    return innerGetEndOfNextNthPeriod(this, this.periodicityType, now, periods);
}

// 回到TimeBasedFileNamingAndTriggeringPolicyBase的computeNextCheck(),记录计算出来的下一个拆分日志文件的时间点
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
protected void computeNextCheck() {
    // 16. 用nextCheck记录计算出来的下一个拆分日志文件的时间点
    this.nextCheck = this.rc.getNextTriggeringDate(this.dateInCurrentPeriod).getTime();
}

2.2.2 拆分日志文件

按上面配置,时间到了(比如按天)就需要拆分文件,文件大小到达上限之后,也需要拆分文件,是否要拆分文件是在写日志的时候进行的,如果要拆分日志则先拆分文件,然后再写当前的日志。

// 1. logback处理写日志的时候,会把日志信息包装成一个eventObject,append到文件的末尾
// 源码位置:ch.qos.logback.core.OutputStreamAppender
protected void append(E eventObject) {
    if (!isStarted()) {
        return;
    }

    // 2. 调子类的subAppend(),例子配的子类appender为RollingFileAppender
    subAppend(eventObject);
}

// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(E event) {
    synchronized (triggeringPolicy) {
        // 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
        if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
            rollover();
        }
    }

    super.subAppend(event);
}

// 例子中配的拆分文件策略是基于时间和大小
// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public boolean isTriggeringEvent(File activeFile, final E event) {
    long time = getCurrentTime();

    // 4. 按时间判断是否要拆分文件
    //    在初始化的时候已经计算好nextCheck,其来源要么是初始化时的当前时间,或者是日志文件的最后一次修改时间;
    //    nextCheck是在这个时间的基础上按时间粒度类型加1得到的,比如时间粒度是按天,那么就是加了1天;
    //    所以这里用写日志的当前时间和nextCheck比较,如果当前时间比nextCheck大,就是需要拆分文件了;
    if (time >= nextCheck) {
        Date dateInElapsedPeriod = this.dateInCurrentPeriod;
        elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInElapsedPeriod, currentPeriodsCounter);
        currentPeriodsCounter = 0;
        setDateInCurrentPeriod(time);
        computeNextCheck(); // 同样的方式用当前时间按时间粒度类型加1更新下一次的nextCheck时间
        return true;
    }

    if (invocationGate.isTooSoon(time)) {
        return false;
    }

    if (activeFile == null) {
        addWarn("activeFile == null");
        return false;
    }
    if (maxFileSize == null) {
        addWarn("maxFileSize = null");
        return false;
    }
    // 5. 如果文件的大小比指定的单个文件最大值大,则也是要拆分文件的
    if (activeFile.length() >= maxFileSize.getSize()) {
        elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInCurrentPeriod, currentPeriodsCounter);
        currentPeriodsCounter++;
        return true;
    }

    return false;
}

// 回到RollingFileAppender的subAppend(),处理日志文件拆分
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(),处理日志文件拆分(E event) {
    synchronized (triggeringPolicy) {
        // 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
        if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
            // 6. 拆分文件
            rollover();
        }
    }

    super.subAppend(event);
}


// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
public void rollover() {
    lock.lock();
    try {
        // 7. 先关掉文件流,避免当前日志丢失,也使得文件可以拆分
        this.closeOutputStream();
        // 8. 拆分文件
        attemptRollover();
        attemptOpenFile();
    } finally {
        lock.unlock();
    }
}
private void attemptRollover() {
    try {
        // 9. 调用TimeBasedRollingPolicy的rollover()拆分文件
        rollingPolicy.rollover();
    } catch (RolloverFailure rf) {
        addWarn("RolloverFailure occurred. Deferring roll-over.");
        this.append = true;
    }
}

// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void rollover() throws RolloverFailure {
    String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();

    String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

    // 10. 在拆分文件之前已经关闭文件流,拆分文件实际就是把文件重命名一下,然后写同名新文件
    //     这里compressionMode是指是否配置了文件压缩,如果配置了压缩,除了重命名之外还得把重命名之后的文件压缩一下
    if (compressionMode == CompressionMode.NONE) {
        if (getParentsRawFileProperty() != null) {
            renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
        }
    } else {
        if (getParentsRawFileProperty() == null) {
            compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
        } else {
            compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
        }
    }

    if (archiveRemover != null) {
        Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
        this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
    }
}

// 回到RollingFileAppender的subAppend(),写日志
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(),处理日志文件拆分(E event) {
    synchronized (triggeringPolicy) {
        // 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
        if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
            // 6. 拆分文件
            rollover();
        }
    }

    // 11. 在父类方法中写日志
    super.subAppend(event);
}

// 源码位置:ch.qos.logback.core.OutputStreamAppender
protected void subAppend(E event) {
    if (!isStarted()) {
        return;
    }
    try {
        if (event instanceof DeferredProcessingAware) {
            ((DeferredProcessingAware) event).prepareForDeferredProcessing();
        }

        byte[] byteArray = this.encoder.encode(event);
        // 12. 写日志
        writeBytes(byteArray);

    } catch (IOException ioe) {
        // as soon as an exception occurs, move to non-started state
        // and add a single ErrorStatus to the SM.
        this.started = false;
        addStatus(new ErrorStatus("IO failure in appender", this, ioe));
    }
}

2.2.3 删除多余文件

如果配置了文件个数和文件大小限制,则达到了这些限制,需要把超限的文件删除掉,从而保持日志文件不超限。

// 在TimeBasedRollingPolicy中先初始化文件删除器
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
    // 省略其它代码
    
    if (timeBasedFileNamingAndTriggeringPolicy == null) {
        timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
    }
    timeBasedFileNamingAndTriggeringPolicy.setContext(context);
    timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
    
    // 1. 在TriggeringPolicy创建文件删除器,例子中配置的是SizeAndTimeBasedFNATP
    timeBasedFileNamingAndTriggeringPolicy.start();

    if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
        addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
        return;
    }
    
    if (maxHistory != UNBOUND_HISTORY) {
        archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
        archiveRemover.setMaxHistory(maxHistory);
        archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
        if (cleanHistoryOnStart) {
            addInfo("Cleaning on start up");
            Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
            cleanUpFuture = archiveRemover.cleanAsynchronously(now);
        }
    } else if (!isUnboundedTotalSizeCap()) {
        addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
    }

    super.start();
}

// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public void start() {
    // 2. 创建文件删除器
    archiveRemover = createArchiveRemover();
    archiveRemover.setContext(context);
    // 省略其它代码
}
protected ArchiveRemover createArchiveRemover() {
    // 3. 创建的是SizeAndTimeBasedArchiveRemover文件删除器,提供了文件名pattern,rc是RollingCalendar
    return new SizeAndTimeBasedArchiveRemover(tbrp.fileNamePattern, rc);
}

// 回到TimeBasedRollingPolicy的start(),继续初始化
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
    // 省略其它代码
    
    if (timeBasedFileNamingAndTriggeringPolicy == null) {
        timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
    }
    timeBasedFileNamingAndTriggeringPolicy.setContext(context);
    timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
    
    // 1. 在TriggeringPolicy创建文件删除器,例子中配置的是SizeAndTimeBasedFNATP
    timeBasedFileNamingAndTriggeringPolicy.start();

    if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
        addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
        return;
    }
    
    // 4. 如果配置了maxHistory,那么文件删除器archiveRemover才起作用
    if (maxHistory != UNBOUND_HISTORY) {
        archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
        archiveRemover.setMaxHistory(maxHistory); // 设置文件个数最大限制
        archiveRemover.setTotalSizeCap(totalSizeCap.getSize()); // 设置文件大小最大限制
        if (cleanHistoryOnStart) {
            addInfo("Cleaning on start up");
            Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
            cleanUpFuture = archiveRemover.cleanAsynchronously(now);
        }
    } else if (!isUnboundedTotalSizeCap()) {
        addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
    }

    super.start(); // 父类的start()只是把this.started置成true
}


// 在打印日志的时候会拆分文件,在拆分文件的时候,触发文件删除器工作
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void rollover() throws RolloverFailure {
    String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();

    String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

    if (compressionMode == CompressionMode.NONE) {
        if (getParentsRawFileProperty() != null) {
            renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
        }
    } else {
        if (getParentsRawFileProperty() == null) {
            compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
        } else {
            compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
        }
    }

    // 如果文件删除器存在,则触发文件删除操作
    if (archiveRemover != null) {
        Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
        // 5. 触发文件删除器的异步删除文件操作,archiveRemover为TimeBasedArchiveRemover
        this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
    }
}

// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public Future<?> cleanAsynchronously(Date now) {
    // 6. 创建一个runnable,放到线程池中异步执行
    ArhiveRemoverRunnable runnable = new ArhiveRemoverRunnable(now);
    ExecutorService executorService = context.getScheduledExecutorService();
    Future<?> future = executorService.submit(runnable);
    return future;
}

// ArhiveRemoverRunnable是TimeBasedArchiveRemover的内部类
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover.ArhiveRemoverRunnable
public class ArhiveRemoverRunnable implements Runnable {
    Date now;
    ArhiveRemoverRunnable(Date now) {
        this.now = now;
    }

    @Override
    public void run() {
        // 7. 根据文件个数上限删除多余文件
        clean(now);
        
        if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
            capTotalSize(now);
        }
    }
}

// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void clean(Date now) {
    long nowInMillis = now.getTime();    
    // 8. 计算文件的周期数量
    //    上次删除文件的时间到当前时间的毫秒数,除以周期,得到循环的次数
    //    如果文件名pattern里配置的是按天拆分文件,那么周期就是天,这个计算就是得到目前有多少天的日志文件
    //    注意:如果一个周期内有多个文件,在此只算1
    int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
    lastHeartBeat = nowInMillis;
    if (periodsElapsed > 1) {
        addInfo("Multiple periods, i.e. " + periodsElapsed + " periods, seem to have elapsed. This is expected at application start.");
    }
    
    // 9. 遍历周期,删除多余文件
    for (int i = 0; i < periodsElapsed; i++) {
        // 从下面代码可看出offset=(-maxHistory - 1),也就是比允许留下文件周期数大一个
        int offset = getPeriodOffsetForDeletionTarget() - i;
        // 计算出offset对应的时间,这个就是需要删除的文件
        // 比如周期是按天,配置maxHistory为保留7天,那从当前往前数到第8天(maxHistory+1)就是要删除的文件
        Date dateOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
        cleanPeriod(dateOfPeriodToClean);
    }
}
protected int getPeriodOffsetForDeletionTarget() {
    return -maxHistory - 1;
}

// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void cleanPeriod(Date dateOfPeriodToClean) {
    // 10. 调用子类的方法找符合条件的文件,子类为SizeAndTimeBasedArchiveRemover
    File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean);

    for (File f : matchingFileArray) {
        addInfo("deleting " + f);
        f.delete();
    }

    if (parentClean && matchingFileArray.length > 0) {
        File parentDir = getParentDir(matchingFileArray[0]);
        removeFolderIfEmpty(parentDir);
    }
}

// 源码位置:ch.qos.logback.core.rolling.helper.SizeAndTimeBasedArchiveRemover
protected File[] getFilesInPeriod(Date dateOfPeriodToClean) {
    File archive0 = new File(fileNamePattern.convertMultipleArguments(dateOfPeriodToClean, 0));
    File parentDir = getParentDir(archive0);
    String stemRegex = createStemRegex(dateOfPeriodToClean);
    // 11. 匹配符合条件的文件
    //     比如文件名pattern为logs/app.%d{yyyy-MM-dd}.%i.log,用dateOfPeriodToClean替换%d{yyyy-MM-dd},
    //     然后去匹配%i,例子中周期是天,也就是匹配这一天的多个文件
    File[] matchingFileArray = FileFilterUtil.filesInFolderMatchingStemRegex(parentDir, stemRegex);
    return matchingFileArray;
}

// 回到TimeBasedArchiveRemover的cleanPeriod(),删除匹配到的文件
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void cleanPeriod(),删除匹配到的文件(Date dateOfPeriodToClean) {
    // 10. 调用子类的方法找符合条件的文件
    File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean);

    // 12. 删除找到的文件,如果文件夹里没有文件了就删除文件夹
    for (File f : matchingFileArray) {
        addInfo("deleting " + f);
        f.delete();
    }

    if (parentClean && matchingFileArray.length > 0) {
        File parentDir = getParentDir(matchingFileArray[0]);
        removeFolderIfEmpty(parentDir);
    }
}

// 回到TimeBasedArchiveRemover.ArhiveRemoverRunnable的run(),处理文件容量
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover.ArhiveRemoverRunnable
public class ArhiveRemoverRunnable implements Runnable {
    Date now;
    ArhiveRemoverRunnable(Date now) {
        this.now = now;
    }

    @Override
    public void run() {
        // 7. 根据文件个数上限删除多余文件
        clean(now);
        
        // 13. 根据文件大小上限删除超限文件,UNBOUNDED_TOTAL_SIZE_CAP = 0
        if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
            capTotalSize(now);
        }
    }
}

// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
void capTotalSize(Date now) {
    long totalSize = 0;
    long totalRemoved = 0;
    
    // 14. 文件是按时间周期拆分的,每个周期可能有多个文件
    //     如果要求文件大小不超总量限制,那么应该从时间的近到远,从一个周期内的序号大到小进行遍历
    //     遍历到一个文件就把大小加起来,如果达到总量限制,那就把文件删除
    //     把时间和序号倒排,方便在一个循环里完成所有多余文件删除
    //     多出周期数的文件已经被删除,在这里按周期数遍历即可
    for (int offset = 0; offset < maxHistory; offset++) {
        // 计算出当前时间前一个周期的时间
        Date date = rc.getEndOfNextNthPeriod(now, -offset);
        // 获取到一个周期内的多个文件
        File[] matchingFileArray = getFilesInPeriod(date);
        // 文件按序号倒排
        descendingSortByLastModified(matchingFileArray);
        // 把一个个文件的length加起来,如果超过总量限制,则删除文件
        for (File f : matchingFileArray) {
            long size = f.length();
            if (totalSize + size > totalSizeCap) {
                addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size));
                totalRemoved += size;
                f.delete();
            }
            totalSize += size;
        }
    }
    addInfo("Removed  " + new FileSize(totalRemoved) + " of files");
}

从上面可以看出,“文件的个数”限制并不是总文件个数,而是文件周期数。比如文件是按天拆分的,那么这个文件个数限制就是天数;若文件是按分钟拆分的,那么这个限制就是分钟数。每个周期内可能会有多个日志文件,所以文件个数是所有文件周期里的文件个数的总和,即文件个数比文件周期数大。所以,准确来说maxHistory配置的是文件周期数,这个如果不看源码,并不好理解,其配置的名称是maxhistory,而不是fileCount之类的,可能也是因为其不是文件个数。

3 架构一小步

增加日志的文件个数和大小限制配置,保护好系统,避免日志量过大而影响业务正常运行,注意文件个数是指周期数(比如按天拆分文件那么就是天数):

<property name="log.file.path" value="logs" /> <!-- 日志存放目录 -->
<property name="log.filename.pattern" value="${log.file.path}/app.%d{yyyy-MM-dd}.%i.log" /> <!-- 日志文件名格式 -->
<property name="log.file.maxsize" value="30MB" /> <!-- 每个日志文件最大的大小 -->
<property name="log.file.maxhistory" value="30" /> <!-- 日志文件的最大保留个数,默认为0(无限) -->
<property name="log.file.max.capacity" value="10GB" /> <!-- 所有日志文件总大小的上限,需要配了文件最大保留个数才有效,默认为0(无限) -->

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${log.file.path}/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${log.filename.pattern}</fileNamePattern>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>${log.file.maxsize}</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
        <maxHistory>${log.file.maxhistory}</maxHistory>
        <totalSizeCap>${log.file.max.capacity}</totalSizeCap>
    </rollingPolicy>
</appender>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值