美文网首页JAVA-第三方
log4j引起的性能问题

log4j引起的性能问题

作者: 理查德成 | 来源:发表于2018-12-03 15:24 被阅读146次
一、一次服务异常

web的问题

近期生产环境出现某web项目访问慢,有时甚至拒绝服务的现象;经查日志,发现web的日志中,频繁出现调用依赖的服务时,连接超时的问题;初步怀疑被依赖的服务请求处理过慢,但是有时一些明显不存在性能问题的请求,处理也很慢。

服务的问题

在我司的系统架构中,服务之间的远程通信采用采用Thrift实现;被依赖服务的日志中,频繁出现Connection reset by peer连接重置的问题。这个问题,是服务端企图从已经关闭的连接中读取或者写数据导致的,这就印证了web端的问题,确实是服务器响应太慢,导致web端因超时在响应前断开了连接,从而服务端出现连接重置的问题。

高并发下的log4j性能瓶颈

既然不存在性能问题的请求响应速度变慢了,那一定是请求处理过程中的某些外部因素,导致了速度变慢;比如数据库异常了;当让排查这个问题时并没有按照这个思路,而是在生产出问题时,直接查看了服务的线程堆栈信息(jstack),发现大量线程的堆栈类似如下:

"pool-1-thread-190" prio=10 tid=0x00002b853809d800 nid=0xc01 waiting for monitor entry [0x00002b84b85d5000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at org.apache.log4j.Category.callAppenders(Category.java:204)
    - waiting to lock <0x00000007880579e8> (a org.apache.log4j.spi.RootLogger)
    at org.apache.log4j.Category.forcedLog(Category.java:391)
    at org.apache.log4j.Category.info(Category.java:666)
    at com.jlpay.commons.rpc.thrift.server.Dispatcher.invoke(Dispatcher.java:38)
    at com.jlpay.commons.rpc.thrift.server.RpcAdapterImpl.Invoke(RpcAdapterImpl.java:32)
    at com.jlpay.commons.rpc.thrift.server.RpcAdapter$Processor$Invoke.getResult(RpcAdapter.java:175)
    at com.jlpay.commons.rpc.thrift.server.RpcAdapter$Processor$Invoke.getResult(RpcAdapter.java:160)
    at org.apache.thrift.ProcessFunction.process(ProcessFunction.java:39)
    at org.apache.thrift.TBaseProcessor.process(TBaseProcessor.java:39)
    at org.apache.thrift.server.AbstractNonblockingServer$FrameBuffer.invoke(AbstractNonblockingServer.java:518)
    at org.apache.thrift.server.Invocation.run(Invocation.java:18)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)

大量线程阻塞,等待某个锁,但是这个锁被以下线程持有:

"pool-1-thread-102" prio=10 tid=0x00002b8538053800 nid=0xba9 runnable [0x00002b84b2f71000]
   java.lang.Thread.State: RUNNABLE
    at java.text.DateFormat.format(DateFormat.java:336)
    at org.apache.log4j.helpers.PatternParser$DatePatternConverter.convert(PatternParser.java:443)
    at org.apache.log4j.helpers.PatternConverter.format(PatternConverter.java:65)
    at org.apache.log4j.PatternLayout.format(PatternLayout.java:506)
    at org.apache.log4j.WriterAppender.subAppend(WriterAppender.java:310)
    at org.apache.log4j.WriterAppender.append(WriterAppender.java:162)
    at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251)
    - locked <0x0000000788057650> (a org.apache.log4j.ConsoleAppender)
    at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66)
    at org.apache.log4j.Category.callAppenders(Category.java:206)
    - locked <0x00000007880579e8> (a org.apache.log4j.spi.RootLogger)

这样,大量线程在等待获取锁写日志,请求响应变慢了,一切都说得通了。

问题总结

那么,上面的线程堆栈信息真的有问题吗 ?

如果单单是堆栈信息里存在线程等待锁而阻塞,没有web服务以及其依赖服务日志中的请求超时以及连接重置问题,这样的堆栈信息是完全没有任何问题的。
在服务中因访问共享资源而使用同步代码很常见,一些特定的业务,以及资源的稀缺性决定了我们代码中的多线程必须使用同步操作。临界区(访问共享资源的代码片段)同一时刻只能由一个线程进入,那么其他线程必然是在临界区外等待的,此时的java线程堆栈信息就如以上,由一个线程获得锁,其他线程在等待。

高并发下
但是为什么这种问题不是特别频繁呢,其实还与并发量有关,高并发下,大量写日志请求,这个问题就会暴露出来。而且写日志操作相对来说还是比较慢的,高并发下会导致请求的平均处理时间高于正常情况下的处理时间;但是处理时间也不会大幅度增加到引起客户端请求超时的地步。因为相比与纯粹的cpu处理操作,写日志是一个慢操作,但是也不是肉眼能见的慢,写完日志的线程最终会释放锁,其他线程获得锁。

如果服务可同时处理的请求很多,线程数量很多,那写日志的等待时间确实是客观的。但是一般的服务,如web容器tomcat之类,分发请求线程池大小配置都是合理的,不会出现巨多线程同时存活;因此,log4j对请求造成的影响,是锦上添花的;但是另一方面,如果日志打印不合理,一个业务中由很多次日志打印,那这部分等待锁的时间叠加起来也是客观的了。

解决问题的方法:

  1. 根本原因还在于服务中确实存在处理很慢的请求,需要优化这一部分请求;
  2. 日志打印要合理,不要多打,也不要不打;在成熟的接口上,关闭日志输出,利于提高效率
  3. 生产日志级别调高,一般只打印info及以上的日志;
  4. 采用性能更高的log4j2或者logback替换log4j
二、log4j的性能瓶颈

通过以上线程堆栈信息:

"pool-1-thread-190" prio=10 tid=0x00002b853809d800 nid=0xc01 waiting for monitor entry [0x00002b84b85d5000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at org.apache.log4j.Category.callAppenders(Category.java:204)
    - waiting to lock <0x00000007880579e8> (a org.apache.log4j.spi.RootLogger)
    at org.apache.log4j.Category.forcedLog(Category.java:391)

可以很明显的看到,at org.apache.log4j.Category.callAppenders(Category.java:204)在类org.apache.log4j.Category的方法callAppenders调用中(204行)等待a org.apache.log4j.spi.RootLogger的锁;

翻看源码:

public void callAppenders(LoggingEvent event) {
        int writes = 0;
        for (Category c = this; c != null; c = c.parent) {
            // Protected against simultaneous call to addAppender, removeAppender,...
            synchronized (c) {
                if (c.aai != null) {
                    writes += c.aai.appendLoopOnAppenders(event);
                }
                if (!c.additive) {
                    break;
                }
            }
        }
        if (writes == 0) {
            repository.emitNoAppenderWarning(this);
        }
    }

这里有一个同步代码块。

org.apache.log4j.spi.RootLogger

堆栈信息中的org.apache.log4j.spi.RootLogger是什么?

log4j中的架构中:

image.png

其中Logger是日志实例,Appender是日志输出目的地。日志输出到哪个目的地,是由日志实例名以及配置决定的:

日志实例:

Logger logger = Logger.getLogger("test.child");

配置:

log4j.rootLogger=DEBUG,Console

log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
log4j.appender.Console.Threshold=DEBUG
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%d{HH\:mm\:ss SS}][%t][%p][%X{logid}]%m%n

日志实例中,test.child就是实例名,通过.来继承,即名为test.child的实例继承自test,所有实例隐式继承自根实例;这里的继承,是指继承日志级别以及输出目的地。

配置中,log4j.rootLogger指定根实例的输出目的地,根实例就是独一无二的org.apache.log4j.spi.RootLogger; log4j.logger.instanceName指定名为instanceName的实例的输出目的地。

竞争父Logger的锁

在实现上,org.apache.log4j.Category是所有Logger的父类,其中一个属性:

/**
 * The parent of this category. All categories have at least one
 * ancestor which is the root category.
 */
volatile protected Category parent;

parent指定了日志实例的父级,回过头来看打印日志的源码:

/**
 * Call the appenders in the hierrachy starting at
 * <code>this</code>.  If no appenders could be found, emit a
 * warning.
 *
 * <p>This method calls all the appenders inherited from the
 * hierarchy circumventing any evaluation of whether to log or not
 * to log the particular log request.
 *
 * @param event the event to log.
 */
public void callAppenders(LoggingEvent event) {
    int writes = 0;

    for (Category c = this; c != null; c = c.parent) {
        // Protected against simultaneous call to addAppender, removeAppender,...
        synchronized (c) {
            if (c.aai != null) {
                writes += c.aai.appendLoopOnAppenders(event);
            }
            if (!c.additive) {
                break;
            }
        }
    }

    if (writes == 0) {
        repository.emitNoAppenderWarning(this);
    }
}

日志打印时,会从当前日志实例开始,调用实例继承链中的所有父实例的appender

同步代码块首先同步的是当前日志实例的内部锁(synchronized机制),继承链中自下而上,锁竞争越激烈,处于最顶层的根Logger(若配置了根)最为激烈,这就是log4j会造成服务性能下降的原因所在。

相关文章

网友评论

    本文标题:log4j引起的性能问题

    本文链接:https://www.haomeiwen.com/subject/cgvycqtx.html