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,所以需要手动引入。

 
  1. <dependency>
  2. <groupId>com.lmax</groupId>
  3. <artifactId>disruptor</artifactId>
  4. <version>3.4.2</version> <!--版本号你可以自定义 -->
  5. </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启动类中添加静态块 ,设置环境变量  。

 
  1. @SpringBootApplication
  2. public class SpringbootApplication {
  3. static {
  4. /**
  5. * 2种设置 都是无效的,因为 log4j2 的加载早于 springboot启动类的 static 代码块,更早于 main方法
  6. */
  7. System.setProperty("Log4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
  8. System.setProperty("-DLog4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
  9. }
  10. }
 

4.2【该设置无效】修改springboot的环境变量。以下两种方式都是无效的,因为springboot的环境变量 加载晚于log4j2 。

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-environment-or-application-context

 
  1. /**
  2. * 自定义一个 yml文件 config.yml ,将 属性值加进去 。然后强制让 spring加载该 yml配置
  3. * Log4jContextSelector: org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  4. */
  5. public class EnvironmentPostProcessorExample implements EnvironmentPostProcessor {
  6. private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  7. @Override
  8. public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
  9. Resource path = new ClassPathResource("com/example/myapp/config.yml");
  10. PropertySource<?> propertySource = loadYaml(path);
  11. environment.getPropertySources().addLast(propertySource);
  12. }
  13. private PropertySource<?> loadYaml(Resource path) {
  14. if (!path.exists()) {
  15. throw new IllegalArgumentException("Resource " + path + " does not exist");
  16. }
  17. try {
  18. return this.loader.load("custom-resource", path).get(0);
  19. }
  20. catch (IOException ex) {
  21. throw new IllegalStateException("Failed to load yaml configuration from " + path, ex);
  22. }
  23. }
  24. }
 

上面的无效,下面的 添加启动过程的监听器,依然无效,原理一样,都是修改 springboot的环境变量,为时已晚。 log4j2 这之前就 都加载配置,启动完成了。

 
  1. /**
  2. * 自定义 监听器,在使用环境变量 之前,修改或者新增环境变量
  3. * 记得 把这个 监听器,加入springboot 中 。否则这个监听器的代码不会执行 SpringApplication.addListeners(…​) method or the SpringApplicationBuilder.listeners(…​)
  4. * 参考 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-application-events-and-listeners
  5. *
  6. * 另外, implements EnvironmentAware 也能获取到 环境变量
  7. */
  8. @Slf4j
  9. public class Application2EnvPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
  10. @SneakyThrows
  11. @Override
  12. public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
  13. ConfigurableEnvironment env = event.getEnvironment();
  14. // 在这里设置 log4j2 是无法生效的,因为log4j2 的启动和加载更早
  15. // 生效有2法:1 命令行启动参数加上 -D上面的命令 ; 或者 2 添加 外置文件 log4j2.component.properties ,设置好参数。
  16. setPropertyIfAbsent(env,"Log4jContextSelector","org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
  17. }
  18.  
  19. private void setPropertyIfAbsent(ConfigurableEnvironment environment, String propertyName,Object value) {
  20. String property = environment.getProperty(propertyName);
  21. if (StringUtils.isBlank(property)) {
  22. String envType = "systemProperties"; // 默认设置为 JVM 环境变量
  23. setProperty(environment, envType, propertyName, value);
  24. log.info("set property ok, {}={}", propertyName, environment.getProperty(propertyName));
  25. return;
  26. }
  27. log.warn("failed to set property [{}={}] ,because {}={}", propertyName, value, propertyName, property);
  28. }
  29. }
 

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 

 
  1. static {
  2. Assert.isTrue(AsyncLoggerContextSelector.isSelected(), "log4j2 的异步 disruptor启动失败");
  3. }
 

 

参考来源: 感谢 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写日志导致线程阻塞的问题(多亏了开发小哥日志打的多,不然就没有下面这一系列骚操作)。
2.jpg

大致描述下当时的情况(内网限制,没法把现场东西拿出来,只能口述了):

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状态,线程状态大致如下;

image.png

当时针对log4j2给的优化建议是: 1、配置immediateFlush=false 2、将filePattern对应的gz后缀去掉(因为对应的compressionLevel=0,根本不压缩),是否就不会调用JDK的Deflater进行压缩。【猜测,也是后面还原现场的原因之一,想亲自验证一下】

二、本地复现&部分源码学习

  问题复现的过程也是蛮艰辛的,遇到各种问题。下面记录的是我本地复现时遇到的问题以及解决办法,附带一些log4j2基于disruptor的部分源码学习,篇幅可能会稍长。
10.jpg

环境: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,此时应上问号脸。线程情况是这样的:

image.png

感觉和预期差的有点大啊,看样子是在往disruptor的RingBuffer里写日志的时候就blocked了,可以对比一下之前线程的线程情况,是没有blocked的。内存dump中好像发现了不一样的:

image.png

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.AsyncLoggerConfigDisruptorenqueue方法

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:

    1. 异步日志配置方式不同的话,会使用不同的处理类(AsyncLoggerConfigDisruptor「mix模式/配置文件进行配置」 | AsyncLoggerDisruptor「全局异步配置」),因此配置参数得相匹配;
    2. SynchronizeEnqueueWhenQueueFull设置成false,会导致CPU使用率飙升,这个应该也是默认为true的原因。可以看下面本地实验的结果,差不多是10倍的差距。【官方文档中是不建议设置成false的,除非绑定CPU核。】

      • true时的CPU使用「159%」:
        false-cpu.png
      • false时的CPU使用「1565%」:
        true-cpu.png
CPU高的主要原因:
enqueue方法处没有阻塞的情况下,所有的线程都会进入到MultiProducerSequencernext方法。这个方法是获取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; }

至此,基本还原现场的情况:
image.png

(3)异步日志重复配置的问题

  这是个人的好奇之举。当即使用了Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector,又在log4j2.xml中配置了<asyncRoot>标签,日志对象将会多一次中间传递:
app -> RingBuffer-1 -> thread a -> RingBuffer-2 -> thread b -> disk
这会带来不必要的性能损耗。

看下这种情况的线程dump就了然了:

image.png

(二)日志压缩

尝试去掉gz后缀名,优化压缩的资源消耗问题。

  • 修改前,CPU采样情况,在资源占用前列可以明显看到压缩的相关方法:
    image.png
  • 去掉gz后缀和压缩级别参数,已经找不到压缩相关的方法了:
    image.png

验证了之前的猜想,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说几点:

  • 默认的TimeoutBlockingWaitStrategyBlockingWaitStrategy策略都是基于ReentrantLock实现的,由于底层是基于AQS,在运行过程中会创建AQS的Node对象,不符合Garbage-free的思想;
  • SLEEP是Garbage-free的,且官方文档提到,相较于BLOCK适用于资源受限的环境,SLEEP平衡了性能和CPU资源两方面因素。

三、总结

  关于log4j2的性能优化,总结以下几点「后面有补充会添加到这里」

  1. 配置滚动日志的时候,若不需要压缩日志,filePattern的文件名不要以gz结尾;
  2. 使用Disruptor异步日志的时候,不要同时使用Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector<asyncRoot>
  3. RollingRandomAccessFile配置immediateFlush="false"属性,这样让I/O线程批量刷盘(这里其实涉及到native方法调用的性能问题);
  4. 可以结合资源情况是否要配置SynchronizeEnqueueWhenQueueFullfalse
  5. 结合实际情况是否要更改I/O线程的WaitStrategy
  6. 若日志可以丢弃,可以配置丢弃策略,配置log4j2.asyncQueueFullPolicy=Discardlog4j2.discardThreshold=INFO「默认」,当队列满时,INFODEBUGTRACE级别的日志会被丢弃;

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>

 

重要配置说明

异步输出日志

要使所有的记录器成为异步的,下面是最简单的配置,并提供最佳性能。

  1. classpath上添加disruptor.jar包
在Log4j-2.9 之后的版本,需要classpath上有 disruptor-3.3.4.jar包 或更高版本的。 在Log4j-2.9 之前的版本,需要 disruptor-3.0.0.jar 或更高版本。
  1. 设置环境变量 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

image.png

不打印代码位置信息

  1. 异步输出日志开启后,默认情况下,代码行号不会被异步记录器传递给I/O线程,即不打印代码位置信息。
如果你的某个布局或自定义过滤器需要位置信息,你需要在所有相关的记录器(包括根记录器)的配置中,设置 "includeLocation=true"。 但是这里,非常不建议开启,因为开启后会严重降低性能。
  1. 同步日志输出时,不打印代码位置信息,需要在所有相关的记录器(包括根记录器)的配置中设置 "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。

Triggering Policies

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的时间单位就是秒。

Rollover Strategies

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 ,没有 IfAllIfAnyIfNot 条件修饰,没有配置默认就是 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日志框架性能对比与调优方式的示例分析 - 开发技术 - 亿速云

SLF4J 手册

 

posted @ 2025-01-17 09:19  CharyGao  阅读(482)  评论(0)    收藏  举报