Log4j 1.x 升级 Log4j 2.x (调研和升级)

因为公司业务需要,目前的log4j 1.x 遇到死锁,需要升级到Log4j 2.x。现在对目前的日志框架进行调研,并根据目前的现状提出升级的方法。

一引言
对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。Java领域存在多种框架,目前比较常用的日志框架包括:Log4j、Log4J2、Commons Logging、Slf4j、Logback和Jul。Log4j是一个基于Java的日志记录工具。它是由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目。Log4j 2是apache开发的一款Log4j的升级产品。Commons Logging Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons Logging。Slf4j 类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。Logback 一套日志组件的实现(slf4j阵营)。Jul (Java Util Logging),自Java1.4以来的官方日志实现。
由于目前公司使用的日志系统是以Log4j 1.x 为主,Log4J 1.x由于已经停止更新和维护,其中存在的消息丢失和阻塞问题严重,所以对现有点的业务造成的很大的影响。同时由于各个系统使用的日志框架混乱,在系统中同时使用Log4j1.x、JDK自带的Logging和Logback。所以需要统一升级一下框架。
目前业界主要使用的是Log4j1.x、Log4j 2.x和logback。由于Log4j 2 是 Log4j 的升级版本,该版本比起前任来说有着显著的改进,包含很多在 Logback 中的改进以及Logback 架构中存在的问题。所以本文会根据日志的发展路线着重调研Log4j1.x中存在的问题,以及logback和Log4j2.x的优点。最后给出日志系统版本升级的建议。

二调研对象
1 log4j 1.x
Log4j是Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
Log4j主要有三个组件:Loggers(记录器),Appenders (输出源)和Layouts(布局),这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出。综合使用这三个组件可以轻松的记录信息的类型和级别,并可以在运行时控制日志输出的样式和位置。一个典型的Log4j的配置文件log4j.properties如下图1-1所示:
这里写图片描述
图1-1
其中info代表的是记录器的级别,console代表的是记录器的名称,下面四行是对记录器console的配置。
在使用Log4j 1.x过程中出现了一些问题,如死锁、内存溢出和效率不高等现象。
2 logback 相对于Log4j 1.x的改进
Logback是由log4j创始人Ceki 设计的又一个开源日志组件,用来取代log4j的一个日志框架,是slf4j的原生实现。logback当前分成三个模块:logback-core、logback- classic和logback-access。
logback-core是其它两个模块的基础模块,而logback-classic是log4j的一个改良版本,同时它完整实现了slf4j API,使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging等。logback-access提供了访问模块与Servlet容器集成提供通过Http来访问日志的功能。
2.1更快的执行速度
在Log4j 1.x 的基础上,logback 重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上10倍。比如Logback内部的消息队列采用了ArrayBlockingQueue,相对于原始的ArrayList和锁操作来说,管程类的消息队列拥有更好地性能。同时在保证logback的组件更加快速的同时,同时所需的内存更加少。
2.2实现SLF4J
logback-classic中的类自然的实现了SLF4J。使用 logback-classic作为底层实现时,涉及到LF4J日记系统的问题你完全不需要考虑。更进一步来说,由于 logback-classic强烈建议使用SLF4J作为客户端日记系统实现,如果需要切换到log4j或者其他,只需要替换一个jar包即可,不需要去改变那些通过SLF4J API 实现的代码。这可以大大减少更换日记系统的工作量。
2.3自动重新载入配置文件
Logback-classic可以在配置文件被修改后,自动重新载入。这个扫描过程很快,无资源争用,并且可以动态扩展支持在上百个线程之间每秒上百万个调用。它和应用服务器结合良好,并且在JEE环境通用,因为它不会调用创建一个单独的线程来做扫描。
但是由于Logback更新配置时,只是简单的丢弃未处理的日志信息,所以无法作为审计日志框架,但是Log4j 2.x 很完美的解决了事件丢失的问题。
2.4 I/O错误恢复
FileAppender和它的子类,包括RollingFileAppender,可以很容易的从I/O错误中恢复。所以,如果一个文件服务器临时宕机,不需要重启应用,而日志功能就能正常工作。当文件服务器恢复工作,logback相关的appender就会透明地和快速的从上一个错误中恢复。
2.4自动清除旧的日志文件
通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,可以控制日志文件的最大数量。当超出规定的数量事,新的文件会删除旧的文件。如果回滚策略是每月回滚的,比如保存一年的日志,可以设置maxHistory属性为12。对于12个月之前的归档日志文件将被自动清除。
2.5自动压缩归档日志文件
RollingFileAppender可以在回滚操作中,自动压缩归档日志文件。压缩通常是异步执行的,所以即使是很大的日志文件,应用都不会因此而被阻塞。
2.6配置文件中的条件处理
由于需要在不同的目标环境中变换logback的配置文件,例如开发环境,测试环境和生产环境。这些配置文件大体是一样的,除了某部分会有不同。为了避免重复,logback支持配置文件中的条件处理,只需使用,和,那么同一个配置文件就可以在不同的环境中使用了。如下图所示:

2.7堆栈轨迹信息包含包的数据
当logback打印一个异常,堆栈轨迹信息将包含包的相关数据。下面是一个通过 logback-demo 生成的堆栈信息:

从上面的信息,可以看出这个应用使用Struts 1.2.9 而且是使用 jetty 6.1.12部署的。所以,堆栈轨迹信息显示关于异常发生的类还有包和包的版本。作为一个开发人员,可以快速的判断目前系统使用的版本,并且根据相应的版本解决出现的bug。

3 log4j2.x 的突出的改进
Log4j 2是对Log4j1.x 的升级,它比其前身Log4j 1.x提供了显着的改进,并提供了Logback中许多可用的改进功能,同时修复了Logback架构中的一些固有问题。

3.1 高并发和低延迟
高并发和低性能只要是针对Log4j的异步日志记录来说的。虽然由于Log4j 2.x 由于充分利用Java 5的并发特性(主要是使用了一些concurrent包下锁),使得性能得到一定的改善,但是Log4j 2.x中的异步日志器(Asynchronous Loggers)的高并发和低延迟才是这个日志框架的亮点。
在Log4j 1.x 中,使用AsyncAppender类来异步收集日志。它的底层实现是使用了普通的ArrayList,没有采用基于管程的BlockingQueue,并结合条件队列操作wait, notifty来实现阻塞队列。默认容量是128个LoggingEvent。由于内部使用了大量的锁(synchronized),在高并发的情况下影响了系统的异步性能。
而Lockback内部采用的是ArrayBlockingQueue,相比Log4 1.x来说,性能有了很大的提高。
在Log4j 2.x中,Asynchronous Loggers是Log4j2新增的日志器,异步日志器在其内部实现采用了LMAX Disruptor(一个无锁的线程间通信库)技术,Disruptor主要通过环形数组结构、元素位置定位和精巧的无锁设计(CAS)实现了在高并发情形下的高性能。而且Log4j 2.x中Asynchronous Appenders作为Asynchronous Loggers工作的一部分,效果进行了增强。每次写入磁盘时,都会进行flush操作,效果和配置“immediateFlush=true”一样。该异步Appender内部采用ArrayBlockingQueue的方式,因此不需要引入disruptor依赖。RandomAccessFileAppender采用ByteBuffer+RandomAccessFile替代了BufferedOutputStream,官方给出的测试数据是它将速度提升了20-200%。
以下是Disruptor论文中讲述的一个实验:
2.4G 6核 64位的计数器累加5亿次
Method Time (ms)
Single thread 300
Single thread with CAS 5,700
Single thread with lock 10,000
Single thread with volatile write 4,700
Two threads with CAS 30,000
Two threads with lock 224,000

CAS操作比单线程无锁慢了1个数量级;有锁且多线程并发的情况下,速度比单线程无锁慢3个数量级。可见无锁速度最快。单线程情况下,不加锁的性能 > CAS操作的性能 > 加锁的性能。在多线程情况下,为了保证线程安全,必须使用CAS或锁,这种情况下,CAS的性能超过锁的性能,前者大约是后者的8倍。加锁的性能是最差的。
同时在Log4j 2.x中,支持Logger和Appender的同步和异步,既可以只是用同步,也可以同步和异步混合使用。全异步模式的性能要高于混合异步模式,但是如果Log4j2用作审计功能(Audit)的话,建议使用混合异步模式。

下面是Log4j 2.x比较了同步Loggers,Asynchronous Appenders和Asynchronous Loggers的吞吐量。(来自官网):

在64个线程的测试中,Asynchronous Loggers比asynchronous appenders快12倍,比Synchronous Loggers速度快了68倍。而不管进行日志记录的线程数量如何,Asynchronous Loggers的吞吐量随着线程数量的增加而增加,而Synchronous Loggers和asynchronous appenders都具有或多或少的恒定吞吐量。

3.2 自动加载配置文件
Log4j 2.x 和Logback都新增了自动加载日志配置文件的功能,又与Logback不同,配置发生改变时不会丢失任何日志事件。当Log4j 2.x中配置发生改变时,如果还有日志事件尚未处理,Log4j 2会继续处理,当处理完成后,Logger会重新指向新配置的LoggerConfig对象,并且删除无用的对象。
Log4j 2.x能够自动检测配置文件的更改并重新配置自身。如果在配置元素上指定了monitorInterval属性,并将其设置为非零值,则在下次评估和/或记录日志事件并自上次检查以来已经过了monitorInterval时,将检查该文件。下面的示例显示了如何配置属性,以便只有在至少600秒后才会检查配置文件的更改。最小间隔为5秒。

monitorInterval=”600” 指log4j2每隔600秒(10分钟),自动监控该配置文件是否有变化,如果变化,则自动根据文件内容重新配置。
需要注意的是Log4j 2.4之前的版本并不支持.properties属性文件。只支持xml、json、jsn 三种格式。从Log4j 2.x及以后的版本增加了对.properties属性文件解析支持,但是Log4j 2.x 的配置文件是不兼容Log4j 1.x的。
3.4 死锁问题的解决
在Log4j 1.x中同步写日志的时候,在高并发情况下出现死锁导致cpu使用率异常飙升。其中的原因是当一个进程写日志的时候需要获取到Logger和Appender。org.apache.log4j.Logger类继承于org.apache.log4j.Category、Appender继承于org.apache.log4j.AppenderSkeleton。通过如下Log4j 1.x中Category源码和Appender源码可以知道,当多线程并发时,可能会因为相互持有Logger和Appender发生死锁。

                    图3-2 Category源码

图3-3 Appender源码

而在log4j 2.x中充分利用Java5的并发支持,并且以最低级别执行锁定。在Log4j2.x中同步日志的输出过程过获得共享资源(LoggerConfig)的方式采用的是原子变量和concurrent包下的锁。原子变量(CAS)的性能将超过锁的性能。同时原子变量不会有死锁等活跃性问题,达到了解决Lo4j 1.x有死锁的bug。同时concurrent包下的锁大大提高了并发程序的性能。Logback中同样修复了log4j 1.x的很多bug,但是,logback中的有很多类采用同步机制(这种机制导致性能下降)。

3.5更加先进的API
在Log4j 1.x版本中,如果未使用SLF4J门面日志框架,通常我们会像如下方式记录日志:

这种记录方式使得日志的记录像是业务逻辑的一部分,脱离的日志本质。如果有人忘记写if语句,程序输出中会多出很多不必要的字符串,同时存在的很大问题就是内存溢出和两次校验日志级别的问题。目前Log4j2.x,采用如下的方式进行记录日志,字符串的拼接会在日志级别的判断之后进行。

在版本2.4中,Logger界面增加了对lambda表达式的支持,不过需要Java 8的支持。这允许客户端代码在不显式检查所请求的日志级别是否启用的情况下延迟日志记录。只有当这个日志级别是启用的时候才有效。不再需要通过代码检查该级别的日志是否启用,使得代码更加简洁。代码如下所示:

先进的API使得垃圾产生的数量减少,减少内存GC的次数。防止垃圾回收是通过避免创建临时对象来实现的,这意味着需要尽可能的复用已经存在的对象。目前Log4j2.6及以上版本的部分Appenders、Filters和Layouts支持免垃圾模式。部分被复用的对象保存在ThreadLocal区域中,但是对于web应用可能会引起内存泄漏。
应用服务器可能会将ThreadLocal保存在线程池中,这意味着即使应用被卸载,用于日志记录的对象仍然会保持引用。因此,通过ThreadLocal来复用对象的功能在web应用程序中默认是关闭的。
log4j防止触发垃圾回收的另一个方式是在将文本转换为字符数组的时候复用缓冲区且该功能默认是开启的。然而使用同步日志记录器的多线程应用程序可能会有性能影响,因为不同的线程需要竞争共享的缓冲区。如果遇到这种情况,应该优先使用异步日志记录器,或者禁用共享缓冲区。
API本身也已经为避免创建临时对象而修改。除了之前支持简单可变长度参数(这样会创建一个临时数据)的方法之外,log4j新增了所有方法的重载版本,最多支持10个参数。调用方法超过10个参数仍然会使用可变长度参数,这将会创建临时数组。
这个限制对于通过SLF4J使用log4j的场景影响较大,因为这个门面库只提供了最多两个参数的非变长参数。用户如果希望使用超过两个参数,并运行在免垃圾回收模式,就需要抛弃SLF4J。

3.6 更加灵活地属性引用
在复杂的项目中,可能有一些约定的属性比如项目名称、配置文件路径等等。这些属性可能会在多个日志的配置中用到。这样就可以将这些属性配置到Log4j2的配置文件中,方便在多个Logger中共享。
定义属性需要在配置文件中添加properties节点,然后添加多个property。配置完成之后使用${property_name}就可以在项目中引用了。正如下面的一个配置文件中PatternLayout这样。

属性可以在配置文件中引用,也可以直接替代或传入潜在的组件,属性在这些组件中能够动态解析。属性可以是配置文件,系统属性,环境变量,线程上下文映射以及事件中的数据中定义的值。用户可以通过增加自己的Lookup插件来定制自己的属性。

3.7 其他新增的功能
3.5.1 自定义日志级别
Log4J 2支持自定义日志级别。可以在代码或配置中定义自定义日志级别。使用 Level.forName()方法在代码中定义自定义日志级别,如下图所示:

在配置文件中,可以使用标签进行定义:

3.5.2 自定义插件
Log4j 2使用插件系统,不需要对Log4j进行任何更改,就能非常容易添加新的Appenders, Filters, Layouts, Lookups, 和Pattern Converters来扩展框架。所有可以配置的组件都以Log4j插件的形式来定义。 Log4j自动识别预定义的插件,如果在配置中引用到这些插件,Log4j就自动载入使。

然后再配置文件中进行引用:

三 性能分析对比
接下来对三种日志log4j 1.x、log4j 2.x 和logback的性能进行对比:
测试环境:Lo4j1 1.2.17,Log4j 2.8.2,LockBack:1.1.7, JDk:1.8.0_131,Win7
logger 50 threads/500000条数据 50 threads/1000000条数据
Lo4j1:Synchronous 1108ms 2338ms
Logback: Synchronous 2733ms 5360ms
Lo4j2:Synchronous 1307ms 2449ms
Log4j1: Async Appender 954ms 1903ms
Logback: Async Appender 1765ms 3562ms
Log4j2:Logger syn/async 248ms 400ms
Log4j2:Logger all async 15ms 19ms

综合测试数据对比可知:log4j2的异步模式表现了绝对的性能优势,优势主要得益于Disruptor框架的使用,一个无锁的线程间通信库代替了原来的队列。
对于更加详细的性能对比,如下图所示,这是官网上的性能测试:
下面是Log4j 2.x与其他日志记录包的异步吞吐量比较:

以下表格是在JDK1.7.0_06的Solaris 10(64位)上,启用了超线程(4个虚拟内核)的4核Xeon X5570双CPU @ 2.93Ghz:

下面的图表将基于ArrayBlockingQueue的asynchronous appenders的 Logback 1.1.7、Log4j 1.2.17和Log4j 2.6响应时间延迟的对比。在每秒128,000个消息的工作负载下,使用16个线程(每个以每秒8,000个消息的速率记录),我们看到Logback 1.1.7,Log4j 1.2.17遇到的延迟尖峰大于Log4j 2的数量级。

综上可知,Log4j 2.x 不论在高并发和低延迟上都表现出来更加有利的优势。
三 升级建议
1 公司现状
目前在采用的是Log4j 1.2.17和slf4j 1.7.6的组合,主系统采用的是Logback 1.1.7和slf4j 1.7.6。系统中使用Log4j 1.2.17和slf4j 1.7.6相混合。主站系统同时使用Log4j1.x、JDK自带的Logging和Logback。比较混乱,如果想升级到Log4j2.8.2,需要桥接器和适配器进行转换。
2 版本选择
目前官网最新版本是Log4j 2.8.2,建议使用最新的版本,下面表格列举了Log4j 2.8.x和2.7.x的主要修订的一些bug。详细的修改日志见http://logging.apache.org/log4j/2.x/changes-report.html
以下列出的修复主要是针对系统中可能会涉及的功能的修复:
版本 解决的问题
2.8.x 0.修正了AsyncLogger无法解析配置属性的错误
1.当安全管理器存在时,Log4j 2.8可能会丢失异常。
2.当LogEvent.getLoggerName()在KafkaAppender中返回null时处理。
3. 当LogEvent.getLoggerName()在LoggerNameLevelRewritePolicy中返回null时处理。
4. 修复%替换转换器文档中的错字。
5. 当找不到log4j 2配置文件时,改进了错误消息。
6. 使用syncSend = false时发送到Kafka时报告错误。
7. 修复了属性Util :: getCharsetProperty中导致ConsoleAppender的UnsupportedCharsetException异常。
8. RollingFileAppender现在支持省略文件名并直接写入文件.
9. 改进LogEvent序列化以处理不可序列化的消息,并在需要的类丢失时进行反序列化。
10. 将LMAX Disruptor从3.3.5更新为3.3.6。
2.7.x 0. 关闭期间使用JUL日志记录时修复了ClassCastException。
1. RollingFileAppender immediateFlush默认值应为true,而不是false。
2. 修复由日志参数对象的toString()方法嵌套日志触发的乱码日志消息。
3. Log4j线程不再在Tomcat关机时泄漏。
4. 支持将配置中指定的属性值作为值属性以及元素。
5. 使用异步日志记录和扩展堆栈跟踪模式时固定的类加载程序死锁。
6. 在配置中声明的属性现在可以在元素体中或名为“value”的属性中使用。
7. RollingFileAppender、FileAppender现在可以按需创建文件。
8. 允许RollingFileAppender使用默认模式布局。
9.LMAX Disruptor从3.3.4更新到3.3.5。
10. Kafka客户端从0.10.0.0更新到0.10.0.1。

3 升级要求
Apache Log4j 2与以前的版本不兼容。升级到项目中的Log4j 2时,请注意以下几点:
①Log4j 2.4及更高版本需要Java 7,版本2.0-alpha1至2.3所需的Java 6。
②XML配置已被简化,与Log4j 1.x不兼容。
③通过属性文件配置从2.4版本支持,但与Log4j 1.x不兼容。
④支持通过JSON或YAML进行配置,但这些格式需要额外的运行时依赖关系。
⑤虽然Log4j 2与Log4j不直接兼容1.xa兼容性桥已经提供了减少编码变化的需要。
在投顾系统中,使用的是Log4j 1.2.17和slf4j 1.7.6的组合,如果升级的话需要进行以下pom文件中的替换:
注意:由于Log4j 2与Log4j 1.x的配置文件比兼容,Properties文件中很多语法发生了变化。需要重写Log4j 2的配置文件,下图是新旧配置的简单对比:

同时由于Spring最新的版本仍然对Log4j 2.x 的配置文件无法解析(虽然接口文档上支持Log4j 2.x,但是源码上却仍然使用的是Log4j 1.x的工具进行解析),所以对于配置文件的初始化,需要放在web.xml中,这样就会导致spring的单元测试无法加载属性配置文件。有两种解决方案:
第一种:Log的配置文件初始化加载放在web.xml中。如下图所示:

第二种方案是需要单元测试的单元中编程载入Log4j 2.x的配置文件,如下代码所示:

这样单元测试才能使用到Log4j 2.x 配置。

对于POM文件的修改,需要去掉Log4j 1和Logback的依赖包,同时加上Log4j 2.x的依赖包,其中Log4j-web用于属性文件的加载和监听,disruptor用于异步记录日志使用。如下所示:

同时, 如果在Log4j 1.x 中使用Logger,而Log4j 1.x使用的是LogManager,如果没有使用sslf4j接口就需要手动替换Logger->LogManager或者替换成Logger->LoggerFactory。
建议工程里面全部修改成利用日志门面使用日志。

猜你喜欢

转载自blog.csdn.net/whbing1471/article/details/74278048