给你一颗“定心丸”——记一次由线上事故引发的Log4j2日志异步打印优化分析

news2024/11/18 17:38:21

一、内容提要

自知是人外有人,天外有天,相信对于Log4j2的异步日志打印早有老师或者同学已是熟稔于心,优化配置更是信手拈来,为了防止我在这里啰里八嗦的班门弄斧,我先将谜底在此公布:_log4j2.asyncQueueFullPolicy=Discard & log4j2.discardThreshold=ERROR__,这两个Log4j2配置在强依赖的RPC服务方系统或中间件系统出现问题且业务流量巨大时,对系统的快速止血和影响范围的控制有奇效。_要问为什么如此配置,是否还有高手?请见下文分晓。

二、现场还原

在2023年12月15日这“普通”的一天午后14点25分,我们收到了来自UMP的报警,接口服务可用率直接从100%干到了0.72%(该服务会直接影响到订单接单的相关流程,对客户体验有着重大影响)。我们顺藤摸瓜,并配合其他的服务监控定位到是强依赖系统出现了故障,导致其服务高度不可用,因此赶紧联系相关系统同事进行问题排查和处理。

大概14:33左右,服务端系统同事反馈已经机器扩容完成,由以下截图可见,确实在14:32左右服务可用率开始有抬头趋势,但是至此问题却并未终结,在14:36服务可用率开始急转直下(错误日志内容除了服务响应超时,服务端线程池满等异常以外,同时还收到了无服务的异常以及其他不相关服务调用的超时异常)。服务端系统同事反馈新扩容机器仅有极少数流量流入,如此“意外之喜”是我们没想到的,其后续排查和问题定位的过程分析也是该文成文的主要原因。最终我们通过操作客户端机器重启,服务得以完全恢复。

图2-1 服务端系统问题初现

图2-2 客户端服务监控可用率再次骤降

图2-3 客户端服务监控TP指标

系统JDK和相关依赖版本信息:

1.JDK:1.8.0_191

2.JSF:1.7.4-HOTFIX-T8

3.SLF4J-API:1.7.25

4.LOG4J-SLF4J-IMPL:2.18.0-jdsec.rc2

5.LOG4J2:2.18.0-jdsec.rc2

6.DISRUPTOR:3.4.2

三、问题点

1._为何_服务可用率恢复到了一定程度后又掉下来了?

2._为何_一个服务方的服务出现问题后,其他服务方的服务却也受到了影响?

3.服务超时控制_为何_没有起作用?

4.服务端出现问题_为何_需要客户端执行重启操作?

四、排查过程

如果上面的问题解决不了,大概我晚上睡觉的时候一掀开被窝也全是_“为何”_了,带着这些问题我们便开始了如下排查过程。

第一查

排查客户端机器是否出现由于GC导致的STW(stop the world)

由以下截图可见Young GC次数确有所升高(猜测应该有较多大对象产生),Full GC并未触发;堆内存,非堆内存的使用率处于正常水平;CPU使用率有所飙高且线程数略有增加,但是尚处于正常可接受范围内,目前看来问题时间段内机器的JVM指标并不能提供太多的定位线索。

图4-1-1 客户端服务器Young GC和Full GC情况

图4-1-2 客户端服务器堆内存和非堆内存情况

图4-1-3 客户端服务器CPU使用率和线程数情况

第二查

排查客户端机器磁盘情况,看是否存在日志阻塞的情况

通过以下截图可见,磁盘使用率,磁盘繁忙,磁盘写速度等磁盘指标看起来也尚属正常,由此暂时推断该问题与日志的相关性并不大。

注:这对我们后来问题排查的方向造成了误导,将罪魁祸首给很快排除掉了,其实再仔细看一下磁盘使用率的截图,在相对较短的时间内磁盘使用率却有了明显的增长,这是出现了“精英怪”呀,不过即便如此,目前也仅仅是猜测,并无充足的证据来支持定罪,接下来的排查才是重量级

图4-2-1 客户端服务器磁盘使用率情况

图4-2-2 客户端服务器磁盘繁忙情况

图4-2-3 客户端服务器磁盘写速度情况

第三查

查看问题时间段内客户端机器的内存状况(jmaq -dump)

通过_JSF-CLI-WORKER_关键字我们可以定位到JSF客户端线程状态,通过以下截图可以看出,JSF客户端线程虽然在执行不同的服务处理流程,但是都在执行WARN/ERROR日志打印的时候卡在了enqueue环节。

图4-3-1 JSF客户端线程状态1

图4-3-2 JSF客户端线程状态2

下面让我们来分析一下源码,具体看一下enqueue到底干了什么?

// enqueue源码分析
// 代码位置org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor#enqueue 363
private void enqueue(final LogEvent logEvent, final AsyncLoggerConfig asyncLoggerConfig) {
        // synchronizeEnqueueWhenQueueFull() 是与另外一个要和大家分享的开关配置(AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull)有关,该配置用于指定当日志异步打印使用的RingBuffer满了以后,将多个日志线程的队列写入操作通过锁(queueFullEnqueueLock)调整为同步
        if (synchronizeEnqueueWhenQueueFull()) {
            // 同步锁:private final Object queueFullEnqueueLock = new Object();
            synchronized (queueFullEnqueueLock) {
                disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
            }
        } else {
            disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
        }
    }


由上述源码我们不妨大胆的假设一下:**JSF的客户端线程由于日志打印被阻塞而导致功能出现异常。**那么顺着这个思路,我们就顺藤摸瓜,看看RingBuffer里面到底存了啥。聚光灯呢?往这打!

当我们找到RingBuffer的时候,不禁倒吸一口凉气,这小小RingBuffer居然有1.61GB之巨,让我们更近一步到RingBuffer的entries里面看看,到底是什么样的压力让RingBufferの💩竟如此干燥!

图4-3-3 RingBuffer的内存情况

(RingBuffer的entries里面都是Log4jEventWrapper,该Wrapper用于存储一些RingBuffer事件在执行的时候所需的必要信息)

图4-3-4 RingBufferLog4jEventWrapper 1

图4-3-5 RingBufferLog4jEventWrapper 2

在遍历entries过程中,我们看到基本上日志级别都是WARN和ERROR,并且含有大量的堆栈信息。排查到这个地方,综合以前了解到的Log4j2异步日志打印的相关知识(具体内容我会在下文压测过程说明完成后附上),我们基本上可以确定是当服务端系统出现问题时,生成的大量堆栈信息需要通过WARN和ERROR日志打印出来,RingBuffer被填满以后,又触发了锁竞争,由于等待的线程较多,系统中多个业务线程由并行变成了串型,并由此带来了一系列的连锁反应。至此也引出了本文的两位主角_【log4j2.asyncQueueFullPolicy】&【log4j2.discardThreshold】_。

log4j2.asyncQueueFullPolicy用于指定当日志打印队列被打满的时候,如何处理尚无法正常打印的日志事件,如果不指定的话,默认(Default)是阻塞,建议使用丢弃(Discard)

log4j2.discardThreshold用于配合log4j2.asyncQueueFullPolicy使用,指定哪些日志级别及以下的日志事件可以执行丢弃操作,默认为INFO,建议使用ERROR

排查到这里我们已经定位到日志生产的量的确比较多。但是如果日志打印出现了瓶颈,日志生产的多是一方面,另一方面还要看日志消费的情况是怎样的,如果消费的性能足够高的话,那么即便是日志生产的多,也不会造成日志打印的瓶颈。

图4-3-6 Log4j2日志消费线程状态

比较遗憾的是,我虽然找到了日志消费和日志文件写入的相关线程,但是并不能够足以支持我对于问题时间段内日志的消费出现了瓶颈进行定论。因为我dump出的内存信息并未统计出线程在问题时间段内写文件操作的执行次数,想以此来和日常进行比较,看是否是有较大的增量。(抑或是有方法,但是我没找到?我使用的是IDEA Profiler,如果您有好兵器,请不吝赐教🙏。)

氛围都已经烘托到这个地步了,就此打住可就有些不解风情了,不妨我们用针对性的压测来验证一下我们的想法。

五、压测验证

压测脚本

(复现场景)

1.持续性大流量请求到服务端,直至将服务端压垮一段时间(该流程持续时间较长,以下截图仅从服务端机器开始出现问题前较近时间开始截取);

2.服务端启动扩容操作(一倍扩容);

3.服务端操作问题机器下线;

4.若客户端调用服务监控可用率等指标长时间未恢复,则执行客户端重启操作;

5.验证客户端调用服务端服务是否已经恢复,服务端扩容机器流量是否正常。

6.调整日志丢弃级别,重复上述流程进行进行相同流程重复压测(出现问题时客户端系统已经配置log4j2.asyncQueueFullPolicy=Discard,故此仅将日志丢弃级别作为压测变量)

注:上述3、4、5操作并不会持续太长时间,因为流量基本保持不变,如果持续压力会再次将服务端扩容机器打垮,本次压测仅用于验证在服务端扩容以后,客户端机器无法正常请求到服务端新扩容机器的问题,故仅验证客户端能够正常请求到新扩容机器且客户端调用服务短时间内可自行恢复,即视为达到压测目标。

第一压 INFO

// 日志配置
log4j2.discardThreshold=INFO

压测结果:日志丢弃级别使用INFO时, 服务端操作问题机器下线后,客户端调用服务监控可用率长时间无法恢复到100%,重启客户端机器后恢复;

图5-2-1 压测过程可用率情况

图5-2-2 压测过程可用率+TP99+调用次数综合情况

第二压 WARN

// 日志配置
log4j2.discardThreshold=WARN

压测结果:日志丢弃的级别调整到WARN后,客户端无需操作,大概3分钟左右服务可用率恢复到95%左右,大概8分钟后可用率恢复至100%;

图5-3-1 压测过程可用率情况

图5-3-2 压测过程可用率+TP99+调用次数综合情况

第三压 ERROR

// 日志配置
log4j2.discardThreshold=ERROR

压测结果:日志丢弃的级别调整到ERROR后,客户端无需操作,在2分钟左右服务可用率即可恢复至100%。

图5-4-1 压测过程可用率情况

图5-4-2 压测过程可用率+TP99+调用次数综合情况

第四压 FATAL

// 日志配置
log4j2.discardThreshold=FATAL

压测结果:日志丢弃的级别调整到了FATAL后,客户端无需操作,在2分钟左右服务可用率即可恢复至100%,效果与ERROR基本相同。

图5-5-1 压测过程可用率情况

图5-5-2 压测过程可用率+TP99+调用次数综合情况

异步原理

下面我们通过一副示意简图来简单介绍一下Log4j2使用Disruptor的RingBuffer进行异步日志打印的基本原理。

图5-6-1 Log4j2异步日志打印流程示意简图

六、还有高手?

配置信息

| 配置名称 | 默认值 | 描述 |
| RingBuffer槽位数量:log4j2.asyncLoggerConfigRingBufferSize(同步&异步混合使用) | 256 * 1024 (garbage-free模式下默认为41024) | RingBuffer槽位数量,最小值为128,在首次使用的时候进行分配,并固定大小 |
| 异步日志等待策略:log4j2.asyncLoggerConfigWaitStrategy(同步&异步混合使用) | Timeout (建议设置为Sleep) | Block:I/O线程使用锁和条件变量等待可用的日志事件,建议在CPU资源重要程度大于系统吞吐和延迟的情况下使用该种策略; Timeout:是Block策略的一个变体,可以确保如果线程没有被及时唤醒(awit())的话,线程也不会卡在那里,可以以一个较小的延迟(默认10ms)恢复; Sleep:是一种自旋策略,使用纳秒级别的自旋交叉执行Thread.yield()和LockSupport.parkNanos(),该种策略可以较好的平衡系统性能和CPU资源的使用率,并且对程序线程影响较小。 Yield:相较于Sleep省去了LockSupport.parkNanos(),而是不断执行Thread.yield()来让渡CPU使用权,但是会造成CPU一直处于较高负载,强烈不建议在生产环境使用该种策略 |
| RingBuffer槽位数量:log4j2.asyncLoggerRingBufferSize(纯异步) | 256 * 1024 (garbage-free模式下默认为4
1024) | RingBuffer槽位数量,最小值为128(2的指数),在首次使用的时候进行分配,并固定大小 |
| 异步日志等待策略:log4j2.asyncLoggerWaitStrategy(纯异步) | Timeout (建议设置为Sleep) | Block:I/O线程使用锁和条件变量等待可用的日志事件,建议在CPU资源重要程度大于系统吞吐和延迟的情况下使用该种策略; Timeout:是Block策略的一个变体,可以确保如果线程没有被及时唤醒(awit())的话,线程也不会卡在那里,可以以一个较小的延迟(默认10ms)恢复; Sleep:是一种自旋策略,使用纳秒级别的自旋交叉执行Thread.yield()和LockSupport.parkNanos(),该种策略可以较好的平衡系统性能和CPU资源的使用率,并且对程序线程影响较小。 Yield:相较于Sleep省去了LockSupport.parkNanos(),而是不断执行Thread.yield()来让渡CPU使用权,但是会造成CPU一直处于较高负载,强烈不建议在生产环境使用该种策略 |
| 异步队列满后执行策略:log4j2.asyncQueueFullPolicy | Default 强烈建议设置为Discard | 当Appender的日志消费速度跟不上日志的生产速度,且队列已满时,指定如何处理尚未正常打印的日志事件,默认为阻塞(Default),强烈建议配置为丢弃(Discard) |
| 日志丢弃阈值:log4j2.discardThreshold | INFO (强烈建议设置为ERROR) | 当Appender的消费速度跟不上日志记录的速度,且队列已满时,若log4j2.asyncQueueFullPolicy为Discard,该配置用于指定丢弃的日志事件级别(小于等于),默认为INFO级别(即将INFO,DEBUG,TRACE日志事件丢弃掉), 强烈建议设置为ERROR(FATAL虽然有些极端,但是也可以) |
| 异步日志超时时间:log4j2.asyncLoggerTimeout | 10 | 异步日志等待策略为Timeout时,指定超时时间(单位:ms) |
| 异步日志睡眠时间:log4j2.asyncLoggerSleepTimeNs | 100 | 异步日志等待策略为Sleep时,线程睡眠时间(单位:ns) |
| 异步日志重试次数:log4j2.asyncLoggerRetries | 200 | 异步日志等待策略为Sleep时,自旋重试次数 |
| 队列满时是否将对RingBuffer的访问转为同步:AsyncLogger.SynchronizeEnqueueWhenQueueFull | true | 当队列满时是否将对RingBuffer的访问转为同步,当Appender日志消费速度跟不上日志的生产速度,且队列已满时,通过限制对队列的访问,可以显著降低CPU资源的使用率,强烈建议使用该默认配置 |

配置方式

| 配置源 | 优先级(值越低优先级越高) | 描述 |
| Spring Boot Properties | -100 | Spring Boot日志配置,需要有【log4j-spring】模块支持 |
| System Properties | 0 | 在类路径下添加log4j2.system.properties_(推荐)_ |
| Environment Variables | 100 | 使用环境变量进行日志配置,注意该系列配置均以LOG4J_作为前缀 |
| log4j2.component.properties file | 200 | 在类路径下添加log4j2.component.properties(其实是【System Properties】的一种配置方式,但是具有较低的优先级,一般用作默认配置) |

以上内容均参考自Log4j2官方文档:Log4j – Configuring Log4j 2 (apache.org)

七、总结

经过上文分析,我们可以将Log4j2的异步日志打印优化总结如下:

1.在日志可以丢弃的情况下,推荐使用_log4j2.asyncQueueFullPolicy=Discard_ 和_log4j2.discardThreshold=ERROR_的组合配置;

2.不要在生产环境使用可以直接与中间件交互的的Appender,如KafkaAppender。此类Appender一般都会有ack机制,与直接打印到日志文件不同的地方便是需要进行网络交互,如果中间件性能出现问题或者网络出现抖动,那么同样也会造成日志阻塞(话分两头,如果必须要使用KafkaAppender,那么可以考虑将syncSend设置为【false】,该配置的作用是指定Appender是否需要等待Kafka Server的ack,但是需要注意的是该开关配置需要Log4j2版本为2.8+);

3.Appender的【immediateFlush】设置为【false】,使用批量日志写入的方式,避免频繁的执行写磁盘操作。

上述三个配置可以保证在极端情况下日志的打印不会成为系统瓶颈,喂系统服下一颗“定心丸”。

作者:京东物流 曹成印

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1417645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ArcGIS微课1000例】0098:查询河流流经过的格网

本实验讲述,ArcGIS中查询河流流经过的格网,如黄河流经过的格网、县城、乡镇、省份等。 文章目录 一、加载数据二、空间查询三、结果导出四、注意事项一、加载数据 加载实验配套数据0098.rar中的河流(黄河)和格网数据,如下图所示: 接下来,将查询河流流经过的格网有哪些并…

Python进阶(1) | 使用VScode写单元测试

Python进阶(1) | 单元测试 2024.01.28 VSCode: 1.85.1 Linux(ubuntu 22.04) 文章目录 Python进阶(1) | 单元测试1. 目的2. Python Profile3. 单元测试框架3.1 什么是单元测试3.2 选一个单元测试框架3.3 编写 Python 单元测试代码3.4 在 VSCode 里发现单元测试3.5 再写一个单元…

Linux的优先级说明

一、背景 在工作中,不少同学对nice,priority,schedue策略,实时优先级,普通进程优先级的概念混淆,导致最后的代码可能引入bug,本文将统一进行说明,部分内容参考网络大佬的文章 &…

美化背景(拼图小游戏)

package Puzzlegame.com.wxj.ui;import javax.swing.*; import javax.swing.border.BevelBorder; import java.util.Random;public class GameJframe extends JFrame { //游戏主界面 //创建一个二维数组//目的:管理数据//加载图片的时候,会根据二维数组中…

STM32读取MPU6050数据并通过角度值控制舵机运动(STM32、GY-521 MPU6050、SG90舵机、MG946舵机)

通过STM32F103C8T6读取MPU6050数据控制舵机运动(STM32、GY-521 MPU6050、SG90舵机、MG946舵机) 最终现象一、MPU6050数据读取二、舵机控制原理①什么是PWM?②STM32F103C8T6如何生成PWM?③控制舵机需要什么样的PWM波? 三…

看图说话:Git图谱解读

很多新加入公司的同学在使用Git各类客户端管理代码的过程中对于Git图谱解读不太理解,我们常用的Git客户端是SourceTree,配合P4Merge进行冲突解决基本可以满足日常工作大部分需要。不同的Git客户端工具对图谱展示会有些许差异,以下是SourceTre…

jenkins对接K8S

创建连接K8S的凭据 查看需要使用到的命名空间 [rootk8s ~]# kubectl get ns |grep arts-system arts-system Active 16d创建service accounts [rootk8s ~]# kubectl create sa jenkins-k8s -n arts-system serviceaccount/jenkins-k8s created [rootk8s ~]# kubectl…

使用vscode查bug

具体操作 修改CMakeList.txt # set(CMAKE_BUILD_TYPE "Release")//注释Release模式 set(CMAKE_BUILD_TYPE "Debug")//设置为Debug模式 # set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g")//注释*这行代码是用来设置 CMake 构建系统中 Release 模式…

Go Zero微服务个人探究之路(十)实战走通微服务前台请求调用的一套流程model->rpc微服务->apiHTTP调用

前言 Go语言凭借低占用,高并发等优秀特性成为后台编程语言的新星,GoZero框架由七牛云技术副总裁团队编写,目前已经成为Go微服务框架里star数量最多的框架 本文记录讲述笔者一步步走通前台向后台发出请求,后台api调用rpc服务的相…

verilog编程之乘法器的实现

知识储备 首先来回顾一下乘法是如何在计算机中实现的。 假设现在有两个32位带符号定点整数x和y,我们现在要让x和y相乘,然后把乘积存放在z中,大家知道,两个32位数相乘,结果不会超过64位,因此z的长度应该为64…

总结6(循环(for))

循环 定义: 某些代码会被重复执行 分类: for 1.格式 for(1; 2; 3) 语句A; 2.执行的流程(1,2,A,3 2,A,3 2,A,3..........) 单个for循环的使用 多个for循环的嵌套使用 1). for(1; 2; 3) for&#xff0…

用通俗易懂的方式讲解:一种全新的大模型检索增强生成方法

如何使大型语言模型更加事实、正确和可靠? 检索增强生成(RAG)是一种有效的方法,可以缓解大型语言模型的基本局限性,如幻觉和缺乏最新知识。 然而,如果您曾尝试过RAG,您会同意我所说的RAG易于原…

【每日一题】4.LeetCode——环形链表

📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏小杨水平有限,欢迎各位大佬指点&…

RK3568平台开发系列讲解(Linux系统篇)互斥锁使用

🚀返回专栏总目录 文章目录 一、互斥锁API二、使用互斥锁的步骤三、互斥锁使用规则四、使用案例沉淀、分享、成长,让自己和他人都能有所收获!😄 一、互斥锁API 在Linux中,你可以使用互斥锁(Mutex)来实现多线程或多进程之间的互斥访问。互斥锁用于确保在同一时间只有一…

24. 两两交换链表中的节点(力扣LeetCode)

文章目录 24. 两两交换链表中的节点题目描述解题思路只使用一个临时节点使用两个临时节点 24. 两两交换链表中的节点 题目描述 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff0…

STM32学习笔记(二) —— 调试串口

我们在调试程序时,经常会使用串口打印相关的调试信息,但是单片机串口不能直接与 PC 端的 USB 接口通讯,需要用到一个USB转串口的芯片来充当翻译的角色。我们使用的开发板上有这个芯片,所以在打印调试信息的时候直接使用USB线连接开…

每日一题——LeetCode1365.有多少小于当前数字的数字

方法一 暴力循环 对于数组里的没一个元素都遍历一遍看有多少元素小于当前元素 var smallerNumbersThanCurrent function(nums) {let n nums.length;let ret [];for (let i 0; i < n; i) {let count 0;for (let j 0; j < n; j) {if (nums[j] < nums[i]) {count…

【C语言】数组的应用:三子棋游戏

由于代码较长&#xff0c;为了增加可读性&#xff0c;我们把代码分别写到game.h&#xff0c;game.c&#xff0c;test.c&#xff0c;里面&#xff0c;其中game.h用来声明函数&#xff0c;实现函数功能的代码在game.c&#xff0c;测试游戏的代码在test.c 为了方便后续的更改&…

qt学习:http+访问百度智能云api实现车牌识别

目录 登录到百度智能云&#xff0c;找到文字识别 完成操作指引 开通 查看车牌识别的api文档 ​编辑​编辑 查看自己应用的api key 查看回应的数据格式 编程步骤 ui界面编辑 添加模块&#xff0c;头文件和定义变量 新建两个类&#xff0c;一个图像Image类&#xff0c…

深度学习之卷积神经网络进阶版

上一讲我们介绍了卷积神经网络和多层感知机&#xff0c;也就是全链接网络。他们在网络架构上是串行的结构&#xff0c;也就是在每一层与每一层之间&#xff0c;前面一层的输出&#xff0c;是后面一层的输入。 在神经网络里面&#xff0c;我们可能会有更加复杂的结构&#xff0…