log4j2最佳实践4-高性能 log4j 2 疑难杂症 - 怎样正确开启全局异步 怎么验证是否真正开启了
核心参数 log4j2.contextSelector
log4j2 的高性能,官网已经吹嘘的很多了。但是很多人不知道,那是在 开启了 全局异步功能后的对比。官网只有一句如下图的提示语,导致很多人不知道 怎么开启。

很多人用 很多人用System.setProperty("name","value") 是错的 ,并不能生效,项目 的log依然是同步模式,但是却自以为开启了全部异步,美滋滋 ,呵呵

比如 非常辛勤高产的芋道代码的文章【阅读芋道这篇文章需要关注它的2个微信号,我帮你关注了,验证码是coke】 https://www.iocoder.cn/Fight/Log4j1-and-Logback-and-Log4j2-performance-test/

怎么正确设置该参数?
1) 首先,确保 pom 文件中 引入 了 jar包。这是开启disruptor必须的jar包 。spring默认是logback,不包含这个jar,所以需要手动引入。
-
<dependency>
-
<groupId>com.lmax</groupId>
-
<artifactId>disruptor</artifactId>
-
<version>3.4.2</version> <!--版本号你可以自定义 -->
-
</dependency>
2)正确的做法是 在 目录 src/main/resources 下面,新建文件 log4j2.component.properties ,在其内部将参数的值设置一下
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

3)其他正确做法:
3.1 命令行启动参数 ,-D 参数的含义是 将参数作为 JVM 的系统属性,可以用 System.getProperty(name) 获取到 。注意,-D类型的参数并不是修改操作系统的参数,而是修改当前 JVM 实例的参数。
java -jar 你的项目名.jar -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
3.2 直接添加 JVM 参数 到idea的启动配置的 VM options上 。加到 其他地方都是无效的 !

4)错误的做法: 因为 log4j2作为jar包library,加载肯定早于 我们手写的源码的main 方法和 springboot的启动类的 静态块,所以一下方法都是错误的,不会真正开启 全局异步功能的,这恐怕是很多人都会 犯的错!
4.1【该设置无效】在 springboot启动类中添加静态块 ,设置环境变量 。
-
-
public class SpringbootApplication {
-
static {
-
/**
-
* 2种设置 都是无效的,因为 log4j2 的加载早于 springboot启动类的 static 代码块,更早于 main方法
-
*/
-
System.setProperty("Log4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
-
System.setProperty("-DLog4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
-
}
-
}
4.2【该设置无效】修改springboot的环境变量。以下两种方式都是无效的,因为springboot的环境变量 加载晚于log4j2 。
-
/**
-
* 自定义一个 yml文件 config.yml ,将 属性值加进去 。然后强制让 spring加载该 yml配置
-
* Log4jContextSelector: org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
-
*/
-
public class EnvironmentPostProcessorExample implements EnvironmentPostProcessor {
-
private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
-
-
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
-
Resource path = new ClassPathResource("com/example/myapp/config.yml");
-
PropertySource<?> propertySource = loadYaml(path);
-
environment.getPropertySources().addLast(propertySource);
-
}
-
private PropertySource<?> loadYaml(Resource path) {
-
if (!path.exists()) {
-
throw new IllegalArgumentException("Resource " + path + " does not exist");
-
}
-
try {
-
return this.loader.load("custom-resource", path).get(0);
-
}
-
catch (IOException ex) {
-
throw new IllegalStateException("Failed to load yaml configuration from " + path, ex);
-
}
-
}
-
}
上面的无效,下面的 添加启动过程的监听器,依然无效,原理一样,都是修改 springboot的环境变量,为时已晚。 log4j2 这之前就 都加载配置,启动完成了。
-
/**
-
* 自定义 监听器,在使用环境变量 之前,修改或者新增环境变量
-
* 记得 把这个 监听器,加入springboot 中 。否则这个监听器的代码不会执行 SpringApplication.addListeners(…) method or the SpringApplicationBuilder.listeners(…)
-
* 参考 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-application-events-and-listeners
-
*
-
* 另外, implements EnvironmentAware 也能获取到 环境变量
-
*/
-
-
public class Application2EnvPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
-
-
-
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
-
ConfigurableEnvironment env = event.getEnvironment();
-
// 在这里设置 log4j2 是无法生效的,因为log4j2 的启动和加载更早
-
// 生效有2法:1 命令行启动参数加上 -D上面的命令 ; 或者 2 添加 外置文件 log4j2.component.properties ,设置好参数。
-
setPropertyIfAbsent(env,"Log4jContextSelector","org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
-
}
-
-
private void setPropertyIfAbsent(ConfigurableEnvironment environment, String propertyName,Object value) {
-
String property = environment.getProperty(propertyName);
-
if (StringUtils.isBlank(property)) {
-
String envType = "systemProperties"; // 默认设置为 JVM 环境变量
-
setProperty(environment, envType, propertyName, value);
-
log.info("set property ok, {}={}", propertyName, environment.getProperty(propertyName));
-
return;
-
}
-
log.warn("failed to set property [{}={}] ,because {}={}", propertyName, value, propertyName, property);
-
}
-
}
5) 原理:log4j2 启动的时候,会加载 这个 目录下的该文件。代码在 org.apache.logging.log4j.util.PropertiesUtil

初始化 log4jContextFactory的时候,会 加载我们写好的那个属性值 创建 logFatory 实例 ; 如果没有找到 属性值,那么就会创建一个默认的,自然就是 同步的logger了 (这块我没有深究,请读者子自行去追踪一下)。
代码在 org.apache.logging.log4j.core.impl.Log4jContextFactory


怎么验证是否正确开启了全局异步功能
原理:开启异步后,肯定会加载 jar包 执行 disruptor的 代码的,这是高性能的关键api代码。所以 ,
验证方法1:我们只需要 把 pom中 的 这个依赖关闭,看看项目是否依然能正常启动,就知道 是否 开启了 log4j2的异步功能了。
假如注释掉 disruptor jar包,项目依然能 正常启动,则 表示 log4j2的 全局异步功能并没有开启!
假如注释掉 disruptor 的jar包后,启动报错如下,恭喜你,表示 正确开启了全局功能了。此时只需要 取消注释 disruptor的jar包,就可以 使用 真正的全局异步高性能了。

注意: 修改 pom文件后,可能不能立刻生效,请确保开启了 reload project after any changes 如图

验证方法2: 代码验证 ,在 springboot 启动类的 第一行,执行静态代码块 断言。prod 生产环境,不推荐使用断言,因为无法tcy catch 断言的异常,会导致项目启动失败。
该段代码来源于 log4j2的源码,org.apache.logging.log4j.core.async.AsyncLoggerContextSelector#isSelected
-
static {
-
Assert.isTrue(AsyncLoggerContextSelector.isSelected(), "log4j2 的异步 disruptor启动失败");
-
}

参考来源: 感谢 stackoverflow 的大神解答,非常高效,答案也很精准。
setting Log4jContextSelector system property for asynchronous logging
Does setting Property of Log4jContextSelector make any difference
高性能 log4j 2 疑难杂症 - 怎样正确开启全局异步 怎么验证是否真正开启了_log4jcontextselector-CSDN博客
log4j2 xsd_Log4j 2.x XSD的描述不完整-CSDN博客
log4j2 ThresholdFilter onMatch/onMismatch neutral/accept_onmismatch="neutral-CSDN博客

一、前言
最近遇到了个log4j2写日志导致线程阻塞的问题(多亏了开发小哥日志打的多,不然就没有下面这一系列骚操作)。
大致描述下当时的情况(内网限制,没法把现场东西拿出来,只能口述了):
log4j2配置情况: 同时配置了3个RollingRandomAccessFile,分别针对SQL语句、INFO日志、ERROR日志,大致的配置如下:
<RollingRandomAccessFile name="RandomAccessFile" fileName="${FILE_PATH}/async-log4j2.log" append="false" filePattern="${FILE_PATH}/rollings/async-testLog4j2-%d{yyyy-MM-dd}_%i.log.gz"> <PatternLayout> <Pattern>${LOG_PATTERN}</Pattern> </PatternLayout> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true" /> <SizeBasedTriggeringPolicy size="450MB"/> </Policies> <DefaultRolloverStrategy max="15" compressionLevel="0"/> </RollingRandomAccessFile>
问题描述: 1、32C的机器压缩日志占用30%+的资源;2、tomcat主线程全部50%+都是park状态,线程状态大致如下;
当时针对log4j2给的优化建议是: 1、配置immediateFlush=false 2、将filePattern对应的gz后缀去掉(因为对应的compressionLevel=0,根本不压缩),是否就不会调用JDK的Deflater进行压缩。【猜测,也是后面还原现场的原因之一,想亲自验证一下】
二、本地复现&部分源码学习
问题复现的过程也是蛮艰辛的,遇到各种问题。下面记录的是我本地复现时遇到的问题以及解决办法,附带一些log4j2基于disruptor的部分源码学习,篇幅可能会稍长。
环境:Macbook Pro x86(16C32G)、jdk1.8、log4j-core 2.12.1、log4j-api 2.12.1、disruptor 3.4.2
测试代码(启动50线程不间断地写日志【现场系统涉及200个Tomcat线程】):
public class TestLog4j { private static Logger logger = LogManager.getLogger(TestLog4j.class); private final ThreadPoolExecutor executor; public TestLog4j() { this.executor = new ThreadPoolExecutor(50, 50, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } public void testLog() { for (int i = 0; i < this.executor.getCorePoolSize(); i++) { this.executor.execute(() -> { while (true) { logger.info("测试日志--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞" + "--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞" + "--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞--麻利麻利哄快阻塞"); } }); } } public static void main(String[] args) { new TestLog4j().testLog(); } }
部分log4j2.xml配置(将备份的压缩日志大小改小至100M,备份文件数量增加至100):
<appenders> <RollingRandomAccessFile name="RandomAccessFile" fileName="${FILE_PATH}/async-log4j2.log" append="false" filePattern="${FILE_PATH}/rollings/async-testLog4j2-%d{yyyy-MM-dd}_%i.log.gz"> <PatternLayout> <Pattern>${LOG_PATTERN}</Pattern> </PatternLayout> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true" /> <SizeBasedTriggeringPolicy size="100MB"/> </Policies> <DefaultRolloverStrategy max="100" compressionLevel="0"/> </RollingRandomAccessFile> </appenders> <loggers> <!--disruptor异步日志--> <AsyncLogger name="DisruptorLogger" level="info" includeLocation="false"> <AppenderRef ref="RandomAccessFile"/> </AsyncLogger> <Asyncroot level="info" includeLocation="false"> <appender-ref ref="RandomAccessFile"/> </Asyncroot> </loggers>
(一)线程阻塞-Blocked
一切准备就绪,点击运行,jps+jstack+jmap,一片自信满满。打开thread dump的那一刻,我就懵了,一片红红的blocked,此时应上问号脸。线程情况是这样的:
感觉和预期差的有点大啊,看样子是在往disruptor的RingBuffer里写日志的时候就blocked了,可以对比一下之前线程的线程情况,是没有blocked的。内存dump中好像发现了不一样的:
RingBuffer只有4096,印象里没有设置的话默认是256*1024。
(1)RingBuffer大小
org.apache.logging.log4j.core.async包下的DisruptorUtil类里定义了很多Disruptor相关的配置属性。
其中有三个RingBuffer size的静态属性,还有一个获取RingBufferSize的方法calculateRingBufferSize。
// DisruptorUtil类 private static final int RINGBUFFER_MIN_SIZE = 128; private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024; private static final int RINGBUFFER_NO_GC_DEFAULT_SIZE = 4 * 1024; static int calculateRingBufferSize(final String propertyName) { // 如果ENABLE_THREADLOCALS为true,则使用RINGBUFFER_NO_GC_DEFAULT_SIZE即4096大小的size int ringBufferSize = Constants.ENABLE_THREADLOCALS ? RINGBUFFER_NO_GC_DEFAULT_SIZE : RINGBUFFER_DEFAULT_SIZE; // 获取配置文件中自定配置大小,如果没有返回上面ringBufferSize final String userPreferredRBSize = PropertiesUtil.getProperties().getStringProperty(propertyName, String.valueOf(ringBufferSize)); try { int size = Integer.parseInt(userPreferredRBSize); // 自定义配置大小小于128,则将size重新赋值为128 if (size < RINGBUFFER_MIN_SIZE) { size = RINGBUFFER_MIN_SIZE; LOGGER.warn("Invalid RingBufferSize {}, using minimum size {}.", userPreferredRBSize, RINGBUFFER_MIN_SIZE); } // 自定义配置大小重新赋值给ringBufferSize ringBufferSize = size; } catch (final Exception ex) { LOGGER.warn("Invalid RingBufferSize {}, using default size {}.", userPreferredRBSize, ringBufferSize); } return Integers.ceilingNextPowerOfTwo(ringBufferSize); }
然后看下Constants.ENABLE_THREADLOCALS就真相大白了:
/** * {@code true} if we think we are running in a web container, based on the boolean value of system property * "log4j2.is.webapp", or (if this system property is not set) whether the {@code javax.servlet.Servlet} class * is present in the classpath. */ public static final boolean IS_WEB_APP = PropertiesUtil.getProperties().getBooleanProperty( "log4j2.is.webapp", isClassAvailable("javax.servlet.Servlet")); /** * Kill switch for object pooling in ThreadLocals that enables much of the LOG4J2-1270 no-GC behaviour. * <p> * {@code True} for non-{@link #IS_WEB_APP web apps}, disable by setting system property * "log4j2.enable.threadlocals" to "false". * </p> */ public static final boolean ENABLE_THREADLOCALS = !IS_WEB_APP && PropertiesUtil.getProperties().getBooleanProperty( "log4j2.enable.threadlocals", true);
大致意思就是,如果应用不是web应用【判断是否存在javax.servlet.Servlet这个类】,就默认使用threadlocals这种模式,即我本地程序的RingBuffer就被设置成了4096。
注释中也提到,可以设置jvm运行时参数,不使用threadlocals这种模式,可以这么设置:-Dlog4j2.enable.threadlocals=false
-
Garbage-free logging
- 大部分日志框架,包括log4j会在正常日志输出的时候创建临时对象( log event objects, Strings, char arrays, byte arrays...),这会增加GC的压力;
- 从Log4j2.6开始,log4j默认使用Garbage-free这种模式。threadlocals是Garbage-free的其中一种实现,在
ThreadLocal基础上,会重用对象(例如log event objects); - 但是在web应用中,threadlocals这种模式很容易导致
ThreadLocal的内存泄漏,所以在web应用中,不会使用threadlocals模式; -
一些不完全Garbage-free的功能,不依赖
ThreadLocal,会将log events对象转换成text,继而将text直接编码成bytes存入可重用的ByteBuffer,而不需要创建零时的Strings, char arrays and byte arrays等对象。- 上述功能默认开始(
log4j2.enableDirectEncoders默认为true),在多线程环境下,日志性能可能会不太理想,因为在使用共享buffer的时候是同步操作。所以考虑性能的话,建议使用异步日志。
- 上述功能默认开始(
参考:https://logging.apache.org/lo...,Garbage-free Logging
(2)阻塞的核心方法enqueue
主要的阻塞点org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor的enqueue方法
private void enqueue(final LogEvent logEvent, final AsyncLoggerConfig asyncLoggerConfig) { // 如果synchronizeEnqueueWhenQueueFull为true,进入阻塞方法 if (synchronizeEnqueueWhenQueueFull()) { synchronized (queueFullEnqueueLock) { disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig); } } else { disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig); } } private boolean synchronizeEnqueueWhenQueueFull() { return DisruptorUtil.ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL // Background thread must never block && backgroundThreadId != Thread.currentThread().getId(); }
DisruptorUtil中关于SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL的两个静态属性:
// 默认都是true static final boolean ASYNC_LOGGER_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties() .getBooleanProperty("AsyncLogger.SynchronizeEnqueueWhenQueueFull", true); static final boolean ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties() .getBooleanProperty("AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull", true);
从源码可以看到,默认enqueue方法就是走阻塞的分支代码。如果要设置成非阻塞的运行方式,需要手动配置参数,方法如下:
新建log4j2.component.properties文件,添加如下配置:
【其他可配置参数详见:https://logging.apache.org/lo...】
# 适用<root> and <logger>加 # Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector配置的异步日志 # 作用于org.apache.logging.log4j.core.async.AsyncLoggerDisruptor AsyncLogger.SynchronizeEnqueueWhenQueueFull=false # 适用<asyncRoot> & <asyncLogger>配置的异步日志 # 作用于org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=false
-
note:
- 异步日志配置方式不同的话,会使用不同的处理类(
AsyncLoggerConfigDisruptor「mix模式/配置文件进行配置」 |AsyncLoggerDisruptor「全局异步配置」),因此配置参数得相匹配; -
将
SynchronizeEnqueueWhenQueueFull设置成false,会导致CPU使用率飙升,这个应该也是默认为true的原因。可以看下面本地实验的结果,差不多是10倍的差距。【官方文档中是不建议设置成false的,除非绑定CPU核。】- true时的CPU使用「159%」:
- false时的CPU使用「1565%」:
- true时的CPU使用「159%」:
- 异步日志配置方式不同的话,会使用不同的处理类(
CPU高的主要原因:
在enqueue方法处没有阻塞的情况下,所有的线程都会进入到MultiProducerSequencer的next方法。这个方法是获取RingBuffer的可写区间的,方法中有个while死循环不断的做CAS操作。在LockSupport.parkNanos(1)处原作者这边也给了疑问:要不要沿用WaitStrategy的等待策略。
@Override public long next(int n) { if (n < 1) { throw new IllegalArgumentException("n must be > 0"); } long current; long next; do { current = cursor.get(); next = current + n; long wrapPoint = next - bufferSize; long cachedGatingSequence = gatingSequenceCache.get(); if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) { long gatingSequence = Util.getMinimumSequence(gatingSequences, current); if (wrapPoint > gatingSequence) { LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy? continue; } gatingSequenceCache.set(gatingSequence); } else if (cursor.compareAndSet(current, next)) { break; } } while (true); return next; }
至此,基本还原现场的情况:
(3)异步日志重复配置的问题
这是个人的好奇之举。当即使用了Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector,又在log4j2.xml中配置了<asyncRoot>标签,日志对象将会多一次中间传递:app -> RingBuffer-1 -> thread a -> RingBuffer-2 -> thread b -> disk。
这会带来不必要的性能损耗。
看下这种情况的线程dump就了然了:
(二)日志压缩
尝试去掉gz后缀名,优化压缩的资源消耗问题。
- 修改前,CPU采样情况,在资源占用前列可以明显看到压缩的相关方法:
- 去掉gz后缀和压缩级别参数,已经找不到压缩相关的方法了:
验证了之前的猜想,RollingFile其实就是根据文件后缀来判断是否进行压缩的。
(三)消费线程(IO线程)的WaitStrategy
即log4j2.asyncLoggerWaitStrategy | log4j2.asyncLoggerConfigWaitStrategy 这两配置,主要用来配置等待日志事件的I/O线程的策略。
官方文档中给出了4种策略:Block, Timeout「默认」, Sleep, Yield。
但是源码中其实有5种:
// org.apache.logging.log4j.core.async.DisruptorUtil static WaitStrategy createWaitStrategy(final String propertyName, final long timeoutMillis) { final String strategy = PropertiesUtil.getProperties().getStringProperty(propertyName, "TIMEOUT"); LOGGER.trace("property {}={}", propertyName, strategy); final String strategyUp = strategy.toUpperCase(Locale.ROOT); // TODO Refactor into Strings.toRootUpperCase(String) switch (strategyUp) { // TODO Define a DisruptorWaitStrategy enum? case "SLEEP": return new SleepingWaitStrategy(); case "YIELD": return new YieldingWaitStrategy(); case "BLOCK": return new BlockingWaitStrategy(); case "BUSYSPIN": return new BusySpinWaitStrategy(); case "TIMEOUT": return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS); default: return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS); } }
多了一个BusySpinWaitStrategy策略「JDK9才真正适用,9以下就是简单的死循环」
这里不一一介绍,主要是disruptor相关的内容「详细内容可以参考这片文章,写的还可以:https://blog.csdn.net/boling_...」,结合log4j2说几点:
- 默认的
TimeoutBlockingWaitStrategy和BlockingWaitStrategy策略都是基于ReentrantLock实现的,由于底层是基于AQS,在运行过程中会创建AQS的Node对象,不符合Garbage-free的思想; SLEEP是Garbage-free的,且官方文档提到,相较于BLOCK适用于资源受限的环境,SLEEP平衡了性能和CPU资源两方面因素。
三、总结
关于log4j2的性能优化,总结以下几点「后面有补充会添加到这里」
- 配置滚动日志的时候,若不需要压缩日志,filePattern的文件名不要以gz结尾;
- 使用Disruptor异步日志的时候,不要同时使用
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector和<asyncRoot>; - 给
RollingRandomAccessFile配置immediateFlush="false"属性,这样让I/O线程批量刷盘(这里其实涉及到native方法调用的性能问题); - 可以结合资源情况是否要配置
SynchronizeEnqueueWhenQueueFull为false; - 结合实际情况是否要更改I/O线程的
WaitStrategy; - 若日志可以丢弃,可以配置丢弃策略,配置
log4j2.asyncQueueFullPolicy=Discard,log4j2.discardThreshold=INFO「默认」,当队列满时,INFO,DEBUG和TRACE级别的日志会被丢弃;
java - Log4j2基于Disruptor异步日志优化(部分源码学习) - 个人文章 - SegmentFault 思否
Disruptor如何搭配Log4j2日志对大型系统很重要,在排查系统问题的时候,主要依靠日志。 日志很重要,但又能不影 - 掘金
日志对大型系统很重要,在排查系统问题的时候,主要依靠日志。
日志很重要,但又能不影响程序的性能,比如一个接口的响应时间本来是 50 ms,结果加了日志之后,变成了 100 ms,这肯定没办法接受。
让日志不影响系统性能的方式有以下的思路:
- 减少日志量
- 不在日志中做字符串拼接,而使用占位符的方式来拼接日志
- 将日志记录的方式从同步变成异步
减少日志量不是非常可行,在某些情况下,就是需要记录比较多的日志,而且这种方式需要依靠开发人员的自觉,很难完全控制。
不使用字符串拼接的方式比较好实现,现在主流的日志框架中都实现了使用占位符的方式来记录日志信息,这样能够节省大量拼接字符串的时间,这种方式好实现,这种方式已经成为日志记录中的标配。
而本文重点要说的是第三点,这种方式对性能的提升也是最大的,但不是所有的日志框架都支持,而 Log4j2 就支持,之前我写过一篇对 Log4j2 的简介,可以点击查看。
Log4j2 工作流程
Log4j2 运行的流程如下所示:
Log4j2 主要由三个组件组成,Logger、Layout、Appender。
Logger 就是在程序中用来记录日志的对象:
private static final Logger logger = LogManager.getLogger(Log4j2HelloWorld.class);
Layout 则用来对日志信息进行格式化:
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
Appender 用来配置日志的展现形式,可以将日志在控制台打印,也可以将存储到文件,数据库,或者外部的消息队列等等。
默认情况下,一条日志信息需要被 Appender 处理完成之后才算是记录完成,在记录日志的过程中,程序不会往下执行,如果日志很大,就会对程序的性能造成影响,整个过程是同步的,所以优化的思路是把日志记录的过程变成异步,让日志记录不会影响程序的执行。
所以 Logger 在获取到日志信息之后,不会立马进行日志格式化和存储,会先把日志信息放到一个队列中,如下图所示:
因为系统中日志产生的速度非常快,所以要求这个队列的性能很好,如果日志的处理跟不上日志的产生速度,那么就会造成日志的信息的丢失,这也是我们不希望看到的。
Disruptor 对 Log4j2 的改造
Disruptor 有着非常好的性能,刚好满足上面我们所提到的要求,在 Log4j2 中加上 Disruptor 之后,我们的日志处理流程就变成了下面这样:
比较有意思的是,Log4j2 中只在 Logger 这块使用了 Disruptor,按照上面的思路,其实 Appender 也可以进行异步处理,Log4j2 也确实提供了异步的方式,但是是使用 ArrayBlockingQueue,而没有使用 Disruptor, 而且官方也不推荐使用异步 Appender。
我理解为既然要进行异步,那么就彻底一点,就应该把日志的所有处理都异步进行处理,如果使用异步 Appender,日志的格式化还是要进行同步处理。
回到正题,Disruptor 是如何在 Log4j2 中使用的呢?
在 Log4j2 中,有一个 AsyncLogger,这个类完成了对 Disruptor 的包装,类结构如下:
AsyncLogger 中调用了 AsyncLoggerDisruptor,这个类才真正了完成异步日志处理。
一个 Disruptor 官方 Demo 如下:
LongEventFactory factory = new LongEventFactory();
int bufferSize = 1024;
Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(factory, bufferSize, DaemonThreadFactory.INSTANCE);
disruptor.handleEventsWith(new LongEventHandler());
disruptor.start();
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
LongEventProducerWithTranslator producer = new LongEventProducerWithTranslator(ringBuffer);
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++) {
bb.putLong(0, l);
producer.onData(bb);
Thread.sleep(1000);
}
上面是官方的一个例子,从上面可以发现,使用 Disruptor 时,需要以下这些组件:
- Factory: 用户创建消息对象
- Disruptor 对象: disruptor 启动之后会以一个独立的线程在后台运行
- Rinbuffer: 用来存储消息
- Producer: 生产者,用来生产消息
- EventHandler: 消费者,处理消息
同样,在 Log4j2 中,也是同样的使用方式,AsyncLoggerDisruptor 中有一个 start 方法,在这个方法中,同样也是完成了对这些组件的初始化:
disruptor = new Disruptor<>(RingBufferLogEvent.FACTORY, ringBufferSize, threadFactory, ProducerType.MULTI, waitStrategy);
生成对象的工厂使用的是 RingBufferLogEvent.FACTORY,这是 RingBufferLogEvent 中的工厂单例,主要用来生成日志对象实例。
RingBufferLogEventHandler 是日志的消费者,用来处理日志信息,这里的实现很有意思,在 Handler 中,还是调用了 RingBufferLogEvent 的 execute 方法来进行日志的处理,而实际日志会被怎么处理,还是要看具体的配置,这在程序运行的时候才能决定。
public void onEvent(final RingBufferLogEvent event, final long sequence,
final boolean endOfBatch) throws Exception {
try {
event.execute(endOfBatch);
}
finally {
}
}
使用过日志就知道,日志有很多的重载方法,这是为了应对更多的场景,所以对应 Disruptor 中的生产者也有很多的实现,生产者都在 AsyncLogger 中实现,
private final TranslatorType threadLocalTranslatorType = new TranslatorType() {
@Override
void log(String fqcn, StackTraceElement location, Level level, Marker marker, Message message,
Throwable thrown) {
logWithThreadLocalTranslator(fqcn, location, level, marker, message, thrown);
}
@Override
void log(String fqcn, Level level, Marker marker, Message message, Throwable thrown) {
logWithThreadLocalTranslator(fqcn, level, marker, message, thrown);
}
};
上面的代码就是生产者的一种实现。
其实 Log4j2 中使用 Disruptor 也没有什么特别的地方,但是却把同步记录日志的机制换成了高性能的异步记录方式。
<?xml version="1.0" encoding="UTF-8"?>
<!-- Don't forget to set system property
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
to make all loggers asynchronous. -->
<Configuration status="WARN">
<!--变量配置:此处的变量都是自定义的 -->
<Properties>
<!--module name:服务名 -->
<property name="MODULE_NAME" value="yourselfLogName"/>
<!--log.pattern:日志输出的前缀格式 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%traceId] [%logger{60}:%L] - %msg%n"/>
<!--TIME_INTERVAL:日志分割的时间间隔,时间单位是根据filePattern来定的 -->
<property name="TIME_INTERVAL" value="1"/>
<!--maxFileSize:单个日志文件的最大大小 -->
<property name="MAX_FILE_SIZE" value="200 MB"/>
<!-- TOTAL_SIZE_CAP:不同日志级别的日志文件总磁盘占用的阈值 -->
<property name="TOTAL_SIZE_INFO" value="30 GB"/>
<property name="TOTAL_SIZE_WARN" value="5 GB"/>
<property name="TOTAL_SIZE_ERROR" value="5 GB"/>
<!--maxHistory:日志的最大留存时间 -->
<property name="MAX_HISTORY" value="P15D"/>
<!--maxHistory:日志分割最大随机延迟的秒数 -->
<property name="MAX_RANDOMDELAY" value="300"/>
<!-- level:日志级别 -->
<property name="LEVEL_INFO" value="info"/>
<property name="LEVEL_WARN" value="warn"/>
<property name="LEVEL_ERROR" value="error"/>
<!-- log path:日志输出的路径,可以配置相对路径、绝对路径、路径软连接 -->
<property name="LOG_PATH" value="yourselfLogPath"/>
</Properties>
<Appenders>
<!-- Console -->
<console name="CONSOLE" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
</console>
<!-- INFO_FILE -->
<RollingRandomAccessFile name="INFO_FILE" fileName="${LOG_PATH}/${MODULE_NAME}-${LEVEL_INFO}.log"
immediateFlush="false"
append="true"
filePattern="${LOG_PATH}/${MODULE_NAME}-${LEVEL_INFO}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<!-- 日志分割策略 -->
<Policies>
<TimeBasedTriggeringPolicy interval="${TIME_INTERVAL}" maxRandomDelay="${MAX_RANDOMDELAY}"/>
<SizeBasedTriggeringPolicy size="${MAX_FILE_SIZE}"/>
</Policies>
<!-- 日志删除策略 -->
<DefaultRolloverStrategy fileIndex="nomax">
<Delete basePath="${LOG_PATH}" maxDepth="1" followLinks="true">
<IfFileName glob="${MODULE_NAME}-${LEVEL_INFO}*.log">
<IfAny>
<IfAccumulatedFileSize exceeds="${TOTAL_SIZE_INFO}"/>
<IfLastModified age="${MAX_HISTORY}"/>
</IfAny>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
<!-- 日志级别 -->
<Filters>
<ThresholdFilter level="${LEVEL_WARN}" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="${LEVEL_INFO}" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</RollingRandomAccessFile>
<!-- WARN_FILE -->
<RollingRandomAccessFile name="WARN_FILE" fileName="${LOG_PATH}/${MODULE_NAME}-${LEVEL_WARN}.log"
immediateFlush="false"
append="true"
filePattern="${LOG_PATH}/${MODULE_NAME}-${LEVEL_WARN}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<!-- 日志分割策略 -->
<Policies>
<TimeBasedTriggeringPolicy interval="${TIME_INTERVAL}" maxRandomDelay="${MAX_RANDOMDELAY}"/>
<SizeBasedTriggeringPolicy size="${MAX_FILE_SIZE}"/>
</Policies>
<!-- 日志删除策略 -->
<DefaultRolloverStrategy fileIndex="nomax">
<Delete basePath="${LOG_PATH}" maxDepth="1" followLinks="true">
<IfFileName glob="${MODULE_NAME}-${LEVEL_WARN}*.log">
<IfAny>
<IfAccumulatedFileSize exceeds="${TOTAL_SIZE_WARN}"/>
<IfLastModified age="${MAX_HISTORY}"/>
</IfAny>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
<!-- 日志级别 -->
<Filters>
<ThresholdFilter level="${LEVEL_ERROR}" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="${LEVEL_WARN}" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</RollingRandomAccessFile>
<!-- ERROR_FILE -->
<RollingRandomAccessFile name="ERROR_FILE" fileName="${LOG_PATH}/${MODULE_NAME}-${LEVEL_ERROR}.log"
immediateFlush="false"
append="true"
filePattern="${LOG_PATH}/${MODULE_NAME}-${LEVEL_ERROR}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<!-- 日志分割策略 -->
<Policies>
<TimeBasedTriggeringPolicy interval="${TIME_INTERVAL}" maxRandomDelay="${MAX_RANDOMDELAY}"/>
<SizeBasedTriggeringPolicy size="${MAX_FILE_SIZE}"/>
</Policies>
<!-- 日志删除策略 -->
<DefaultRolloverStrategy fileIndex="nomax">
<Delete basePath="${LOG_PATH}" maxDepth="1" followLinks="true">
<IfFileName glob="${MODULE_NAME}-${LEVEL_ERROR}*.log">
<IfAny>
<IfAccumulatedFileSize exceeds="${TOTAL_SIZE_ERROR}"/>
<IfLastModified age="${MAX_HISTORY}"/>
</IfAny>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
<!-- 日志级别 -->
<Filters>
<ThresholdFilter level="${LEVEL_ERROR}" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- logger 用来设置某一个包或者具体的某一个类的日志打印级别、以及指定 -->
<!-- common framework level -->
<AsyncLogger name="org.apache" level="INFO"/>
<AsyncLogger name="org.apache.http" level="INFO"/>
<!-- root -->
<Root level="INFO" includeLocation="false">
<AppenderRef ref="CONSOLE"/>
<AppenderRef ref="INFO_FILE"/>
<AppenderRef ref="WARN_FILE"/>
<AppenderRef ref="ERROR_FILE"/>
</Root>
</Loggers>
</Configuration>
重要配置说明
异步输出日志
要使所有的记录器成为异步的,下面是最简单的配置,并提供最佳性能。
- classpath上添加disruptor.jar包
在Log4j-2.9 之后的版本,需要classpath上有 disruptor-3.3.4.jar包 或更高版本的。 在Log4j-2.9 之前的版本,需要 disruptor-3.0.0.jar 或更高版本。
- 设置环境变量
log4j2.contextSelector
log4j2.contextSelector = org.apache.logging.log4j.core.async.AsyncLoggerContextSelector 或者 log4j2.contextSelector = org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector
官方文档详细描述:Log4j – Log4j 2 Lock-free Asynchronous Loggers for Low-Latency Logging (apache.org)
对于Springboot的项目,只需要增加 log4j2.component.properties 文件,文件里面配置内容如下:
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
不打印代码位置信息
- 异步输出日志开启后,默认情况下,代码行号不会被异步记录器传递给I/O线程,即不打印代码位置信息。
如果你的某个布局或自定义过滤器需要位置信息,你需要在所有相关的记录器(包括根记录器)的配置中,设置 "includeLocation=true"。 但是这里,非常不建议开启,因为开启后会严重降低性能。
- 同步日志输出时,不打印代码位置信息,需要在所有相关的记录器(包括根记录器)的配置中设置 "includeLocation=false"
分割策略和删除策略
分割策略
日志分割策略,大部分情况下会按照指定文件大小、指定时间间隔进行日志分割。
比如:日志按每200MB和每天的间隔去切割,示例配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" >
<Appenders>
<RollingRandomAccessFile name="RollingRandomAccessFile"
immediateFlush="false"
append="true"
fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%traceId] [%logger{60}:%L] - %msg%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" maxRandomDelay="300"/>
<SizeBasedTriggeringPolicy size="200 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="RollingRandomAccessFile"/>
</Root>
</Loggers>
</Configuration>
RollingRandomAccessFileAppender
| 参数名 | 类型 | 描述 |
|---|---|---|
| append | boolean | 当为true(默认值)时,记录将被追加到文件的末尾。当设置为false时,文件将在写入新记录前被清除。 |
| immediateFlush | boolean | 当设置为true(默认值)时,每次写入后都会有一次刷新。这将保证数据被写入磁盘,但可能影响性能。每次写入后的刷新,只有在将此应用程序与同步记录器一起使用时才有用。异步记录器和应用程序将在一批事件结束时自动刷新,即使 immediateFlush 被设置为 false 也会自动刷新。这样既保证了数据被写入到磁盘,也使效率更高。 |
| fileName | String | 要写入的文件的名称。如果该文件或其任何父目录不存在,它们将被创建。 |
| filePattern | String | 归档日志文件的文件名模式。该模式的格式取决于所使用的RolloverStrategy。DefaultRolloverStrategy将接受与SimpleDateFormat兼容的日期/时间模式和/或代表一个整数计数器的%i。 |
SizeBased Triggering Policy
SizeBasedTriggeringPolicy会在文件达到指定大小时引起分割。大小可以以字节为单位指定,后缀为KB、MB、GB或TB,例如20MB。大小也可以包含一个小数,如1.5MB。大小是用Java根语言评估的,所以小数单位必须使用句号。当与基于时间的触发策略相结合时,文件模式必须包含%i,否则目标文件将在每次滚动时被覆盖,因为基于大小的触发策略不会导致文件名中的时间戳值改变。 当不使用基于时间的触发策略时,基于大小的触发策略将导致时间戳值的改变。
TimeBased Triggering Policy
一旦日期/时间模式不再适用于当前正在写入的日志文件,TimeBasedTriggeringPolicy就会对日志文件进行分割。
| 参数名 | 类型 | 描述 |
|---|---|---|
| interval | integer | 根据日期模式中最具体的时间单位,日志分割应该多长时间发生一次。例如,在日期模式中,小时是最具体的时间单位,interval为4时,那么每4小时就会发生一次分割。此参数默认值是1。 |
| modulate | boolean | 表示是否应调整时间间隔,使下一次日志分割发生在时间间隔边界。例如,如果最具体的时间单位是小时,假设当前时间为凌晨3点,interval是4,那么第一次日志分割将发生在凌晨4点,然后接下来的分割将发生在上午8点、中午12点、下午4点,等等。此参数默认值是false。 |
| maxRandomDelay | integer | 表示随机延迟分割的最大秒数。默认情况下,这是0,表示没有延迟。这个设置在服务器上很有用,在服务器上,如果多个应用程序被配置为同时分割日志文件,那么随机延迟分割可以使日志分割的负载分散到不同时间。 |
这里着重说明一下interval的时间单位,它是根据filePattern里日期模式配置中最具体的时间单位而定的。
举例说明:
当 filePattern="logs/app-%d{yyyy-MM-dd}-%i.log" 时,因为日期模式是yyyy-MM-dd,具体到天这个级别,所以interval的时间单位就是天。
当 filePattern="logs/app-%d{yyyy-MM-dd HH}-%i.log" 时,因为日期模式是yyyy-MM-dd HH,具体到小时这个级别,所以interval的时间单位就是小时。
当 filePattern="logs/app-%d{yyyy-MM-dd HH:mm}-%i.log" 时,因为日期模式是yyyy-MM-dd HH:mm,具体到分钟这个级别,所以interval的时间单位就是分钟。
当 filePattern="logs/app-%d{yyyy-MM-dd HH:mm:ss}-%i.log" 时,因为日期模式是yyyy-MM-dd HH:mm:ss,具体到秒这个级别,所以interval的时间单位就是秒。
Default Rollover Strategy
默认的分割策略同时接受日期/时间模式和来自RollingFileAppender本身指定的filePattern属性的整数。
如果日期/时间模式存在,它将被替换成当前的日期和时间值。如果该模式包含一个整数,它将在每次滚动时被递增。如果模式中同时包含日期/时间和整数,整数将被递增,直到日期/时间模式的结果改变。
如果文件模式以".gz"、".zip"、".bz2"、".deflate"、".pack200 "或".xz "结尾,生成的日志将使用与后缀相匹配的压缩方案进行压缩。
其中bzip2、Deflate、Pack200 和 XZ 格式需要 Apache Commons Compress,XZ需要XZ for Java。
| 参数名 | 类型 | 描述 |
|---|---|---|
| fileIndex | String | 如果设置为 "max"(默认),索引较高的文件将比索引较小的文件更新。如果设置为 "min",文件重命名和计数器将遵循上述的固定窗口策略。从2.8版开始,如果fileIndex属性被设置为 "nomax",那么最小和最大值将被忽略,文件编号将以1递增,每次分割将有一个递增的值,没有最大文件数限制。 |
| min | integer | 计数器的最小值。此参数默认值为1。 |
| max | integer | 计数器的最大值。一旦达到这个值,旧的日志将在随后的日志分割中被删除。此参数默认值是7。 |
删除策略
删除策略,大部分情况下会按照日志占用磁盘的空间大小、日志最长留存时间进行自动删除。
常见的配置有保留最近15天的日志且日志磁盘占用不超过200GB,超过200GB 或者 15天之前的日志,只要满足任何一个条件,就会触发日志删除。
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="warn" > <Appenders> <RollingRandomAccessFile name="RollingRandomAccessFile" immediateFlush="false" append="true" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%traceId] [%logger{60}:%L] - %msg%n</Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy interval="1" maxRandomDelay="300"/> <SizeBasedTriggeringPolicy size="200 MB"/> </Policies> <DefaultRolloverStrategy fileIndex="nomax"> <Delete basePath="logs" maxDepth="1" followLinks="true"> <IfFileName glob="app-%d{yyyy-MM-dd}*.log"> <IfAny> <IfAccumulatedFileSize exceeds="200 GB"/> <IfLastModified age="P15D"/> </IfAny> </IfFileName> </Delete> </DefaultRolloverStrategy> </RollingRandomAccessFile> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="RollingRandomAccessFile"/> </Root> </Loggers> </Configuration>
Log4j-2.5引入了一个Delete动作,与DefaultRolloverStrategy max属性相比,该动作使用户能够更多的控制在滚动时间删除哪些文件。删除动作让用户可以配置一个或多个条件,选择相对于基本目录要删除的文件。
请注意,有可能删除任何文件,而不仅仅是滚动的日志文件,所以使用这个动作时要小心! 通过testMode参数,你可以测试你的配置,而不会意外地删除错误的文件。
| 参数名 | 类型 | 描述 |
|---|---|---|
| basePath | String | 必填。开始扫描要删除的文件的基本路径。 |
| maxDepth | integer | 要访问的目录的最大层数。值为0意味着只访问起始文件(基本路径本身),除非被安全管理器拒绝。值为Integer.MAX_VALUE表示应该访问所有级别。默认值是1,意味着只访问指定基本目录中的文件。 |
| followLinks | integer | 是否跟踪文件软链接。默认为false。 |
| testMode | integer | 如果为true,文件不会被删除,而是在INFO级别向状态记录器打印一条信息。用它来做一次试运行,测试配置是否像预期的那样工作。默认为false。 |
| pathConditions | integer | 如果没有指定ScriptCondition时,此配置是必需的。此参数接受一个或多个PathCondition元素。如果指定了一个以上的条件,在删除之前,它们都需要接受一个路径。条件可以是嵌套的,在这种情况下,只有当外部条件接受了路径时,内部条件才会被评估。如果条件不是嵌套的,它们可以按任何顺序进行评估。条件也可以通过使用IfAll、IfAny和IfNot复合条件与逻辑运算符AND、OR和NOT相结合。 |
这里重点说明一下pathConditions(以下示例不要在生产中使用):
<DefaultRolloverStrategy fileIndex="nomax"> <Delete basePath="logs" maxDepth="1" followLinks="true"> <IfFileName glob="app-%d{yyyy-MM-dd}*.log"> <IfAccumulatedFileSize exceeds="200 GB"/> <IfLastModified age="P15D"/> </IfFileName> </Delete> </DefaultRolloverStrategy>
上面配置中的 IfAccumulatedFileSize 和 IfLastModified ,没有 IfAll、IfAny、IfNot 条件修饰,没有配置默认就是 IfAll,表示 and 的逻辑。
用户可以创建自定义条件或使用内置条件。
- IfFileName - 接受其路径(相对于基本路径)与正则表达式或glob匹配的文件。
- IfLastModified - 接受与指定时间一样长或更长的文件,配置格式需要按照Duration配置,否则不生效。
- IfAccumulatedFileCount - 接受在文件树行走过程中超过某个计数阈值的路径。
- IfAccumulatedFileSize - 接受在文件树行走过程中超过累积文件大小阈值后的路径。
- IfAll - 如果所有的嵌套条件都满足,则接受该路径(逻辑和)。嵌套条件可以按任何顺序进行评估。
- IfAny - 如果其中一个嵌套条件满足,则接受该路径(逻辑OR)。嵌套条件可以以任何顺序进行评估。
- IfNot - 如果嵌套条件不接受它,则接受一个路径(逻辑NOT)。
日志区分级别输出
日志按不同级别分别输出到不同的日志文件,方便定位问题和细粒度日志文件删除。
ThresholdFilter
如果LogEvent中的级别与配置的级别相同或更具体,该过滤器返回onMatch结果,否则返回onMismatch值。例如,如果ThresholdFilter被配置为ERROR级别,而LogEvent包含DEBUG级别,那么将返回onMismatch值,因为ERROR事件比DEBUG更具体。
| 参数名 | 类型 | 描述 |
|---|---|---|
| level | String | 一个有效的级别名称来匹配。 |
| onMatch | String | 当过滤器匹配时要采取的行动。可以是ACCEPT、DENY或NEUTRAL。默认值是NEUTRAL。 |
| onMismatch | String | 当过滤器不匹配时要采取的行动。可以是ACCEPT、DENY或NEUTRAL。默认值是DENY。 |
配置示例,只打印info日志到文件中:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Don't forget to set system property
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
to make all loggers asynchronous. -->
<Configuration status="WARN">
<Appenders>
<!-- INFO_FILE -->
<RollingRandomAccessFile name="INFO_FILE" fileName="logs/app.log"
immediateFlush="false"
append="true"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%traceId] [%logger{60}:%L] - %msg%n" charset="UTF-8"/>
<!-- 日志分割策略 -->
<Policies>
<TimeBasedTriggeringPolicy interval="1" maxRandomDelay="300"/>
<SizeBasedTriggeringPolicy size="200 MB"/>
</Policies>
<!-- 日志删除策略 -->
<DefaultRolloverStrategy max=“20” />
<!-- 日志级别 -->
<Filters>
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- root -->
<Root level="INFO" includeLocation="false">
<AppenderRef ref="INFO_FILE"/>
</Root>
</Loggers>
</Configuration>
java - log4j2这么配就对了 - 个人文章 - SegmentFault 思否


logback VS log4j2:一倍左右的性能差异,是时候注意了!-CSDN博客


Logback和Log4j2日志框架性能对比与调优方式的示例分析 - 开发技术 - 亿速云


浙公网安备 33010602011771号