• logback异步日志打印阻塞工作线程


    前言

    最新做项目,发现一些历史遗留问题,典型的是日志打印的配置问题,其实都是些简单问题,但是往往简单问题引起严重的事故,比如日志打印阻塞工作线程,以logback和log4j2为例。logback实际上是springboot的官方默认日志实现框架,承载SLF4J-API,所以基于java开发的云原生项目基本上就是logback打印日志,logback异步appender日志的打印架构

    可以看到consoleAppender实际上也是异步(非同步)的范畴

    准备demo

    springboot的demo,这个可以用Spring官方的脚手架生成

    其中启动的console日志就是logback打印的,虽然我们没有配置logback的xml。而现实情况是需要配置文件的,毕竟需要异步打印日志,日志切割,保存时间等都需要配置,关键还要日志格式。

    1. "1.0" encoding="UTF-8"?>
    2. <configuration debug="false">
    3. <property name="LOGBACK_HOME" value="/opt/xxx" />
    4. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    5. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    6. <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
    7. encoder>
    8. appender>
    9. <appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
    10. <file>${log.path}/app.logfile>
    11. <encoder>
    12. <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
    13. <charset>UTF-8charset>
    14. encoder>
    15. <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    16. <fileNamePattern>${log.path}/app.%d{yyyy-MM-dd}.%i.log.gzfileNamePattern>
    17. <maxFileSize>200MBmaxFileSize>
    18. <maxHistory>10maxHistory>
    19. rollingPolicy>
    20. appender>
    21. <appender name="ASYNC_LOG" class="ch.qos.logback.classic.AsyncAppender" >
    22. <discardingThreshold>0discardingThreshold>
    23. <queueSize>1024queueSize>
    24. <neverBlock>trueneverBlock>
    25. <appender-ref ref="fileLog" />
    26. appender>
    27. <logger name="com.apache.ibatis" level="TRACE"/>
    28. <logger name="java.sql.Connection" level="DEBUG"/>
    29. <logger name="java.sql.Statement" level="DEBUG"/>
    30. <logger name="java.sql.PreparedStatement" level="DEBUG"/>
    31. <root level="INFO">
    32. <appender-ref ref="STDOUT" />
    33. <appender-ref ref="ASYNC_LOG"/>
    34. root>
    35. configuration>

    笔者参考各个网站简单写了一个xml

    原理

    那么为什么日志打印会阻塞工作线程了,不是异步的嘛吗?异步是没错,但是异步解耦却是依赖队列,不同于分布式MQ,本地队列在某些配置时是阻塞的,所以异步日志实际上是半同步,有点像MySQL的复制原理,架构设计实际上很多地方非常相似。如果不想丢日志,可以提高消费队列日志线程的数量,增加CPU资源消耗。

    参考logback官方文档:Chapter 4: Appenders

    这个就是前言的代码分析,console也是属于异步范畴。

    AsyncAppender配置如下 

    这里关键有3个配置

    1. queueSize,队列的大小,默认256
    2. discardingThreshold,队列数量剩余数,达到或者小于,就丢弃TRACE DEBUG INFO日志
    3. neverBlock,从不阻塞,队列满就丢日志

     

    最简单的架构图,写文件是异步线程,但是写queue是同步的

    源码分析

     ch.qos.logback.classic.AsyncAppender,关键还是父类ch.qos.logback.core.AsyncAppenderBase

    1. /**
    2. * In order to optimize performance this appender deems events of level TRACE,
    3. * DEBUG and INFO as discardable. See the
    4. * on appenders in the manual for further information.
    5. *
    6. *
    7. * @author Ceki Gülcü
    8. * @since 1.0.4
    9. */
    10. public class AsyncAppender extends AsyncAppenderBase {
    11. boolean includeCallerData = false;
    12. /**
    13. * Events of level TRACE, DEBUG and INFO are deemed to be discardable.
    14. * 定义丢弃日志的级别,文档写的就是这里实现的
    15. *
    16. * @param event
    17. * @return true if the event is of level TRACE, DEBUG or INFO false otherwise.
    18. */
    19. protected boolean isDiscardable(ILoggingEvent event) {
    20. Level level = event.getLevel();
    21. return level.toInt() <= Level.INFO_INT;
    22. }
    23. protected void preprocess(ILoggingEvent eventObject) {
    24. eventObject.prepareForDeferredProcessing();
    25. if (includeCallerData)
    26. eventObject.getCallerData();
    27. }

    这个类核心还是,按照级别丢日志的定义,比如queue能存储的大小少于1/5时,那些级别日志丢弃 ,再看父类启动的时候分析初始值,发现queue是

    ArrayBlockingQueue

     

    定义了队列discardingThreshold的值,注意:这个是队列数信号量,不是百分比,发现一些业务配置20,😄

    public static final int DEFAULT_QUEUE_SIZE = 256;
    int queueSize = DEFAULT_QUEUE_SIZE;

    默认队列数256,建议配置大一点,根据内存分配情况,过大会OOM 

     在分析日志入队列的过程

     

    分析 

    ArrayBlockingQueue
    1. /**
    2. * Inserts the specified element at the tail of this queue if it is
    3. * possible to do so immediately without exceeding the queue's capacity,
    4. * returning {@code true} upon success and {@code false} if this queue
    5. * is full. This method is generally preferable to method {@link #add},
    6. * which can fail to insert an element only by throwing an exception.
    7. *
    8. * @throws NullPointerException if the specified element is null
    9. */
    10. public boolean offer(E e) {
    11. Objects.requireNonNull(e);
    12. final ReentrantLock lock = this.lock;
    13. lock.lock();
    14. try {
    15. if (count == items.length)
    16. return false;
    17. else {
    18. enqueue(e);
    19. return true;
    20. }
    21. } finally {
    22. lock.unlock();
    23. }
    24. }

    队列数满,直接丢弃,所以不阻塞,丢失日志。 

    log4j2

    log4j2实际上根据各方测试说,比logback性能强一些,但是也会出现同样的问题

    官方文档:Log4j – Log4j 2 Appenders

    apache开源的文档管理要好一些,写的很详细,而且有详细的说明和示例,不过设计原理都差不多 

    总结

    实际上这个问题是使用问题,非常简单,不过越是简单的使用,却可能出现致命问题,一般公司都会统一脚手架或者统一规范的方式来实现标准的日志配置文件,防止错误配置导致业务问题,不知道未来Java虚拟线程大规模使用会不会缓解日志打印阻塞工作线程的问题,毕竟调度更优,不过如果线程池满载,虚拟线程也是无能为力。还是需要在丢日志和存储消费日志的能力作取舍。

  • 相关阅读:
    数据结构(二)——线性表(单链表)
    springboot自动装配
    PPT真人出镜如何录制?ppt和真人怎么同时出镜?
    Open3D Ransac算法分割多个点云平面
    RabbitMQ之ttl(过期消息)解读
    C语言趣味代码(三)
    虽然webpack4以后。webpack 可以不用再引入一个配置文件来打包项目,但还是梳理常用配置信息
    CSS文本属性
    计算机毕业设计Java客户关系管理平台(源码+mysql数据库+系统+lw文档)
    Cannot read property ‘pickAlgorithm‘ of null
  • 原文地址:https://blog.csdn.net/fenglllle/article/details/134361656