zoukankan      html  css  js  c++  java
  • Log4J,在踩坑中升级版本

    基本概念

    Commons-logging

    apache最早提供的日志的门面接口。避免和具体的日志方案(log4j、logback等)直接耦合。类似于JDBC的api接口,具体的的JDBC driver实现由各数据库提供商实现。JCL的思想也是想通过统一接口解耦,将日志方案的实现与日志接口分离。
    但是Apache Commons Logging的动态绑定并不是总能成功。解决方法之一就是在程序部署时静态绑定指定的日志工具,这就是slf4j产生的原因。

    slf4j

    slf4j跟JCL一样,slf4j也是只提供log接口,具体的实现是在打包应用程序时所放入的绑定器(名字为 slf4j-XXX-version.jar)来决定,XXX 可以是log4j12, jdk14, jcl, nop等,他们实现了跟具体日志工具(比如 log4j)的绑定及代理工作。举个例子:如果一个程序希望用log4j日志工具,那么程序只需针对slf4j-api接口编程,然后在打包时再放入log4j-slf4j-impl.jar等相关包就可以了。

    比如我们的系统,使用了slf4j+log4j,需要使用到下面几个包

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.21</version>
    </dependency>
    

    Log4j、Logback

    Log4j

    Apache的一个开放源代码项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;用户也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,用户能够更加细致地控制日志的生成过程。这些可以通过一个 配置文件来灵活地进行配置,而不需要修改程序代码。

    LogBack

    Logback是由log4j创始人设计的又一个开源日记组件。logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个改良版本。此外logback-classic完整实现slf4j API使你可以很方便地更换成其它日记系统如log4j或JDK14 Logging。logback-access访问模块与Servlet容器集成提供通过Http来访问日记的功能。

    Log4j与LogBack比较

    LogBack作为一个通用可靠、快速灵活的日志框架,将作为Log4j的替代和SLF4J组成新的日志系统的完整实现。LOGBack声称具有极佳的性能,“ 某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在LogBack中需要3纳秒,而在Log4J中则需要30纳秒。 LogBack创建记录器(logger)的速度也更快:13微秒,而在Log4J中需要23微秒。更重要的是,它获取已存在的记录器只需94纳秒,而 Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的”。 另外,LOGBack的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档。

    Log4j版本升级

    对于Log4j的使用场景,肯定是多线程并发的情况。一个new FileOutputStream对象表示一个打开文件对象。那么当多个线程使用同一个FileOutputStream对象写文件时,就需要进行同步操作。
    可以把一个FileAppender对象理解成维护了一个打开文件对象,当Logger在多线程情况下把日志写入文件时,需要对Appender进行同步,也就是说对Logger加锁,保证使用同一个Logger对象时,一次只有一个线程使用这个FileAppender来写文件,避免了多线程情况下写文件错误。

    log4j 1.x版本的锁

    Hierarchy.java,多个线程使用同一个Logger时,加锁。

    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);
      }
    }
    

    AppenderSkeleton.java,多个线程使用同一个Appender时,对Appender加锁

      public  synchronized void doAppend(LoggingEvent event) {
        if(closed) {
          LogLog.error("Attempted to append to closed appender named ["+name+"].");
          return;
        }
    
        if(!isAsSevereAsThreshold(event.getLevel())) {
          return;
        }
    
        Filter f = this.headFilter;
    
        FILTER_LOOP:
        while(f != null) {
          switch(f.decide(event)) {
            case Filter.DENY: return;
            case Filter.ACCEPT: break FILTER_LOOP;
            case Filter.NEUTRAL: f = f.getNext();
          }
        }
    
        this.append(event);    
      }
    

    这几个锁在高并发的情况下对系统的性能影响很大,会阻塞掉业务进程,特别是在for循环里加锁的方式很不可取。而日志收集工作应该是作为一个辅助功能,不能影响主业务功能。

    log4j 2.3的线程切换

    Log4j 2使用了新一代的基于LMAX Disruptor的无锁异步日志系统。在多线程的程序中,异步日志系统吞吐量比Log4j 1.x和logback高10倍,而时间延迟更低。

    听起来很美,是不是?
    发现这个问题的原因是我们线上出了一次故障,当一次网络异常,导致上游不断重试并发请求量特别高时,cpu利用率跑到了4000%,服务完全恢复不过来了,一开始以为是GC的原因导致服务出问题了。后来在线下进行复现,查看GC发现没啥异常,但是发现了如下的数据

    dstat

    usr 占用不高,sys 占用超高,同时csw(context switch) 达到1200w,一次csw 大约耗费1000ns,已经逼近cpu的极限。

    jstack -l 43911> 43911.log
    grep tid 43911.log | wc -l
    12312
    grep RUNNABLE 43911.log | wc -l
    53

    总线程12312,处于runnable的只有53个,看看这些线程在干什么

    "pool-6-thread-14380" prio=10 tid=0x00007f7087594000 nid=0x53e1 runnable [0x00007f6b67bfc000]
    java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:349)
    at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:136)
    at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:105)
    at com.lmax.disruptor.RingBuffer.publishEvent(RingBuffer.java:502)
    at org.apache.logging.log4j.core.async.AsyncLoggerConfigHelper.callAppendersFromAnotherThread(AsyncLoggerConfigHelper.java:342)
    at org.apache.logging.log4j.core.async.AsyncLoggerConfig.callAppenders(AsyncLoggerConfig.java:114)
    ......

    grep "LockSupport.java:349" 43911.log | wc -l
    11536

    线程都跑在LockSupport.java:349,
    unsafe.park(false, 1);
    1 nano = 10^-9s, 推测大量线程频繁的短时间sleep造成了大量的线程切换,做个实验:

    public static void contextSwitchTest(int threadCount) throws Exception {
      ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
      for (int i = 0; i < threadCount; i++) {
        executorService.execute(new Runnable() {
          @Override
          public void run() {
            while (true) {
              LockSupport.parkNanos(1);
            }
          }
        });
      }
      executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }
    

    在一台搭载了两个E5-2670 v3 @ 2.30GHz的机器上测试这段代码,在threadCount达到600后,测试跑起来后立即发现上下文切换到百万级别,推测被印证。

    原因找到了,接下来看看出问题是log生产速度怎么样:通过不断地ls -al error.log/business.log,发现Log的生成速度才几MB/s,远没有达到磁盘的极限200M/s,再做个测试:

    private static void loggerSpeedTest(int threadCount) throws Exception {
      ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
      for (int i = 0; i < threadCount; i++) {
        executorService.execute(new Runnable() {
          @Override
          public void run() {
            while (true) {
              LOGGER.error("test log4j2 logging speed", new UnsupportedOperationException());
            }
          }
        });
      }
      executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }
    

    线程少的时候速度还行,线程一多很慢,问题找到了,但什么造成的这个问题呢,顺着stacktrace挖一挖:

    AsyncLoggerConfig.callAppenders()

    @Override
    protected void callAppenders(final LogEvent event) {
        // populate lazily initialized fields
        event.getSource();
        event.getThreadName();
    
        // pass on the event to a separate thread
        if (!helper.callAppendersFromAnotherThread(event)) {
            super.callAppenders(event);
        }
    }
    

    AsyncLoggerConfigHelper.callAppendersFromAnotherThread()

    public boolean callAppendersFromAnotherThread(final LogEvent event) {
        // TODO refactor to reduce size to <= 35 bytecodes to allow JVM to inline it
        final Disruptor<Log4jEventWrapper> temp = disruptor;
        if (temp == null) { // LOG4J2-639
            LOGGER.fatal("Ignoring log event after log4j was shut down");
            return true;
        }
    
        // LOG4J2-471: prevent deadlock when RingBuffer is full and object
        // being logged calls Logger.log() from its toString() method
        if (isAppenderThread.get() == Boolean.TRUE //
                && temp.getRingBuffer().remainingCapacity() == 0) {
    
            // bypass RingBuffer and invoke Appender directly
            return false;
        }
        // LOG4J2-639: catch NPE if disruptor field was set to null after our check above
        try {
            LogEvent logEvent = event;
            if (event instanceof RingBufferLogEvent) {
                logEvent = ((RingBufferLogEvent) event).createMemento();
            }
            logEvent.getMessage().getFormattedMessage(); // LOG4J2-763: ask message to freeze parameters
    
            // Note: do NOT use the temp variable above!
            // That could result in adding a log event to the disruptor after it was shut down,
            // which could cause the publishEvent method to hang and never return.
            disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
        } catch (final NullPointerException npe) {
            LOGGER.fatal("Ignoring log event after log4j was shut down.");
        }
        return true;
    }
    

    RingBuffer.publishEvent()

    @Override
    public <A, B> void publishEvent(EventTranslatorTwoArg<E, A, B> translator, A arg0, B arg1){
        final long sequence = sequencer.next();
        translateAndPublish(translator, sequence, arg0, arg1);
    }
    

    MultiProducerSequencer.next()

    @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;
    }
    

    整个流程下来就是说在消费速度跟不上生产速度的时候,生产线程做了无限重试,重试间隔为1 nano,这个1 nano 会导致线程被频繁休眠/唤醒,造成kernal cpu 利用率高,context switch已经达到了cpu的极限,进而导致写log的线程的线程获取cpu时间少,吞吐量下降。

    查了下log4j的bug,发现:https://github.com/apache/logging-log4j2/commit/7a7f5e4ed1ce8a27357a12a06d32ca2b04e5eb56

    if this fails because the queue is full, then fall back to asking AsyncEventRouter what to do with the event,

    把log4j2版本切到2.7, 跑一下上面的测试,发现性能上来了, context swtich也有了数量级的下降,看看怎么办到的:

    AsyncLoggerConfig.callAppenders()

    @Override
    protected void callAppenders(final LogEvent event) {
        populateLazilyInitializedFields(event);
        if (!delegate.tryEnqueue(event, this)) {
            final EventRoute eventRoute = delegate.getEventRoute(event.getLevel());
            eventRoute.logMessage(this, event);
        }
    }
    

    AsyncLoggerConfigDisruptor.getEventRoute()

    @Override
    public EventRoute getEventRoute(final Level logLevel) {
        final int remainingCapacity = remainingDisruptorCapacity();
        if (remainingCapacity < 0) {
            return EventRoute.DISCARD;
        }
        return asyncQueueFullPolicy.getRoute(backgroundThreadId, logLevel);
    }
    

    DefaultAsyncQueueFullPolicy.getRoute()

    @Override
    public EventRoute getRoute(final long backgroundThreadId, final Level level) {
    
        // LOG4J2-1518: prevent deadlock when RingBuffer is full and object being logged calls
        // Logger.log in application thread
        // See also LOG4J2-471: prevent deadlock when RingBuffer is full and object
        // being logged calls Logger.log() from its toString() method in background thread
        return EventRoute.SYNCHRONOUS;
    }
    

    没有了park,被block的线程没有一直被调度,context switch减少了,kernel cpu下降了,真正获取到lock的线程获得了更多的cpu用来干活了。

    log4j的性能

    一想到性能,有哪些是对java程序影响大的呢? 我们直观地会认为反射、编解码,这些东西对性能影响比较大。
    使用JProfiler进行分析后,一些结果却让人吃惊不小,最耗CPU的居然是以下函数
    InetSocketAddress.getHostName()
    Log.info
    String.format
    String.replace
    Gson.fromJson
    把log关闭后,InetSocketAddress.getHostName()的居然占到了惊人的27%!

    当然,性能并不是最重要的指标,相比于日志能带来的定位线上问题的好处,这一点性能的损耗其实不值一提,毕竟业务开发中,效率和稳定性才是最重要的。

    参考文档

    java日志,需要知道的几件事
    本文中关于log4j 2.3的主要内容其实是我mentor写的,我主要是重新走了一下流程,补全了图片与测试数据,并对内容做了少部分处理。

  • 相关阅读:
    hive与hbase整合
    待重写
    hive DML
    【知识强化】第六章 总线 6.1 总线概述
    【知识强化】第五章 中央处理器 5.1 CPU的功能和基本结构
    【知识强化】第四章 指令系统 4.3 CISC和RISC的基本概念
    【知识强化】第四章 指令系统 4.2 指令寻址方式
    【知识强化】第四章 指令系统 4.1 指令格式
    【知识强化】第三章 存储系统 3.6 高速缓冲存储器
    【知识强化】第三章 存储系统 3.5 双口RAM和多模块存储器
  • 原文地址:https://www.cnblogs.com/flystar32/p/6751895.html
Copyright © 2011-2022 走看看