最新做项目,发现一些历史遗留问题,典型的是日志打印的配置问题,其实都是些简单问题,但是往往简单问题引起严重的事故,比如日志打印阻塞工作线程,以logback和log4j2为例。logback实际上是springboot的官方默认日志实现框架,承载SLF4J-API,所以基于java开发的云原生项目基本上就是logback打印日志,logback异步appender日志的打印架构
可以看到consoleAppender实际上也是异步(非同步)的范畴
springboot的demo,这个可以用Spring官方的脚手架生成
其中启动的console日志就是logback打印的,虽然我们没有配置logback的xml。而现实情况是需要配置文件的,毕竟需要异步打印日志,日志切割,保存时间等都需要配置,关键还要日志格式。
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
${log.path}/app.log
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
UTF-8
${log.path}/app.%d{yyyy-MM-dd}.%i.log.gz
200MB
10
0
1024
true
笔者参考各个网站简单写了一个xml
那么为什么日志打印会阻塞工作线程了,不是异步的嘛吗?异步是没错,但是异步解耦却是依赖队列,不同于分布式MQ,本地队列在某些配置时是阻塞的,所以异步日志实际上是半同步,有点像MySQL的复制原理,架构设计实际上很多地方非常相似。如果不想丢日志,可以提高消费队列日志线程的数量,增加CPU资源消耗。
参考logback官方文档:Chapter 4: Appenders
这个就是前言的代码分析,console也是属于异步范畴。
AsyncAppender配置如下
这里关键有3个配置
最简单的架构图,写文件是异步线程,但是写queue是同步的
ch.qos.logback.classic.AsyncAppender,关键还是父类ch.qos.logback.core.AsyncAppenderBase
/**
* In order to optimize performance this appender deems events of level TRACE,
* DEBUG and INFO as discardable. See the
* chapter
* on appenders in the manual for further information.
*
*
* @author Ceki Gülcü
* @since 1.0.4
*/
public class AsyncAppender extends AsyncAppenderBase {
boolean includeCallerData = false;
/**
* Events of level TRACE, DEBUG and INFO are deemed to be discardable.
* 定义丢弃日志的级别,文档写的就是这里实现的
*
* @param event
* @return true if the event is of level TRACE, DEBUG or INFO false otherwise.
*/
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();
}
这个类核心还是,按照级别丢日志的定义,比如queue能存储的大小少于1/5时,那些级别日志丢弃 ,再看父类启动的时候分析初始值,发现queue是
ArrayBlockingQueue
定义了队列discardingThreshold的值,注意:这个是队列数信号量,不是百分比,发现一些业务配置20,
public static final int DEFAULT_QUEUE_SIZE = 256; int queueSize = DEFAULT_QUEUE_SIZE;
默认队列数256,建议配置大一点,根据内存分配情况,过大会OOM
在分析日志入队列的过程
分析
ArrayBlockingQueue
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full. This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
队列数满,直接丢弃,所以不阻塞,丢失日志。
log4j2实际上根据各方测试说,比logback性能强一些,但是也会出现同样的问题
官方文档:Log4j – Log4j 2 Appenders
apache开源的文档管理要好一些,写的很详细,而且有详细的说明和示例,不过设计原理都差不多
实际上这个问题是使用问题,非常简单,不过越是简单的使用,却可能出现致命问题,一般公司都会统一脚手架或者统一规范的方式来实现标准的日志配置文件,防止错误配置导致业务问题,不知道未来Java虚拟线程大规模使用会不会缓解日志打印阻塞工作线程的问题,毕竟调度更优,不过如果线程池满载,虚拟线程也是无能为力。还是需要在丢日志和存储消费日志的能力作取舍。