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

前言

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

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

准备demo

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

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

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

    <!--定义日志文件的存储地址 可以使用环境变量或者系统变量占位-->
    <property name="LOGBACK_HOME" value="/opt/xxx" />

    <!--控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 文件 滚动日志 -->
    <appender name="fileLog"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 当前日志输出路径、文件名 -->
        <file>${log.path}/app.log</file>
        <!--日志输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--历史日志归档策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 历史日志: 归档文件名 -->
            <fileNamePattern>${log.path}/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!--单个文件的最大大小-->
            <maxFileSize>200MB</maxFileSize>
            <!--日志文件保留天数-->
            <maxHistory>10</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- 文件 异步日志(async) -->
    <appender name="ASYNC_LOG" class="ch.qos.logback.classic.AsyncAppender" >
        <!-- 不丢失日志.默认的256/5,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的长度,默认值为256 -->
        <queueSize>1024</queueSize>
        <neverBlock>true</neverBlock>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="fileLog" />
    </appender>

    <!--自定义日志级别 myibatis可以输出SQL-->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="ASYNC_LOG"/>
    </root>
</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

java 复制代码
/**
 * In order to optimize performance this appender deems events of level TRACE,
 * DEBUG and INFO as discardable. See the
 * <a href="http://logback.qos.ch/manual/appenders.html#AsyncAppender">chapter
 * on appenders</a> in the manual for further information.
 *
 *
 * @author Ceki G&uuml;lc&uuml;
 * @since 1.0.4
 */
public class AsyncAppender extends AsyncAppenderBase<ILoggingEvent> {

    boolean includeCallerData = false;

    /**
     * Events of level TRACE, DEBUG and INFO are deemed to be discardable.
     * 定义丢弃日志的级别,文档写的就是这里实现的
     * 
     * @param event
     * @return true if the event is of level TRACE, DEBUG or INFO false otherwise.
     */
    protected boolean isDiscardable(ILoggingEvent event) {
        Level level = event.getLevel();
        return level.toInt() <= Level.INFO_INT;
    }

    protected void preprocess(ILoggingEvent eventObject) {
        eventObject.prepareForDeferredProcessing();
        if (includeCallerData)
            eventObject.getCallerData();
    }

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

复制代码
ArrayBlockingQueue

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

复制代码
public static final int DEFAULT_QUEUE_SIZE = 256;
int queueSize = DEFAULT_QUEUE_SIZE;

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

在分析日志入队列的过程

分析

复制代码
ArrayBlockingQueue
java 复制代码
    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

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

log4j2

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

官方文档:Log4j -- Log4j 2 Appenders

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

总结

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

相关推荐
千里码aicood35 分钟前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455661 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
架构师吕师傅2 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
bug菌2 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
夜月行者4 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
Yvemil74 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance4 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
代码在改了4 小时前
springboot厨房达人美食分享平台(源码+文档+调试+答疑)
java·spring boot
猿java5 小时前
使用 Kafka面临的挑战
java·后端·kafka