天天看点

日志框架总结(一)一、日志概述二、Java日志框架三、实际应用及区分

日志框架总结(一)

  • 一、日志概述
  • 二、Java日志框架
    • 2.1、日志迭代过程
    • 2.2、Logback vs Log4j2
  • 三、实际应用及区分
    • 3.1、 Logback 实践:
      • 3.1.1、快速入门
      • 3.1.2、logback.xml 配置文件分析
    • 3.2、 Log4j2 实践:
      • 3.2.1 使用
      • 3.2.2 log4j2.xml 配置及说明
      • 3.2.3 log4j2 最佳实践
        • 3.2.3.1 日志分类
        • 3.2.3.2 日志模式-同步/异步
        • 3.2.3.3 日志滚动和清除策略
        • 3.2.3.4 其他注意事项和调优
      • 3.2.4 log4j-disruptor等待策略
      • 3.2.5 其他建议

一、日志概述

一个在生产环境里运行的程序如果没有日志是很让维护者提心吊胆的,有太多杂乱又无意义的日志也是令人伤神。程序出现问题时候,从日志里如果发现不了问题可能的原因是很令人受挫的。本文想讨论的是如何在Java程序里写好日志。

一般来说日志分为两种:业务日志和异常日志,使用日志我们希望能达到以下目标:

1、对程序运行情况的记录和监控;

2、在必要时可详细了解程序内部的运行状态;

3、对系统性能的影响尽量小;

二、Java日志框架

java里常见的日志库有java.util.logging(JDKlog)、Apache log4j、log4j2、logback、slf4j等等,这么多的日志框架里如何选择。

2.1、日志迭代过程

首先需要梳理的是日志接口,以及日志接口对应的实现。然后再考虑如何选择使用哪个日志框架。

下图说明了几个日志框架的关系:

日志框架总结(一)一、日志概述二、Java日志框架三、实际应用及区分

上述关系图,大致就是两个 API 接口 + 4个实现框架。我们应用服务调用日志框架,只需遵循 API 接口规范,配置调用即可。

现在了解下上面接口及框架迭代历史进程,如下图:

日志框架总结(一)一、日志概述二、Java日志框架三、实际应用及区分
  • Log4j

    Log4j : 是apache下一个功能非常丰富的java日志库实现,Log4j应该是出现比较早而且最受欢迎的java日志组件,它是基于java的开源的日志组件。Log4j的功能非常强大,通过Log4j可以把日志输出到控制台、文件、用户界面。也可以输出到操作系统的事件记录器和一些系统常驻进程。值得一提的是:Log4j可

    以允许你非常便捷地自定义日志格式和日志等级,可以帮助开发人员全方位的掌控自己的日志信息是一个日志开源框架。

    这个是最先出现流行的日志框架。

  • Java.util.logging

    Log4j 流行之时,java自身也不忘在这方面发力,于是java 1.4 之后,java原生库也写了一套日志框架 即是 Java.util.logging。

    此时,也有很多厂家的日志框架在共同竞争,各自为营,相互兼容性很差。导致应用软件API 在相互调用受阻。于是,apache 就站出来,设计了一套公共接口 – commons-logging ,来兼容各方面的日志框架,便于实际开发进程;

  • Commons-logging(jcl)

    1、 在log4j , java.util.logging 等问世之后,相对于日志输出就简化很多,但是由于没有统一规范,开发者调用这些框架也是很麻烦,相互兼容性也很差(不同厂家的方法各不一样),同时对日志框架迭代更新维护带来不确定性,于是 apache 为规范日志输出,定义一套标准日志API接口 ---- commons-logging API 接口。开发者,只需关注接口,而不需看重实现细节,极大提供效率。

    2、尽管 commons-logging(jcl) 也为众多日志实现库提供了统一的接口,作用和slf4j类似。它允许运行时绑定任意的日志库。但commons-loggins对log4j和java.util.logging的配置问题兼容性不太好,还会遇到类加载问题。所以当时log4j的作者CEKI又创作了 SLF4j API 接口。

  • SLF4j:

    1、它是基于API的java日志框架,slf4j提供了简单统一的日志记录的接口,开发者在配置部署时只需要是吸纳这个接口就能是实现日志功能。它自身并没有提供具体的日志解决方案,它是负责服务于各种各样的日志系统,允许用户在部署应用上使用自己常用的日志框架。也就是说,SLF4j是一个抽象层,它提供了众多的适配器能是配合其他所有开源日志框架。

    2、为了考虑其他项目会使用大量的第三方库,而第三方库使用的日志框架又各不相同,不同的日志框架又需要不同的配置,不同配置就会导致日志输出到不同的位置。所以我们就需要一个可以将日志level、日志输出等统一管理,而slf4j的适配器又对各种日志都实现了接管,接管后就可以统一配置这些第三方库中使用的日志。

  • logback

    对比 Log4j的优势:

      1、更快的实现:Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。

      2、非常充分的测试:Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。

      3、Logback-classic非常自然实现了SLF4j:Logback-classic实现了SLF4j。在使用SLF4j中,你都感觉不到logback-classic。而且因为logback-classic非常自然地实现了slf4j , 所 以切换到log4j或者其他,非常容易,只需要提供成另一个jar包就OK,根本不需要去动那些通过SLF4JAPI实现的代码。

      4、非常充分的文档 官方网站有两百多页的文档。

      5、自动重新加载配置文件,当配置文件修改了,Logback-classic能自动重新加载配置文件。扫描过程快且安全,它并不需要另外创建一个扫描线程。这个技术充分保证了应用程序能跑得很欢在JEE环境里面。

      6、Lilith是log事件的观察者,和log4j的chainsaw类似。而Lilith还能处理大数量的log数据 。

      7、谨慎的模式和非常友好的恢复,在谨慎模式下,多个FileAppender实例跑在多个JVM下,能够安全地写道同一个日志文件。RollingFileAppender会有些限制。Logback的FileAppender和它的子类包括 RollingFileAppender能够非常友好地从I/O异常中恢复。

      8、配置文件可以处理不同的情况,开发人员经常需要判断不同的Logback配置文件在不同的环境下(开发,测试,生产)。而这些配置文件仅仅只有一些很小的不同,可以通过,和来实现,这样一个配置文件就可以适应多个环境。

      9、Filters(过滤器)有些时候,需要诊断一个问题,需要打出日志。在log4j,只有降低日志级别,不过这样会打出大量的日志,会影响应用性能。在Logback,你可以继续 保持那个日志级别而除掉某种特殊情况,如alice这个用户登录,她的日志将打在DEBUG级别而其他用户可以继续打在WARN级别。要实现这个功能只需加4行XML配置。可以参考MDCFIlter 。

      10、SiftingAppender(一个非常多功能的Appender):它可以用来分割日志文件根据任何一个给定的运行参数。如,SiftingAppender能够区别日志事件跟进用户的Session,然后每个用户会有一个日志文件。

      11、自动压缩已经打出来的log:RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响。

      12、堆栈树带有包版本:Logback在打出堆栈树日志时,会带上包的数据。

      13、自动去除旧的日志文件:通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,你可以控制已经产生日志文件的最大数量。如果设置maxHistory 12,那那些log文件超过12个月的都会被自动移除。

  • Log4j2

    Log4j2 的特性:

      1、API分离:Log4j的API与实现是分开的,从而使应用程序开发人员可以清楚地了解他们可以使用哪些类和方法,同时确保向前的兼容性。这允许Log4j团队以兼容的方式安全地改进实施。

      2、性能提升:Log4j 2包含基于LMAX Disruptor库的下一代异步记录器。在多线程方案中,与Log4j 1.x和Logback相比,异步Logger的吞吐量高18倍,延迟降低了几个数量级。

      3、支持多种API:Log4j 2 API将提供最佳性能,而Log4j 2提供对Log4j 1.2,SLF4J,Commons Logging和java.util.logging(JUL)API的支持。

      4、避免锁定:编码为Log4j 2 API的应用程序始终可以选择使用任何SLF4J兼容库作为其Log4j-to-slf4j适配器的记录器实现。利用jdk1.5并发的特性,减少了死锁的发生;

      5、自动重载配置:与Logback一样的是,Log4j 2可以在修改后自动重新加载其配置。与Logback不同的是,它在进行重新配置时不会丢失日志事件。丢数据这种情况少,可以用来做审计功能。而且自身内部报的exception会被发现,但是logback和log4j不会。

      6、进阶筛选:与Logback一样,Log4j 2支持基于上下文数据,标记,正则表达式和Log事件中的其他组件进行过滤。可以指定过滤以将所有事件应用到所有事件,然后再传递给Logger或事件通过Appender。此外,过滤器还可以与Loggers关联。与Logback不同,您可以在任何这些情况下使用通用的Filter类。

      7、插件架构:Log4j使用插件模式来配置组件。这样,您无需编写代码即可创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件,并在配置引用它们时使用它们。

      8、物业支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的基础组件。属性来自配置文件中定义的值,系统属性,环境变量,ThreadContext映射以及事件中存在的数据。用户可以通过添加自己的查找插件来进一步自定义属性提供程序。

      9、Java 8 Lambda支持:以前,如果构建日志消息的成本很高,则通常会在构建消息之前显式检查是否启用了请求的日志级别。在Java 8上运行的客户端代码可以受益于Log4j的lambda支持。如果未启用请求的日志级别,由于Log4j不会评估lambda表达式,因此可以用更少的代码获得相同的效果。

      10、自定义日志级别:在Log4j 2中,可以通过代码或配置轻松定义自定义日志级别。不需要子类。

      11、无垃圾:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,而在Web应用程序中是低垃圾的。这样可以减少垃圾收集器上的压力,并可以提供更好的响应时间性能。【拥有号称能够减少 JVM 垃圾回收停顿时间的 Garbage-free(无垃圾模式)】

      12、与应用服务器集成:版本2.10.0引入了模块log4j-appserver,以改善与Apache Tomcat和Eclipse Jetty的集成。

      13、启用云:2.12.0版引入了对通过Lookup访问Docker容器信息以及通过Spring Cloud Configuration访问和更新Log4j配置的支持。

2.2、Logback vs Log4j2

日志框架大战随着 SLF4j 的一统天下而落下帷幕,但 SLF4j 仅仅是接口,实现方面, logback 与 log4j2 仍然难分高下,接下来聊聊,日志框架实现到底是该选择 Log4j2 还是 Logback。这篇文章我们将从功能、API 设计、可扩展性、性能四个方面展开讨论。

  • 生态
  1. 老牌的 Log4j2 凭借着入场早、背靠 Apache 两大优势有着不错的用户支持,官网文档也很完善。
  2. 新生的 Logback 凭借着 SLF4j 的原生实现以及被 Spring Boot 钦点的日志框架(Spring 也提供了Log4j2 的 starter,切换依赖即可完成更换日志框架,前文讲过,此处不再赘述),同样也拥有着很好的前景。
  3. 社区支持方面,Log4j2 作为 Apache 顶级项目,支持度应该是很不错的,Logback 作为Ceki创业后的产物,也能有很好的保证,二者生态可谓不相上下;
  • 功能:我们从使用者的角度可以分为:配置、使用、以及独有特性。
  1. 配置文件方面,Log4j 提供了更多的配置文件配置方式,Log4j2 支持 properties、YAML、JSON、XML四种,Logback 则支持 XML 与 groovy 两种方式;
  2. Appender 方面,两者均支持自定义扩展 Appender ,Log4j2 并且提供了几乎全部场景的 Appender,文件、控制台、JDBC、NoSql、MQ、Syslog、Socket、SMTP等,Logback提供 Appender 略少于 Log4j2,提供了文件、控制台、数据库、Syslog、Socket、SMTP等,动态化输出方面,Log4j2 提供了ScriptAppenderSelector,Logback 则提供了 Evaluator 与 SiftingAppender(两者均可以用于判断并选择对应的 Appender);
  3. 独有特性方面,Logback 支持 Receivers, 可以接收其他 Logback 的 Socket Appender 输出,Logbak 还拥有 logback-access 模块,可以用来与 Servlet容器(如 Tomcat 和 Jetty)集成,提供 http 访问日志功能;Log4j2 则拥有号称能够减少 JVM 垃圾回收停顿时间的 Garbage-free(无垃圾模式),Log4j2 API 支持使用 Java 8 lambda,SLF4j 则在 2.0 版本提供流式(Fluent)API 同时支持 lambda;
  • API 设计及可扩展性

    如前文所说,SLF4j 则在 2.0 版本提供流式(Fluent)API ,届时Logback将会原生实现(理论上会比动态转译过去要好),而 Log4j2 并没有提供支持。扩展方面,Logback 采用配置文件中直接写对应实现(class=“ch.qos.logback.core.rolling.RollingFileAppender”)来自定义实现扩展,Log4j2 采用插件机制,无需配置,但比较复杂,个人认为 Logback 反而清晰一些。

  • 性能

    对于高并发,大量日志输出,Log4j2 确实有一定优势。因为其内部的队列使用的是disruptor。异步输出性能提升不少,是实话!

    但是,对于日志框架应用,我们要求可能并不需要那么高,即对于一般的日志输出,Logback 与 Log4j2 性能方面差不多;

    具体参考: https://www.jianshu.com/p/359b14067b9e,http://www.cainiaoxueyuan.com/bc/17731.html

  • 总结

    Logback 使用更简单、Log4j2 功能更强大,如果不是深度使用,两者并不会有太大差别,并且在使用SLF4j的时候可以无缝切换。建议,不必纠结选型,按照偏好选择即可。

三、实际应用及区分

3.1、 Logback 实践:

3.1.1、快速入门

第1步: 对于一般 Maven 项目,引入依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
           

第2步:导入 logback.xml 即是logback日志输出配置文件,如下

<?xml version="1.0" encoding="UTF-8"?>  
<configuration debug="true">
	<!-- <property name="LOG_HOME" value="/home/core/project"/> -->
	<property name="LOG_HOME" value="D:/log/" />
	<property name="APP_NAME" value="lolo"/>
	<!-- 控制台输出-->
	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>
		</layout>
	</appender>
	<!-- 按照每天生成日志文件 -->  
	<appender name="DATELOG" class="ch.qos.logback.core.rolling.RollingFileAppender">  
		<File>${LOG_HOME}${APP_NAME}.log</File>
		<filter class="ch.qos.logback.classic.filter.LevelFilter"> 
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch> 
		</filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
          <FileNamePattern>${LOG_HOME}/${APP_NAME}.%i.bak</FileNamePattern>
            <MinIndex>1</MinIndex>
            <MaxIndex>30</MaxIndex>
        </rollingPolicy>
        <triggeringPolicy
            class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>
        </layout>
	</appender>
	<!-- 按照每天生成日志文件   错误信息 -->  
	<appender name="DATELOG_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">  
		<File>${LOG_HOME}${APP_NAME}.error.log</File>
		<filter class="ch.qos.logback.classic.filter.LevelFilter"> 
			<level>ERROR</level> 
			<onMatch>ACCEPT</onMatch> 
			<onMismatch>DENY</onMismatch> 
		</filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"><FileNamePattern>${LOG_HOME}/${APP_NAME}.error.%i.bak</FileNamePattern>
            <MinIndex>1</MinIndex>
            <MaxIndex>30</MaxIndex>
        </rollingPolicy>
        <triggeringPolicy
            class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>
        </layout> 
	</appender> 
	<logger name="com.jieshun.jscp.p" level="DEBUG" additivity="false" >
	    <appender-ref ref="CONSOLE" />
	    <appender-ref ref="DATELOG" />
		<appender-ref ref="DATELOG_ERROR" />
	</logger>
	<root level="INFO" >
	    <appender-ref ref="CONSOLE" />
	    <appender-ref ref="DATELOG" />
		<appender-ref ref="DATELOG_ERROR" />
	</root>
</configuration>
           

第3步:再代码调用

1) 可以使用 lomback 的@SLf4j 注解,即可直接使用默认的 log 对象;

@Slf4j
public class TestDemo {
	@Test
    public void testDateStr2TimeStamp() throws Exception{
        String dateStr = "2020-07-13 11:16:00";
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = formatter.parse(dateStr);
        log.info(" 2020-07-13 11:16:00 to times = {}",date.getTime());
//        System.out.println(date.getTime());
    }
}
           
注意调用 @Slf4j 注解时,需要引入 lomback 依赖!
<dependency>
    <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.18.12</version>
     <scope>provided</scope>
</dependency>
           

2)不使用 @Slf4j 注解,那么在类的首行就要自定义一个日志输出对象

public class TestDemo {
	// 使用 static 修饰,独立类对象之外,为所有类的对象共用
    private static Logger logger = LoggerFactory.getLogger(TestDemo.class); // TestDemo => Person

	@Test
    public void testDateStr2TimeStamp() throws Exception{
        String dateStr = "2020-07-13 11:16:00";
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = formatter.parse(dateStr);
        logger.info(" 2020-07-13 11:16:00 to times = {}",date.getTime());
//        System.out.println(date.getTime());
    }
}

========== 输出的结果 ==========
14:03:09,386 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
14:03:09,387 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@36d4b5c - Registering current configuration as safe fallback point
2020-07-24 14:03:09.394 [main] INFO  com.lolo.entity.Person[51] -  2020-07-13 11:16:00 to times = 1594610160000
           
【注意】
  1. Logger logger 对象对应的类为:

    org.slf4j.Logger

    ; 在我们输入 Logger 可能有一堆相同类名对象,请注意使用

    org.slf4j.Logger

    。因为前面已讲过,Slf4j 一统天下。使用此接口,方便后续任意日志框架移植和切换;
  2. LoggerFactory.getLogger(TestDemo.class); 中的 class 类,应为日志所在类的名称,不要随意书写。若像上面代码写成 Person.class ,那么日志输出就成了

    ... com.lolo.entity.Person[51] ...

    ,导致我们来看日志定位不明朗不准确;

3.1.2、logback.xml 配置文件分析

<?xml version="1.0" encoding="UTF-8"?>

<!-- 1. configuration:
    scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
    scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
    debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 若true logback内部都是稳定,一般情况不会有其相关日志
    一般情况采用默认设置即可!
-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 2. contextName:
        用来设置上下文名称,每个logger都关联到logger上下文,默认上下文名称为default。
        但可以使用<contextName>设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
        作用不大,真实有效的日志,没有特别处理,不会带有上下文的名称。在项目启动时,会看到些许日志带上了上下文名称,
        如 ch.qos.logback.classic.LoggerContext[myAppName]
    -->
<!--    <contextName>myAppName</contextName>-->

    <!-- 3. timestamp:
        获取时间戳字符串,他有两个属性key和datePattern
    key: 标识此<timestamp> 的名字;
    datePattern: 设置将当前时间(解析配置文件的时间)转换为字符串的模式,遵循java.txt.SimpleDateFormat的格式。
        一般也无需设置。 后面的 layout 下的 Pattern 可以自动匹配时间戳
    -->
    <timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
    <contextName>${bySecond}</contextName>
    <!-- 4. property:
        用来定义变量值,它有两个属性name和value,通过<property>定义的值会被插入到logger上下文中,可以使“${}”来使用变量。
    -->
    <!-- <property name="LOG_HOME" value="/home/core/project"/> -->
    <property name="LOG_HOME" value="D:/log/" />
    <property name="APP_NAME" value="lolo"/>

    <!-- 5. appender:
        负责写日志的组件,它有两个必要属性name和class。name指定appender名称,class指定appender的全限定名.
        ConsoleAppender: 负责将日志写入控制台;(常用)
        RollingFileAppender: 负责将日志写入指定文件,持久化,并且是循环输出文件; (常用)
        FileAppender:把日志添加到文件,有以下子节点:(少用)
            <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
            <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
            <encoder>:对记录事件进行格式化。
            <prudent>:如果是 true,日志会被安全的写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认是 false。
    -->
    <!-- 控制台输出-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 5.1 encoder,layout
            格式化输出: encoder, layout 都可以
        -->
        <encoder>
            <!-- 规则:输出日志的logger名,可有一个整形参数,功能是缩短logger名,设置为0表示只输入logger最右边点符号之后的字符串。
            %logger	    mainPackage.sub.sample.Bar	mainPackage.sub.sample.Bar
            %logger{0}	mainPackage.sub.sample.Bar	Bar
            %logger{5}	mainPackage.sub.sample.Bar	m.s.s.Bar
            %logger{10}	mainPackage.sub.sample.Bar	m.s.s.Bar
            %logger{15}	mainPackage.sub.sample.Bar	m.s.sample.Bar
            %logger{16}	mainPackage.sub.sample.Bar	m.sub.sample.Bar
            %logger{26}	mainPackage.sub.sample.Bar	mainPackage.sub.sample.Bar
            -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{16}[%line] - %msg%n</pattern>
        </encoder>
<!--        <layout class="ch.qos.logback.classic.PatternLayout">-->
<!--            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>-->
<!--        </layout>-->
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="DATELOG" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!--
            <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
        -->
        <File>${LOG_HOME}/${APP_NAME}.log</File>
        <!--
            <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
        -->
        <append>true</append>
        <!-- LevelFilter: 级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志。有以下子节点:
            <level>:设置过滤级别
            <onMatch>:用于配置符合过滤条件的操作
            <onMismatch>:用于配置不符合过滤条件的操作
        -->
        <!--
            日志过滤,只输出 info 的日志
        -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--
            <rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。

             TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。

                    <fileNamePattern>:必要节点,包含文件名及“%d”转换符, “%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,
                    如:%d{yyyy-MM}。如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender 的file字节点可有可无,通过设置file,
                    可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,
                    活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。“/”或者“\”会被当做目录分隔符。

                    <maxHistory>:可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且<maxHistory>是6,
                    则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。
        -->

        <!-- 每天生成一个日志文件,保存30天的日志文件。
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        -->

        <!-- FixedWindowRollingPolicy: 根据固定窗口算法重命名文件的滚动策略。有以下子节点:
            <minIndex>:窗口索引最小值
            <maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12。
            <fileNamePattern >:必须包含“%i”例如,假设最小值和最大值分别为1和2,命名模式为 mylog%i.log,会产生归档文件mylog1.log和mylog2.log。
            还可以指定文件压缩选项,例如,mylog%i.log.gz 或者 没有log%i.log.zip
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <FileNamePattern>${LOG_HOME}/${APP_NAME}.%i.bak</FileNamePattern>
            <MinIndex>1</MinIndex>
            <MaxIndex>30</MaxIndex>
        </rollingPolicy>
        <!--
            <triggeringPolicy >: 告知 RollingFileAppender 合适激活滚动。
            SizeBasedTriggeringPolicy: 查看当前活动文件的大小,如果超过指定大小会告知 RollingFileAppender 触发当前活动文件滚动。只有一个节点:
                <maxFileSize>:这是活动文件的大小,默认值是10MB。
        -->
        <triggeringPolicy
                class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>

        <!--
            <prudent>:当为true时,不支持FixedWindowRollingPolicy。
            支持TimeBasedRollingPolicy,但是有两个限制,1不支持也不允许文件压缩,2不能设置file属性,必须留空。
            默认:false
        -->
        <prudent>false</prudent>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}[%line] - %msg%n</pattern>
        </encoder>
<!--        <layout class="ch.qos.logback.classic.PatternLayout">-->
<!--            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>-->
<!--        </layout>-->

    </appender>

    <!-- 按照每天生成日志文件   错误信息 -->
    <appender name="DATELOG_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <File>${LOG_HOME}${APP_NAME}.error.log</File>
        <!--
            日志过滤,只输出 error 的日志
        -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <FileNamePattern>${LOG_HOME}/${APP_NAME}.error.%i.bak</FileNamePattern>
            <MinIndex>1</MinIndex>
            <MaxIndex>30</MaxIndex>
        </rollingPolicy>
        <triggeringPolicy
                class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}[%line] - %msg%n</pattern>
        </encoder>
<!--        <layout class="ch.qos.logback.classic.PatternLayout">-->
<!--            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%n</Pattern>-->
<!--        </layout>-->
    </appender>


    <!-- 6. logger:
        用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性。
        name:用来指定受此loger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
        如果未设置此属性,那么当前loger将会继承上级的级别。
        addtivity:是否向上级loger传递打印信息。默认是true,向上级传递。参考:https://blog.csdn.net/qq_36850813/article/details/83092051
    -->
    <logger name="com.jieshun.jscp.p" level="DEBUG" additivity="false" >
        <!-- 6.1 appender-ref:
            可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger
        -->
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="DATELOG" />
        <appender-ref ref="DATELOG_ERROR" />
    </logger>

    <!-- 7. root: 类同 logger, 代表根节点。有且仅有一个的
            属性 level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL。默认是DEBUG。
    -->
    <root level="INFO" >
        <!--
        包含零个或多个<appender-ref>元素,标识这个appender将会添加 root
        -->
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="DATELOG" />
        <appender-ref ref="DATELOG_ERROR" />
    </root>
</configuration>
           

3.2、 Log4j2 实践:

3.2.1 使用

第1步: 引入依赖。

<dependency>
   <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.12.1</version>
</dependency>

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>
           
1、一般需要 log4j-core, log4j-api, slf4j-api jar包。下面的 log4j-slf4j-impl 涵盖所有,因为依赖传递,包含了log4j-core, log4j-api, slf4j-api 。若jar 包引入又问题,将会报如下错:
# 1. 错误如下
 SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
 SLF4J: Defaulting to no-operation (NOP) logger implementation
 SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
# 2. 因为sl4j和log4j的不兼容导致的,具体处理方案如下: 
 1)若能重新引入依赖,那参考上面引入一个高版本的 log4j-slf4j-impl即可。
 2)若不想上面对应的操作,那就试探引入 对应 slf4j-log4j 版本桥接包,尽让版本高些即可;
           

2、disruptor 的版本不能太低,否则会报:java.lang.NoSuchMethodError: com.lmax.disruptor.dsl.Disruptor.

3.、在依赖包时,默认会到mvn中央仓库去拿数据。但是那可能很慢,导致更新失败或拉不到jar。此时,把 repositories 配置到 aliyun 仓库即可

<repositories>
     <repository>
          <id>aliyun-repo</id>
         <name>nexus aliyun</name>
           <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
       </repository>
  </repositories>
           

3.2.2 log4j2.xml 配置及说明

<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL
    1) Configuration后面的 status ,这个用于设置log4j2自身内部的信息输出,可以不设置 默认 info,
        当设置成trace时,你会看到log4j2内部各种详细输出
    2) monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<Configuration status="info" monitorInterval="30">
    <Properties>
        <Property name="baseLogDir">logs</Property>
        <!-- logback: %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{64}[%line] - %msg%n<-->
        <!-- log4j2:%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %c{64}[%L] - %msg%n/> -->

        <!-- logback 与 log4j2 格式化: 稍有区分。 以下 log4j2 标准
            %d{HH:mm:ss.SSS} 表示输出到毫秒的时间
 	        %t 输出当前线程名称 (%thread] 也行,同logback)
  	        %-5level 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补空格; (%level 也行)
  	        %logger 输出logger名称,因为Root Logger没有名称,所以没有输出;(%c 也行)
 	        %msg 日志文本
  	        %n 换行
        其他常用的占位符有:
  	        %F 输出所在的类文件名,如Log4j2Test.java
  	        %L 输出行号;(不能 %Line。同时,显示行,必须<logger> 配置includeLocation="true")
  	        %M 输出所在方法名
  	        %l 输出语句所在的行数, 包括类名、方法名、文件名、行数;(生效:必须<logger> 配置includeLocation="true"))
        -->
        <Property name="pattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %c{64}[%L] - %msg%n</Property>
    </Properties>
    <!-- 先定义所有的appender -->
    <Appenders>
        <!-- 这个输出控制台的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout charset="UTF-8">
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
        </Console>

        <!-- 应用info日志 -->
        <RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
                                 filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
            <PatternLayout>
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="500MB" />
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
            <Filters>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!-- max=20标识一小时内最多产生20个日志文件 -->
            <DefaultRolloverStrategy max="20">
                <!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3天*24小时*20个日志文件 -->
                <!-- 注意应用需要根据业务需求和磁盘大小评估需要保留的日志个数,对于500M的日志文件来说,要根据应用日志的情况,观察单个日志压缩后文件大小,并计算总大小需要的空间 -->
                <Delete basePath="${baseLogDir}" maxDepth="1">
                    <IfFileName glob="*.gz" />
                    <IfLastModified age="3d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <!-- root是默认的logger,也是所有logger的父级logger,如果需要,可以在这里配置一个文件appender以打印外部组件日志 -->
        <AsyncRoot level="info">
            <AppenderRef ref="Console" />
        </AsyncRoot>
        <!-- 应用日志,采用异步模式,name根据实际的包名修改;生产环境中additivity建议设置为false以避免在root logger中重复打印;
            includeLocation 设置 false, 避免精确到 行 %L, 类%C, 方法%method 的定位,影响性能;
         -->
        <AsyncLogger name="com.lolo" level="info" includeLocation="false" additivity="false">
            <AppenderRef ref="APPINFO_APPENDER" />
        </AsyncLogger>
    </Loggers>
</Configuration>
           

3.2.3 log4j2 最佳实践

3.2.3.1 日志分类

简易日志配置如上。若我们需要对日志分类管理及存储: ThresholdFilter 配置作适当修改即可!

<?xml version="1.0" encoding="UTF-8"?>
<!-- monitorInterval配置成一个正整数,则每隔这么久的时间(秒),log4j2会刷新一次配置。如果不配置则不会动态刷新 -->
<Configuration status="INFO" monitorInterval="30">
    <Properties>
        <!-- 应用需要修改为合适的log路径 -->
        <Property name="baseLogDir">logs</Property>
        <Property name="pattern">%d{yyyyMMdd-HHmmss.SSS} [%level] %c{1} - %msg%n</Property>
    </Properties>
    <!-- 先定义所有的appender -->
    <Appenders>
        <!-- 这个输出控制台的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout>
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
        </Console>
        <!-- 系统日志,可以作为root logger的appender,供打印一些中间件的日志 -->
        <RollingRandomAccessFile name="SYS_APPENDER" fileName="${baseLogDir}/server.log"
            filePattern="${baseLogDir}/server.log.%d{yyyyMMddHH}.%i.gz">
            <PatternLayout>
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="200MB" />
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
            <Filters>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!-- max=6标识一小时内最多产生6个日志文件 -->
            <DefaultRolloverStrategy max="6">
                <!-- 对于指定的路径下的指定后缀的文件,只保留1天的日志文件,那么最多会有24小时*6个日志文件 -->
                <Delete basePath="${baseLogDir}" maxDepth="1">
                    <IfFileName glob="*.gz" />
                    <IfLastModified age="1d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingRandomAccessFile>
        <!-- 应用info日志 -->
        <RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
            filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
            <PatternLayout>
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="500MB" />
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
            <Filters>
                <!-- 当前appender只打印info日志,warn及以上日志忽略,由后面的appender决定是否需要打印 -->
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!-- max=20标识一小时内最多产生20个日志文件 -->
            <DefaultRolloverStrategy max="20">
                <!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3天*24小时*20个日志文件 -->
                <!-- 注意应用需要根据业务需求和磁盘大小评估需要保留的日志个数,对于500M的日志文件来说,要根据应用日志的情况,观察单个日志压缩后文件大小,并计算总大小需要的空间 -->
                <Delete basePath="${baseLogDir}" maxDepth="1">
                    <IfFileName glob="*.gz" />
                    <IfLastModified age="3d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingRandomAccessFile>
        <!-- 应用错误日志 -->
        <RollingRandomAccessFile name="APPERROR_APPENDER" fileName="${baseLogDir}/apperror.log"
            filePattern="${baseLogDir}/apperror.log.%d{yyyyMMddHH}.%i.gz">
            <PatternLayout>
                <Pattern>${pattern}</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="500MB" />
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
            <Filters>
                <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!-- max=10标识一小时内最多产生10个日志文件 -->
            <DefaultRolloverStrategy max="10">
                <!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3*24小时*10个日志文件 -->
                <Delete basePath="${baseLogDir}" maxDepth="1">
                    <IfFileName glob="*.gz" />
                    <IfLastModified age="3d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingRandomAccessFile>
    </Appenders>

    <Loggers>
        <!-- root是默认的logger,也就是公共的logger,供记录一些不常打印的系统参数或者其他组件参数 -->
        <AsyncRoot level="WARN">
            <AppenderRef ref="Console" />
            <AppenderRef ref="SYS_APPENDER" />
        </AsyncRoot>
        <!-- 常打印的应用日志,建议独立配置,并采用异步模式。name根据实际的包名修改,生产环境中additivity建议设置为false以避免在root logger中重复打印 -->
        <AsyncLogger name="com.lolo" level="INFO" includeLocation="false" additivity="false">
            <AppenderRef ref="APPINFO_APPENDER" />
            <AppenderRef ref="APPERROR_APPENDER" />
        </AsyncLogger>
    </Loggers>
</Configuration>
           

3.2.3.2 日志模式-同步/异步

log4j2提供了AsyncAppender和AsyncLogger以及全局异步,开启方式如下

  • 同步模式:默认配置即为同步模式,即没有使用任何AsyncAppender和AsyncLogger;
  • 全局异步:配置按照同步方式配,通过添加jvm启动参数即可开启全局异步,无需修改配置和应用;
# 全局异步 jvm 参数配置
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
# 修正disruptor等待策略  4个策略: Block - Timeout - Sleep - Yield
-Dlog4j2.asyncLoggerWaitStrategy=Block
           
  • 混合异步:使用异步Logger和同步Logger的混合配置,且不开启全局异步,即Logger配置中部分AsyncLogger,部分Logger;
日志模式使用注意事项:
  1. 如果使用异步,建议使用AsyncLogger实现而不是AsyncAppender
  2. 如果使用同步,AsyncLogger、AsyncAppender和全局异步只能使用一种,不可以同时配置AsyncAppender和AsyncLogger,或者配置了异步的情况下启用了全局异步

3.2.3.3 日志滚动和清除策略

log4j2提供了基于文件大小的滚动策略和基于时间的滚动策略,也可以二者并用,这里给出基于大小的滚动策略配置和基于大小/时间双滚动策略配置。

  • 基于大小的滚动策略
<!--此处举例为每500M滚动一个文件,且最多保留20个文件,具体需要根据应用的日志量和希望保留日志大小以及磁盘空间进行评估-->
  <RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
      filePattern="${baseLogDir}/appinfo.log.%i.gz">
      <PatternLayout>
          <Pattern>${pattern}</Pattern>
      </PatternLayout>
      <Policies>
          <SizeBasedTriggeringPolicy size="500MB" />
      </Policies>
      <Filters>
          <!-- 当前appender只打印info日志,warn及以上日志忽略,由后面的错误日志记录 -->
          <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
          <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
      </Filters>
      <!-- max=20表示最多保留20个日志文件 -->
      <DefaultRolloverStrategy max="20"/>
  </RollingRandomAccessFile>
           
  • 基于大小/时间双滚动滚动策略
<!--此处举例为每小时滚动一个文件且每500M滚动一个文件,控制每小时最多保留20个文件,总的文件保留3天-->
  <!--具体需要根据应用的日志量和希望保留日志大小以及磁盘空间进行评估-->
  <RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
      filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
      <PatternLayout>
          <Pattern>${pattern}</Pattern>
      </PatternLayout>
      <Policies>
          <SizeBasedTriggeringPolicy size="500MB" />
          <TimeBasedTriggeringPolicy interval="1" modulate="true" />
      </Policies>
      <Filters>
          <!-- 当前appender只打印info日志,warn及以上日志忽略,由后面的错误日志记录 -->
          <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
          <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
      </Filters>
      <!-- max=20表示一小时内最多保留20个日志文件 -->
      <DefaultRolloverStrategy max="20">
          <!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3天*24小时*20个日志文件 -->
          <!-- 注意应用需要根据业务需求和磁盘大小评估需要保留的日志个数,对于500M的日志文件来说,要根据应用日志的情况,观察单个日志压缩后文件大小,并计算总大小需要的空间 -->
          <Delete basePath="${baseLogDir}" maxDepth="1">
              <IfFileName glob="*.gz" />
              <IfLastModified age="3d" />
          </Delete>
      </DefaultRolloverStrategy>
  </RollingRandomAccessFile>
           
注意:控制总的日志留存时间的机制,需要log4j-2.5及以上的版本支持,如上面的 2.12版本!

3.2.3.4 其他注意事项和调优

  • 推荐在Configuration中添加monitorInterval以支持动态刷新;
  • 推荐使用异步日志而不是同步日志,可以是混合异步也可以是全局异步

    不推荐配置AsyncAppender,如果需要混合异步,使用AsyncLogger;

  • PatternLayout不要使用%L、%C、%method等含有“位置信息”的配置项,非常影响性能。同时logger配置中需要加上inclueLocation=“false”,这样即使配置了位置信息也会被忽略;
  • 推荐使用RollingRandomAccessFile做为appender;
  • 推荐基于大小和时间的双重文件滚动策略,并配合压缩;

3.2.4 log4j-disruptor等待策略

相关测试验证: 同步性能最差(这里就不多说了),异步全局异步的性能接近异步appender的10倍,同样是异步实现的,为何性能有如此大的差距?

参考:disruptor 算法

log4j2的日志使用了disruptor,其内部使用了基于ringbuffer的环形队列,并且也有生产者消费者的概念。在消费者等待消息事件(也就是日志消息)时,其内部有一处等待策略的配置,配置项可以是Block/Timeout/Sleep/Yield,默认Timeout,不同策略的含义如下:

  • Block,在I/O线程等待时使用锁和条件触发机制,当cpu的优先级高于吞吐量和延迟的时候推建议使用。官方推荐在资源受限/虚拟化环境中使用;
  • Timeout,是Block策略的变种,它将定期从等待中被唤醒,确保了如果错过了通知,disruptor消费者不会被永久阻塞,但会有较小的延迟(10ms);
  • Sleep,等待IO事件先自旋,然后调用Thread.yield(),最后park,在性能和cpu之间取得一个折中;
  • Yield,等待IO事件先自旋,然后调用用Thread.yield()等待日志事件,相较于Sleep可能会消耗更多的cpu;

log4j2默认策略是Timeout,在实际测试中,我们尝试测试出不同策略下的cpu占用和延迟时间情况,但测试结果并没有明显的数据对比,因此这里仅供参考,应用如果修改,需要结合场景做全面的测试。例如如果发现cpu占用较高,可以尝试修改为Block或者其他策略并测试观察。

# 修改disruptor wait策略的方法为(以修改为Block为例)
-Dlog4j2.asyncLoggerWaitStrategy=Block
           

3.2.5 其他建议

// 不推荐方式
logger.debug("this is log4j2, current time:" + System.currentTimeMillis());
// 推荐使用占位符
logger.debug("this is log4j2, current time:{}", System.currentTimeMillis());
           

上面两行的代码功能相同,但是前一句在每次执行时,无论我们的日志级别是不是debug即以上,每次都会生成一个新的字符串,字符串的字面值是前缀加上系统当前时间。即使我们的日志级别配置成warn,该句也会产生一个新的字符串

后一句,当我们日志级别是debug或者小于debug的时候,才会真的创建一个完整的字符串,否则内存中只会有包含了占位符的唯一一个字符串

如果这种情况非常多,那么直接拼接字符串的方式对于内存的浪费就非常明显了。若这时候使用占位符的方式,可以明显的改善字符串的生成数量。当然也不是说任何地方都要使用占位符,因为占位符拼接成字符串,也是有开销的,起码要遍历占位符后面的参数。因此一般建议:

  • 对于一定要打印的日志,使用字符串拼接的方式(必要时引入StringBuilder辅助而不是一直加);
  • 对于调整级别才需要打印的日志,使用占位符的方式而不是直接拼接;