给你一颗“定心丸”——记一次由线上事故引发的Log4j2日志异步打印优化分析
一、内容提要
自知是人外有人,天外有天,相信对于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和相关依赖版本信息:
三、问题点
四、排查过程
如果上面的问题解决不了,大概我晚上睡觉的时候一掀开被窝也全是“为何”了,带着这些问题我们便开始了如下排查过程。
第一查
排查客户端机器是否出现由于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,如果您有好兵器,请不吝赐教。)
氛围都已经烘托到这个地步了,就此打住可就有些不解风情了,不妨我们用针对性的压测来验证一下我们的想法。
五、压测验证
压测脚本
(复现场景)
注:上述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模式下默认为4*1024) | 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的异步日志打印优化总结如下:
上述三个配置可以保证在极端情况下日志的打印不会成为系统瓶颈,喂系统服下一颗“定心丸”。
作者:京东物流 曹成印
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源
给你一颗“定心丸”——记一次由线上事故引发的Log4j2日志异步打印优化分析的更多相关文章
- 记一次线上事故的JVM内存学习
今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...
- 记一次真实的线上事故:一个update引发的惨案!
目录 前言 项目背景介绍 要命的update 结语 前言 从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...
- 记一次 android 线上 oom 问题
背景 公司的主打产品是一款跨平台的 App,我的部门负责为它提供底层的 sdk 用于数据传输,我负责的是 Adnroid 端的 sdk 开发. sdk 并不直接加载在 App 主进程,而是隔离在一个单 ...
- 记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理
昨晚我正在床上睡得着着的,突然来了一条短信. 啥,线上MySQL死锁了,我赶紧登录线上系统,查看业务日志. 能清楚看到是这条insert语句发生了死锁. MySQL如果检测到两个事务发生了死锁,会回滚 ...
- 记一次线上bug排查-quartz线程调度相关
记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...
- NOI Day2线上同步赛崩盘记
Preface 蒟蒻愉快的NOI线上赛Day2之行,不过因为太菜就凉了 这次由于策略&&网络的问题,最后两题都没有交,结果就靠T1稳住拿了75分就回家了. 我真是太菜了. 屠龙勇士 首 ...
- 解Bug之路-记一次线上请求偶尔变慢的排查
解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...
- 【算法随记】Canny边缘检测算法实现和优化分析。
以前的博文大部分都写的非常详细,有很多分析过程,不过写起来确实很累人,一般一篇好的文章要整理个三四天,但是,时间越来越紧张,后续的一些算法可能就以随记的方式,把实现过程的一些比较容易出错和有价值的细节 ...
- 记go中一次http超时引发的事故
记一次http超时引发的事故 前言 分析下具体的代码实现 服务设置超时 客户端设置超时 http.client context http.Transport 问题 总结 参考 记一次http超时引发的 ...
- 记一次线上频繁fullGc的排查解决过程
发生背景 最近上线的一个项目几乎全是查询业务,并且都是大表的慢查询,sql优化是做了一轮又一轮,前几天用户反馈页面加载过慢还时不时的会timeout,但是我们把对应的sql都优化一遍过后,前台响应还是 ...
随机推荐
- PS CJ37/CJ38 增加和返回预算
一.预算补充CJ37/预算返回CJ38 二.补充预算CJ37,点击保存 预算返回CJ38,点击保存 三.代码示例 预算补充代码 "------------------------------ ...
- VMware15.5安装Ubuntu20.04
一.安装前的准备 1.下载好Ubuntu20.04的镜像文件,直接从官网下载就好,激活密匙. 2.准备好VMware软件,这里就忽略安装过程了. 二.建立虚拟机以及开启正式的Ubuntu安装过程 参考 ...
- Codeforces Round #733 (Div. 1 + Div. 2)
比赛链接:Here 1530A. Binary Decimal 现在规定一种只由0和1组成的数字,我们称这种数字为二进制数字,例如10,1010111,给定一个数n,求该数字最少由多少个二进制数字组成 ...
- OpenSCA受邀出席2023 Open Compliance Summit
近日,由Linux基金会主办的2023 Open Compliance Summit(开放合规峰会,简称OCS)在日本东京隆重召开.悬镜安全旗下全球极客开源数字供应链安全社区OpenSCA受邀参与,O ...
- linux服务器之间免密登录
目标 192.168.0.10 免密登录 192.168.0.11.192.168.0.12两台服务器 1.登录192.168.0.10 生成ssh密钥 ssh-keygen -t r ...
- 《深入理解计算机系统》(CSAPP)读书笔记 —— 第七章 链接
链接( Clinking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行.链接可以执行于编译时( compile time),也就是在源代码被翻译成机器代 ...
- vue-awesome-swiper swiper/dist/css/swiper.css 报not found错误
解决办法删除package-lock.json文件写死package.json版本号 "vue-awesome-swiper": "^3.1.3", 删除no ...
- vue项目使用el-table实现无限滚动
https://blog.csdn.net/weixin_44994731/article/details/107980827 1.安装el-table-infinite-scroll yarn ad ...
- B2033 A*B 问题
A*B 问题 题目描述 输入两个正整数 \(A\) 和 \(B\),求 \(A \times B\) 的值.注意乘积的范围和数据类型的选择. 输入格式 一行,包含两个正整数 \(A\) 和 \(B\) ...
- [转帖](1.2)sql server for linux 开启代理服务(SQL AGENT),使用T-SQL新建作业
https://www.cnblogs.com/gered/p/12518090.html 回到顶部 [1]启用SQL Server代理 sudo /opt/mssql/bin/mssql-conf ...