简介Logger和Appender的异步化配置和基本原理
前面的博客里,我简单介绍过了Log4j2的简单配置和其中基本组件LogManager,LoggerContext以及Configuration的机制和流程。而还有两个关键的组件Logger和Appender,他们是对msg做真正处理的关键组件,在众多类型的Logger和Appender中,我主要想把目光集中在其中比较特定的,异步的Logger和Appender。如果大家平时只是简单使用Log4j,可能对异步的Logger和Appender有些分不清,或者并未配置过这类的异步化组件。我将用两篇文章分别介绍异步Appender和异步Logger。
前面的博客中,我介绍过,记录日志的代码,这里为了讲述方便,我重新把代码放上来
Logger logger = LogManager.getLogger(loggerName); logger.info(msg);
这篇文章主要聚焦于异步性的实现,所以对于获取logger和调用info操作的通用流程我直接用两张流程图给出:
了解了通用流程后,我将分别介绍异步Appender和异步Logger的原理
我们以官方文档的两个配置为例来介绍异步Appender的配置,配置文件如下
<Configuration status="warn" name="MyApp" packages="">
<Appenders>
<File name="MyFile" fileName="logs/app.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
<Async name="Async">
<AppenderRef ref="MyFile"/>
</Async>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Async"/>
</Root>
</Loggers>
</Configuration>
<Configuration name="LinkedTransferQueueExample">
<Appenders>
<List name="List"/>
<Async name="Async" bufferSize="262144">
<AppenderRef ref="List"/>
<LinkedTransferQueue/>
</Async>
</Appenders>
<Loggers>
<Root>
<AppenderRef ref="Async"/>
</Root>
</Loggers>
</Configuration>
配置中配置了两个Appender,一个为FileAppender另一个则是异步Appender,使用 <Async> 标签进行声明,在AsyncAppender中引用了FileAppender,那么存在疑问,一般的Appender都会有一个明确的输出位置,而对于这个异步Appender都需要引用一个其他的Appender才将msg最终输出。下面的介绍将会彻底解开这个疑问。
下图展示了AsyncAppender的架构
AsyncAppender的核心部件是一个阻塞队列,logger将数据通过append方法放入到阻塞队列中,随后后台线程从队列中取出数据然后进行后续的操作
上文简单介绍了架构,下面从源码角度来详细阐述AsyncAppender的流程
public final class AsyncAppender extends AbstractAppender {
private static final int DEFAULT_QUEUE_SIZE = 128;
private static final LogEvent SHUTDOWN = new AbstractLogEvent() {
};
private static final AtomicLong THREAD_SEQUENCE = new AtomicLong(1);
private final BlockingQueue<LogEvent> queue;
private final int queueSize;
private final boolean blocking;
private final long shutdownTimeout;
private final Configuration config;
private final AppenderRef[] appenderRefs;
private final String errorRef;
private final boolean includeLocation;
private AppenderControl errorAppender;
private AsyncThread thread;
private AsyncQueueFullPolicy asyncQueueFullPolicy;
}
可以看到其中的一些关键属性有一个阻塞队列queue,一个后台线程thread,一个AppenderRef的数组appenderRefs以及一个关键属性blocking。对于一个任何一个Appender对象,我们都应该关注他的append()而方法,对于一个后台线程,重要的方法则是run方法,对于AsyncAppender,这两个方法刚好对应了AsyncAppender的两个核心步骤,即放入消息以及处理消息,下面将分别说明。
废话不多说,线上代码:
@Override
public void append(final LogEvent logEvent) {
if (!isStarted()) {
throw new IllegalStateException("AsyncAppender " + getName() + " is not active");
}
if (!Constants.FORMAT_MESSAGES_IN_BACKGROUND) { // LOG4J2-898: user may choose
logEvent.getMessage().getFormattedMessage(); // LOG4J2-763: ask message to freeze parameters
}
final Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation);
if (!transfer(memento)) {
if (blocking) {
// delegate to the event router (which may discard, enqueue and block, or log in current thread)
final EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel());
route.logMessage(this, memento);
} else {
error("Appender " + getName() + " is unable to write primary appenders. queue is full");
logToErrorAppenderIfNecessary(false, memento);
}
}
}
可以看到这个appende方法流程并不复杂,只有以下两步:
1、创建LogEvent的复制对象memento 2、将event放入队列
虽然只有简单两步,也需要注意一个边界情况,那就是当阻塞队列满时Appender的处理,这里我首先给出流程,然后结合代码进行简要说明。
如流程图所示,首先会判断用户是否设置了blocking选项,如果未选择blocking选项,则Appender直接会将msg放入errorAppender中,如果用户没有配置这些Appender,则会直接丢弃这些消息,如果设置了这个属性,则会按照一定的策略来处理这些消息。策略可以分为3种,他们分别为:
1、Default---等待直到队列有空闲,退化为同步操作 2、Discard---按照日志级别丢弃一部分日志 3、用户自定义(需要实现AsyncQueueFullPolicy接口)
当使用append方法将消息放入阻塞队列后,后台的线程将会一步的进行处理,这也就是异步线程的run方法的功能所在,首先简单看其数据结构
private class AsyncThread extends Log4jThread {
private volatile boolean shutdown = false;
private final List<AppenderControl> appenders;
private final BlockingQueue<LogEvent> queue;
}
AsyncThread这是一个内部类,其中包含一个外部Appender类的阻塞队列,还有对应的AsyncAppender所引用的Appender。接下来我们详细看其中的run方法
public void run() {
while (!shutdown) {
LogEvent event;
try {
event = queue.take();
if (event == SHUTDOWN) {
shutdown = true;
continue;
}
} catch (final InterruptedException ex) {
break; // LOG4J2-830
}
event.setEndOfBatch(queue.isEmpty());
final boolean success = callAppenders(event);
if (!success && errorAppender != null) {
try {
errorAppender.callAppender(event);
} catch (final Exception ex) {
// Silently accept the error.
}
}
}
// Process any remaining items in the queue.
LOGGER.trace("AsyncAppender.AsyncThread shutting down. Processing remaining {} queue events.",
queue.size());
int count = 0;
int ignored = 0;
while (!queue.isEmpty()) {
try {
final LogEvent event = queue.take();
if (event instanceof Log4jLogEvent) {
final Log4jLogEvent logEvent = (Log4jLogEvent) event;
logEvent.setEndOfBatch(queue.isEmpty());
callAppenders(logEvent);
count++;
} else {
ignored++;
LOGGER.trace("Ignoring event of class {}", event.getClass().getName());
}
} catch (final InterruptedException ex) {
// May have been interrupted to shut down.
// Here we ignore interrupts and try to process all remaining events.
}
}
LOGGER.trace("AsyncAppender.AsyncThread stopped. Queue has {} events remaining. "
+ "Processed {} and ignored {} events since shutdown started.", queue.size(), count, ignored);
}
可以看到,异步线程的逻辑比较简单,该线程会一直尝试从阻塞队列中获取logEvent数据,如果能够成功获取数据,则会调用AppenderRef所引用Appender的append方法,通过这个方法,我们可以看到,实际上,AsyncAppender可以看做一个中转站,其作用仅仅将消息的处理异步化,当消息放入阻塞队列后,info方法就能返回成功,这样能够大幅提高日志记录的吞吐,同时,用户可以自行权衡性能与日志收集质量上进行权衡(设置blocking选项),此外,用户还可以设置不同类型的阻塞队列已到达更好的日志记录吞吐。
最后,让我们整体来看AsyncAppender所支持的所有配置项以及其中每个配置项的作用
| 名称 | 类型 | 描述 | 默认值 |
|---|---|---|---|
| AppenderRef | String | 引用的Appender | |
| blocking | boolean | 是否阻塞等待(这里指队列满后的处理) | true |
| shutdownTimeout | integer | appender关闭时等待的超时时间 | 0(立刻关闭) |
| bufferSize | integer | 阻塞队列的最大容量 | 1024 |
| errorRef | String | 队列满后如果不阻塞时配置的errorAppender | |
| filter | Filter | 过滤器 | |
| name | String | 名称 | |
| ignoreExceptions | boolean | 用于决定是否需要记录在日志事件处理过程中出现的异常 | true |
| BlockingQueueFactory | BlockingQueueFactory | Buffer的种类(默认ArrayBlockingQueue,能够支持DisruptorBlockingQueue,JCToolsBlockingQueue,LinkedTransferQueue) | ArrayBlockingQueueFactory |
至此,我结合源码简单介绍了AsyncAppnder的使用配置以及基本原理,在下一篇文章中,我将介绍另一个异步化组件AsyncLogger
谢谢你请我吃糖果