近期工作中涉及到文件记录、文件翻转等操作,思考有没有成熟的代码以便参考.

因此,第一时间就联想到Logback的AsyncAppender以及RollingFileAppender.

  • AsyncAppender:通过队列储存日志事件,启动Worker线程读取日志事件并写入关联的Appender中;
  • RollingFileAppender:当日志文件满足设定的翻滚条件时,对文件进行翻滚操作.

PS: AsyncAppender可以与RollingFileAppender结合使用,提升日志事件写入效率.

1 AsyncAppender

public class AsyncAppender extends AsyncAppenderBase<ILoggingEvent> {

    // 省略部分功能
boolean includeCallerData = false; protected boolean isDiscardable(ILoggingEvent event) {
Level level = event.getLevel();
return level.toInt() <= Level.INFO_INT;
} protected void preprocess(ILoggingEvent eventObject) {
eventObject.prepareForDeferredProcessing();
if (includeCallerData)
eventObject.getCallerData();
}
}

(1)isDiscardable:确定日志事件是否可以丢弃(当缓冲队列达到上限时,出于性能考虑需要丢弃诸如TRACE、DEBUG级别日志);

(2)preprocess:预处理日志事件(包括格式化msg,线程名称,MDC中存储的数据),如果includeCallerData为true,则需要通过日志事件的堆栈信息,获取日志所在的文件,行号等信息;

因为AsyncAppender的绝大部分功能由AsyncAppenderBase中实现,因此接下来主要讲解AsyncAppenderBase的功能点.

public class AsyncAppenderBase<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {

    BlockingQueue<E> blockingQueue;

    public static final int DEFAULT_QUEUE_SIZE = 256;
int queueSize = DEFAULT_QUEUE_SIZE; int appenderCount = 0; static final int UNDEFINED = -1;
int discardingThreshold = UNDEFINED;
boolean neverBlock = false; Worker worker = new Worker(); public static final int DEFAULT_MAX_FLUSH_TIME = 1000;
int maxFlushTime = DEFAULT_MAX_FLUSH_TIME;
}

以上是AsyncAppenderBase的主要属性:

  • blockingQueue:阻塞队列,存储日志事件,等待被各Appender消费;
  • queueSize:队列大小,如果没有设置,则默认为256;
  • discardingThreshold:日志事件的丢弃阈值,当队列中的事件数量超过阈值后,则会丢弃诸如TRACE/DEBUG级别日志;
  • worker:消费者线程(单消费者足够了);
  • maxFlushTime:程序结束时,worker线程的退出时间(从而保证尽量将队列中的日志事件写至文件中).

1.1 Worker线程

    class Worker extends Thread {

        public void run() {
AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
AppenderAttachableImpl<E> aai = parent.aai; // loop while the parent is started
while (parent.isStarted()) {
try {
E e = parent.blockingQueue.take();
aai.appendLoopOnAppenders(e);
} catch (InterruptedException ie) {
break;
}
} for (E e : parent.blockingQueue) {
aai.appendLoopOnAppenders(e);
parent.blockingQueue.remove(e);
} aai.detachAndStopAllAppenders();
}
}

Worker线程比较简单,其主要功能就是判断Appeneder是否处于运行状态(parent.isStarted()):

  • 运行:读取阻塞队列中的数据,通过AppenderAttachableImpl分发至各Appender中;
  • 不运行:循环读取缓冲队列中的残余数据,通过AppenderAttachableImpl分发至各Appender中,最后关闭关联的Appender.

1.2 启动appender

 @Override
public void start() { // 省略部分校验代码
blockingQueue = new ArrayBlockingQueue<E>(queueSize); if (discardingThreshold == UNDEFINED)
discardingThreshold = queueSize / 5;
worker.setDaemon(true);
worker.setName("AsyncAppender-Worker-" + getName());
super.start();
worker.start();
}

主要步骤:

(1) 根据设置的队列大小,创建缓冲队列大小;

(2) 如果未设置discardingThreshold,则设置discardingThreshold阈值为缓冲队列大小的4/5(1-1/5);

(3) 设置worker线程为守护线程,设置线程名称;

(4) 启动Appender,启动worker线程读取数据(需要确保Appender在worker线程前启动).

1.3 关闭appender

    @Override
public void stop() {
if (!isStarted())
return; super.stop(); // interrupt the worker thread so that it can terminate. Note that the interruption can be consumed
// by sub-appenders
worker.interrupt(); InterruptUtil interruptUtil = new InterruptUtil(context); try {
interruptUtil.maskInterruptFlag();
worker.join(maxFlushTime);
// check to see if the thread ended and if not add a warning message
if (worker.isAlive()) {
addWarn("Max queue flush timeout (" + maxFlushTime + " ms) exceeded. Approximately " + blockingQueue.size()
+ " queued events were possibly discarded.");
} else {
addInfo("Queue flush finished successfully within timeout.");
}
} catch (InterruptedException e) {
int remaining = blockingQueue.size();
addError("Failed to join worker thread. " + remaining + " queued events may be discarded.", e);
} finally {
interruptUtil.unmaskInterruptFlag();
}
}

主要步骤:

(1) super.stop():关闭Appender,worker线程执行退出逻辑;

(2) worker.interrupt():给worker线程设置中断标志(worker中未检测中断标志,因此保持继续运行状态),设置的意义是让当前appender关联的sub-appender消费,从而安全的关闭sub-appender;

(3) interruptUtil.maskInterruptFlag():清除当前线程的interrupt状态;

(4) worker.join(maxFlushTime):等待worker线程退出,结束其生命周期;

(5) 判断worker线程是否存活,如果存活说明阻塞队列中仍存有部分日志事件未被写入文件等载体中,记录消息;

(6) interruptUtil.unmaskInterruptFlag():恢复worker线程的interrupt状态.

划重点:

上述代码中,对worker线程的中断标志进行了若干次操作:

(1) interrupt:中断worker关联的sub-appender;

(2) interruptUtil.maskInterruptFlag: 取消中断标志(在线程标志为true的状态下,join操作会立即返回),因此为了确保join操作有效,需要清除worker线程的interrupt标志;

(3) interruptUtil.unmaskInterruptFlag:结束操作,恢复worker线程的interrupt标志.

1.4 添加日志事件

日志事件的添加,实质是就是往阻塞队列中插入日志事件.

根据阻塞队列接口,分两种插入方式:

(1) offer:非阻塞插入,插入失败不进行处理(存在丢日志可能性);

(2) put:阻塞插入(插入失败后会循环进行再次插入操作).

2 RollingFileAppender

public class RollingFileAppender<E> extends FileAppender<E> {
File currentlyActiveFile;
TriggeringPolicy<E> triggeringPolicy;
RollingPolicy rollingPolicy;
}

主要属性:

  • currentlyActiveFile:当前日志写入文件;
  • triggeringPolicy:日志文件触发策略(触发条件包括:单个日志文件大小,文件中日志文件总体积,翻转时刻等条件);
  • rollingPolicy:翻转策略(主要确定日志文件翻转文件名,存储路径等信息).

需要注意,如果没有日志事件写入,那么即使日志文件达到时间或者大小的触发条件,也不会创建相应的新日志文件.

2.1 添加日志事件

    protected void subAppend(E event) {
synchronized (triggeringPolicy) {
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
rollover();
}
}
super.subAppend(event);
}

主要步骤:

(1) 判断当前文件是否达到触发条件,如果是则翻转文件(使用synchronized加锁以保证同一时间段,只有一个线程进行文件的翻转操作);

(2) 调用基类的subAppend方法,将日志文件写入BufferedOutputStream中.

翻转文件:

    public void rollover() {
lock.lock();
try {
this.closeOutputStream();
attemptRollover();
attemptOpenFile();
} finally {
lock.unlock();
}
}

由以上代码可知,翻转文件涉及到以下几个操作:

(1) 关闭当前BufferedOutputStream;

(2) attemptRollover: 进行文件翻转(重命名已写入文件名,根据需求压缩日志文件,根据日志文件夹总大小以及日期删除文件等);

(3) attemptOpenFile:根据翻转条件,确定新日志文件名称,并创建对应的日志文件供后续写入.

2.2 SizeAndTimeBasedRollingPolicy

工作中,经常使用的文件翻转工具类为SizeAndTimeBasedRollingPolicy(实现了RollingPolicy以及TriggeringPolicy接口),顾名思义,其根据时间和文件大小确定日志文件的翻转触发条件.

2.2.1 日志触发

需要注意,触发器实际定义于SizeAndTimeBasedFNATP类中.

    @Override
public boolean isTriggeringEvent(File activeFile, final E event) { long time = getCurrentTime(); // first check for roll-over based on time
if (time >= nextCheck) {
Date dateInElapsedPeriod = dateInCurrentPeriod;
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInElapsedPeriod, currentPeriodsCounter);
currentPeriodsCounter = 0;
setDateInCurrentPeriod(time);
computeNextCheck();
return true;
} // next check for roll-over based on size
if (invocationGate.isTooSoon(time)) {
return false;
} if (activeFile.length() >= maxFileSize.getSize()) {
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInCurrentPeriod, currentPeriodsCounter);
currentPeriodsCounter++;
return true;
}
return false;
}

主要步骤:

(1) 获取当前时间点,并与nextCheck进行比较(日志出发时间点);

(2) 如果当前时间点大于nextCheck,则计算得到新的日志文件名前缀,赋值至elapsedPeriodsFileName;清空currentPeriodsCounter(记录时间段内的日志文件总数);计算下一个触发时间点后退出函数;

(3) 若当前时间点小于nextCheck,则进行文件大小的校验(通过isTooSoon判断函数触发是否过于频繁,如果时,则退出等待以后校验);

(4) 比较当前日志文件和设置的最大文件大小比较,如果当前文件大小达到阈值,则计算新的日志文件名前缀,currentPeriodsCounter进行+1操作.

2.2.2 日志翻转

    public void rollover() throws RolloverFailure {

        // when rollover is called the elapsed period's file has
// been already closed. This is a working assumption of this method. String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName); if (compressionMode == CompressionMode.NONE) {
if (getParentsRawFileProperty() != null) {
renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
} // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == null }
} 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);
}
}

主要步骤:

(1) 获取isTriggeringEvent函数设置的elapsedPeriodsFileName文件名称;

(2) 如果不需要对日志文件进行压缩操作,则尝试将当前日志文件的名称重命名为elapsedPeriodsFileName;

(3) 如果需要对日志文件进行压缩,则尝试将日志文件进行异步压缩操作(需要注意,涉及到日志文件重命名操作);

(4) 设置archiveRemover,将当前时间点传入archiveRemover,通过其删除过期文件,或删除早期文件以保证文件夹大小在合理范围.

划重点:

(1) getParentsRawFileProperty: 配置文件中可以设置活动日志文件名称(简称rawFileName),当日志文件达到触发条件时,将日志文件内容转移至翻转文件中,重新创建日志文件并命名为rawFileName;

(2) renameUtil.rename:日志文件转移时,会判断当前日志文件和翻转文件是否在同一块volume上,如果是则重命名文件即可,如果不是则复制当前文件内容至翻转文件中;

(3) 以上Future任务均是提交至Logback的线程池中执行,以保证日志记录的稳定性,避免成为应用的性能负担.

总结

通过以上篇幅可知,对于日志文件的翻转和写入,Logback均进行了细致和合理的设计,保证了日志组件的高可用性和性能.

在编写应用程序,涉及IO操作时,不妨参考Logback的代码编写.

PS:

如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!

Logback的AsyncAppender与RollingFileAppender流程解析的更多相关文章

  1. TCP/IP协议三次握手与四次握手流程解析

    原文链接地址:http://www.2cto.com/net/201310/251896.html TCP/IP协议三次握手与四次握手流程解析 TCP/IP协议的详细信息参看<TCP/IP协议详 ...

  2. SSL/TLS算法流程解析

    SSL/TLS 早已不是陌生的词汇,然而其原理及细则却不是太容易记住.本文将试图通过一些简单图示呈现其流程原理,希望读者有所收获. 一.相关版本 Version Source Description ...

  3. TCP/IP协议三次握手与四次握手流程解析(转载及总结)

    原文地址:http://www.2cto.com/net/201310/251896.html,转载请注明出处: TCP/IP协议三次握手与四次握手流程解析 一.TCP报文格式  TCP/IP协议的详 ...

  4. Django生命周期 URL ----> CBV 源码解析-------------- 及rest_framework APIView 源码流程解析

    一.一个请求来到Django 的生命周期   FBV 不讨论 CBV: 请求被代理转发到uwsgi: 开始Django的流程: 首先经过中间件process_request (session等) 然后 ...

  5. [MapReduce_3] MapReduce 程序运行流程解析

    0. 说明 Word Count 程序运行流程解析 &&  MapReduce 程序运行流程解析 1. Word Count 程序运行流程解析 2. MapReduce 程序运行流程图

  6. HBase - 数据写入流程解析

    本文由  网易云发布. 作者:范欣欣 本篇文章仅限内部分享,如需转载,请联系网易获取授权. 众所周知,HBase默认适用于写多读少的应用,正是依赖于它相当出色的写入性能:一个100台RS的集群可以轻松 ...

  7. EurekaClient自动装配及启动流程解析

    在上篇文章中,我们简单介绍了EurekaServer自动装配及启动流程解析,本篇文章则继续研究EurekaClient的相关代码 老规矩,先看spring.factories文件,其中引入了一个配置类 ...

  8. Mysql流程解析

    Mysql流程解析 流程图 流程图解析 客户端发送一条sql语句. 1.此时,mysql会检查sql语句,查看是否命中缓存,如果命中缓存,直接返回结果,不继续执行.没有命中则进入解析器. 2.解析器会 ...

  9. Session (简介、、相关方法、流程解析、登录验证)

    Session简介 Session的由来 Cookie虽然在一定程度上解决了"保持状态"的需求,但是由于Cookie本身最大支持4096字节,以及Cookie本身保存在客户端,可能 ...

随机推荐

  1. Spring Boot 中使用 HttpClient 进行 POST GET PUT DELETE

    有的时候,我们的 Spring Boot 应用需要调用第三方接口,这个接口可能是 Http协议.可能是 WebService.可能是 FTP或其他格式,本章讨论 Http 接口的调用. 通常基于 Ht ...

  2. 11. 搭建一个完整的K8S集群

    11. 搭建一个完整的Kubernetes集群 1. kubectl的命令遵循分类的原则(重点) 语法1: kubectl 动作 类 具体的对象 例如: """ kube ...

  3. Educational Codeforces Round 82 B. National Project

    Your company was appointed to lay new asphalt on the highway of length nn. You know that every day y ...

  4. java 抛出异常与finally的混用对于语句块的执行顺序的影响

    代码如下: package test1; public class EmbededFinally { public static void main(String args[]) { int resu ...

  5. JPA 级联保存的问题

    前提:系统有学校-学生关系,学校可以包含多个学生,学生只能属于一个学校 在使用 spring-data-jpa 的时候,保存学校的同时保存学生信息,不需要先逐个保存学生信息,再将学生信息放在学校中保存 ...

  6. Windows下MySQL5.7版本中修改编码为utf-8

    我们新安装的MySQL数据库默认的字符是 latin1 ,所以每次新建数据库都要修改字符,非常麻烦.所以我们必须将它改成UTF8字符的. 修改方法如下: 一.修改MySQL的my.ini 首先在 \P ...

  7. 【协作式原创】查漏补缺之Golang中mutex源码实现

    概览最简单版的mutex(go1.3版本) 预备知识 主要结构体 type Mutex struct { state int32 // 指代mutex锁当前的状态 sema uint32 // 信号量 ...

  8. updataxml报错注入

    // take the variables//接受变量 // //也就是插入post提交的uname和passwd,参见:https://www.w3school.com.cn/sql/sql_ins ...

  9. javaweb使用button的onclick属性访问servlet

    1.定义一个servlet: 如我定义了一个名称为Choose_class.java的servlet 2.定义一个button <input type="button"  v ...

  10. HHR计划---作业复盘-直播第三课

    一,出租车广告: 1,三个点不合格:周期太长了,大而全互联网产品,不符合MVP原则:业务关键点丢掉了:没有业务认知和成长. 2,关键假设: (1)车主有没有需求呀,画像怎么样? (2)车主收入如何,能 ...