log4j2异步模式源码解析

今天线上业务因为日志打印问题,影响了服务的稳定性,因此了解了下log4j2的日志打印的异步模式。本文先提供下配置异步模式的方法,然后跟踪源码,看下log4j2是如何实现异步日志打印的,然后说明一些配置异步模式过程中容易弄错的点以及原因,最后总结下流程。通过和作者一起debug跟踪代码,大家可以了解到一些debug查看源码的思路,并且可以了解log4j2实现的关键节点和主要流程。

日志打印模式

首先需要说明的是log4j2组件中有几种日志打印模式,同步模式,异步模式,以及无锁异步模式。

  • 同步模式是默认的日志打印模式,他是在处理业务的线程上执行日志打印逻辑,当业务线程执行到 log.info() 这种打印日志代码时,需要执行日志打印代码,将日志输出完成后,才能往后执行业务逻辑,因此会导致业务处理上有一定延迟
  • 异步模式通过ArrayBlockingQueue队列,让业务线程往队列中存日志数据,然后专门独立跑一个线程用于执行日志写入逻辑,因此可以解放业务线程,不需要等待日志写入完成就可以执行后续业务逻辑,不过当多个线程写入数据时,是需要获取队列的锁的,因此对业务线程也有一定影响。
  • 无锁异步模式是通过使用Disruptor技术,一个无锁的线程间通信库,通过使用CAS技术来最大化的降低多线程写入时的同步延时。

我们这里要讨论的也就是无锁异步模式,其他模式感兴趣的可以在log4j2的官网进行了解。

无锁异步模式配置方式

这个配置起来还是比较简单,只需要两步:

  1. 首先是需要在java启动时增加jvm参数 -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  2. 然后是在pom中引入必要组件disruptor的依赖
    xml 复制代码
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>3.4.4</version>
    </dependency>

无锁异步模式源码解析

我们首先介绍下无锁异步模式的总体流程,让大家心里有个基本的了解,这样后续看源码时就能有一个大致的方向,然后再深入到各个流程中,查看源码到底是怎么实现对应功能的。

总体流程

无锁异步模式的总体流程可以分为三个部分

  1. 首先是业务代码执行log.info()逻辑时,会通过Disruptor组件将日志存入 ringbuffer中,然后快速返回并执行后续业务代码,从而减少了系统调用的延迟,提高了业务处理的吞吐量
  2. Disruptor组件通过CAS来进行多线程同步,确保多个业务线程可以尽快的将数据存入ringbuffer中,并且单独的日志处理线程可以拿到日志数据并进行处理
  3. 无锁异步模式单独启动了一个线程进行日志处理,他可以不断地获取到业务的日志并写入到磁盘中

下面是我整理的一个流程的类图,大家可以在debug时根据图中的类找到方法打个断点,这样就能在idea的线程栈中找到程序调用的方法栈,从而查看源码逻辑。

业务线程日志打印逻辑

我们先来看下业务线程执行log.info()这种日志打印代码时,背后发生了什么。首先,我们debug跟踪log.info的代码

跟踪之后,发现他进入了AsyncLogger类的publish方法,我们发现这个方法使用到了Disruptor组件,通过其tryPublish进行尝试发布日志,同时下面也有个handleRingBufferFull方法,这个看名字就知道是队列满了之后会进行的处理,有兴趣的话大家可以自行查看代码逻辑。

继续跟踪代码,可以发现他通过tryNext方法获取到了一个序列号,可以大概知道这个方法后就是多线程同步的逻辑,他是通过每次加1,使用cursor字段的compareAndSet方法,用cas的方式获取下一个值,多线程同时进入此段逻辑后,如果获取下个值失败就再继续循环获取下一个序列,直到超出最大数量,抛出InsufficientCapacityException异常,此时就是日志队列已经满了,会执行队列满的操作,即前面的handleRingBufferFull方法。

如果成功获取到了序列号,则代表业务线程已经提前占了队列的一个坑,可以往队列里面放日志了,存放日志的代码在translateAndPublish方法中,跟踪进去后的代码如下图,其中跟踪translateTo方法可以看到,他将日志的各个信息设置进了RingBufferLogEvent类型的event对象中,这个就是存放日志数据的类,而这个对象是前面通过this.get(sequence) 获取到的。

也就是说,ringBuffer这个日志队列是一个类似数组的数据结构,有最大的数量,并且每个位置存放着一个代表日志的RingBufferLogEvent类型的类,代码通过cas获取ringBuffer的序列号来提前预定一个位置,成功后会通过get方式获取这个位置的日志类,并且将要写入的日志数据设置进去,从而实现了存储日志的操作。

完成这些之后,代码就可以返回并执行后续业务逻辑了。不过如果仔细跟踪代码,其实业务线程在返回之前,还会调用一个this.waitStrategy.signalAllWhenBlocking()方法,用以唤醒处理日志的线程。

那么问题来了,上面都是我们直接从log.info()代码直接跟踪过来看到的,那么处理日志的线程是在什么地方启动的呢?又怎么知道他的处理逻辑代码在哪呢?

日志打印线程处理逻辑

我们前面说到,业务线程在代码返回前还会走到this.waitStrategy.signalAllWhenBlocking()方法,看名字可以知道他是唤醒线程让他进行处理日志的,因此日志打印线程必然有在监听,我们可以通过processorNotifyCondition这个条件,在当前类中搜索,查找监听的程序,并加入断点,这样日志线程执行等待操作时就会执行到断点,我们可以通过debug的线程栈查看断点之前执行的代码,也可以追踪日志打印线程。

我们通过线程栈往前看,可以发现BatchEventProcessor类的processEvents方法就是日志打印线程处理日志的逻辑,总体流程为while循环获取下个可用的序列号,如果有的话,就可以通过this.dataProvider.get(nextSequence)获取到日志数据,然后通过this.eventHandler.onEvent方法执行具体的日志写入逻辑。你可以查看dataProvider变量的值,会发现他就是ringbuffer,也就和我们之前看到的日志存储逻辑对上了。

跟踪this.eventHandler.onEvent方法,会发现他进入到下面截图的代码,通过获取激活的LoggerConfig类,然后让这个类具体执行写入日志的逻辑。这个类就是我们在log4j2配置文件里面配置的数据解析而来的,往下看的话会发现他会调用callAppenders方法,这个就是我们配置中的Appender的组件了,根据组件的不同,可以写日志到文件,也可以打印到控制台等。

现在日志处理线程的处理代码也大致清楚了,但是我们还不知道这个日志线程是什么时候创建出来的,怎么看他创建时的代码。

日志打印线程启动逻辑

我们从前面知道,BatchEventProcessor类里面有日志打印线程的总体处理逻辑,启动一个线程需要执行其run方法,那启动之前肯定要初始化这个类的,因此我们可以在BatchEventProcessor类的创建方法中打断点,然后重启项目。

等到执行到断点后查看线程栈,可以发现,BatchEventProcessor类的创建逻辑是从AsyncLoggerDisruptor类的start方法进来的。这个正是log4j2的无锁异步代码的核心组件,可以详细看下他的创建方法。

首先可以看到的是,他通过传入RingBufferLogEventHandler类,然后封装并创建了BatchEventProcessor类,也就是说之前的this.eventHandler.onEvent处理日志的具体逻辑,其类型就是RingBufferLogEventHandler类,也就是在这里传入的。

其次我们可以看到new Disruptor代码,他有几个比较关键的入参,ringBufferSize和waitStrategy等,如果你看过官方的文档,就会知道这两个重要参数的含义,ringBufferSize是用于控制ringbuffer这个队列的长度,waitStrategy用于管控日志处理线程的睡眠等待策略。

其中可以关注一下的是ringBufferSize这个值,他控制队列的长度,影响日志可以存放的个数。通过查看代码,可以知道他是通过AsyncLogger.RingBufferSize这个jvm参数控制的,同时也能看到他默认值的取值逻辑,如果程序是web服务,他会使用262144这个比较大的默认值,如果不是web服务,他会使用4096这个默认值。

配置易错点

在配置无锁异步模式时,很多人在配置log4j2配置时,容易将Loggers下的Root节点换为AsyncRoot节点,其实这是有问题的。AsyncRoot节点是异步模式使用的,如果在无锁异步模式中配置了这个节点,相当于是启动了两个异步模式。

写日志时,开始的流程和之前描述的无锁异步模式一样,但是到无锁异步模式的日志处理线程准备写日志时,因为是AsyncRoot节点,他写日志的方式也是通过单独启动一个线程进行处理,相当于是日志先从业务线程传给a日志处理线程,a线程再把日志传给b线程,b线程执行写入操作,虽然日志写入不会有问题,但是多了一个做无用功的线程,会导致性能有所下降。

我们修改配置后重新debug,在之前写入日志时获取LoggerConfig的地方打断点,可以发现,他变成了AsyncLoggerConfig类,这个和我们配置的AsyncRoot节点的操作对的上。

跟踪这个类的方法,会发现他并不是像之前那样调用Appender的方法进行日志写入,而是又通过this.delegate.tryEnqueue方法,执行了和之前类似的传递日志给异步线程的操作,因此可以知道,AsyncRoot节点本身就是会启动异步线程的配置,如果使用了无锁异步配置,就不要配置AsyncRoot节点了,避免性能浪费。

总结

本文通过带领大家一起通过跟踪代码的方式查看log4j2的源码,在这个过程中讲解了一些debug查看源码的方法,同时也和大家一起了解了log4j2的实现原理,大家了解原理后就可以更好的使用无锁异步模式,同时也能更好的进行参数的配置调优。

相关推荐
Elastic 中国社区官方博客11 分钟前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18824 分钟前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn1 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su1 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈2 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780512 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123452 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒3 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记3 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598783 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端