一、内容提要

自知是人外有人,天外有天,相信对于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模式下默认为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的异步日志打印优化总结如下:

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 转载请注明来源

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

  1. 记一次线上事故的JVM内存学习

    今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...

  2. 记一次真实的线上事故:一个update引发的惨案!

    目录 前言 项目背景介绍 要命的update 结语 前言   从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...

  3. 记一次 android 线上 oom 问题

    背景 公司的主打产品是一款跨平台的 App,我的部门负责为它提供底层的 sdk 用于数据传输,我负责的是 Adnroid 端的 sdk 开发. sdk 并不直接加载在 App 主进程,而是隔离在一个单 ...

  4. 记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理

    昨晚我正在床上睡得着着的,突然来了一条短信. 啥,线上MySQL死锁了,我赶紧登录线上系统,查看业务日志. 能清楚看到是这条insert语句发生了死锁. MySQL如果检测到两个事务发生了死锁,会回滚 ...

  5. 记一次线上bug排查-quartz线程调度相关

    记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...

  6. NOI Day2线上同步赛崩盘记

    Preface 蒟蒻愉快的NOI线上赛Day2之行,不过因为太菜就凉了 这次由于策略&&网络的问题,最后两题都没有交,结果就靠T1稳住拿了75分就回家了. 我真是太菜了. 屠龙勇士 首 ...

  7. 解Bug之路-记一次线上请求偶尔变慢的排查

    解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...

  8. 【算法随记】Canny边缘检测算法实现和优化分析。

    以前的博文大部分都写的非常详细,有很多分析过程,不过写起来确实很累人,一般一篇好的文章要整理个三四天,但是,时间越来越紧张,后续的一些算法可能就以随记的方式,把实现过程的一些比较容易出错和有价值的细节 ...

  9. 记go中一次http超时引发的事故

    记一次http超时引发的事故 前言 分析下具体的代码实现 服务设置超时 客户端设置超时 http.client context http.Transport 问题 总结 参考 记一次http超时引发的 ...

  10. 记一次线上频繁fullGc的排查解决过程

    发生背景 最近上线的一个项目几乎全是查询业务,并且都是大表的慢查询,sql优化是做了一轮又一轮,前几天用户反馈页面加载过慢还时不时的会timeout,但是我们把对应的sql都优化一遍过后,前台响应还是 ...

随机推荐

  1. Axure 快速回到原点

    如果点了[快速回到原点]不生效,可能是因为输入法导致.

  2. 使用 Kubeadm 部署 Kubernetes(K8S) 安装 -- 持久化存储(PV&PVC)

    使用 Kubeadm 部署 Kubernetes(K8S) 安装 -- Ingress-Ngnix 使用 Kubeadm 部署 Kubernetes(K8S) 安装 -- 持久化存储(NFS网络存储) ...

  3. 负载均衡 SLB 健康检查异常

    负载均衡 SLB 健康检查异常,接口地址不能访问 接口地址的访问首先需要健康检查状态为正常. 如果接口没有"首页",需要提供一个可访问的controller

  4. CO01/CO02生产订单组件库存地点替换

    一.生产订单组件库存地点替换 当生产订单维护组件点击保存时,根据对应的工厂和工作中心,到配置表中查询对应的库存地点,并将自动带出的库存地点替换 二.隐式增强 在函数CO_VB_ORDER_POST中添 ...

  5. OOALV 分割屏幕

    1功能说明 需要开发一个报表,分为上下两个部分,下边需要再分割为左右两个部分,点击上边部分的行,下边两个报表信息发生变化. 效果如下: 2代码实现 1.数据查询 分别查询MARA.MARC.MAKT三 ...

  6. MongoDB 和 MySQL 之间有何区别?

    MongoDB 和 MySQL 是两个可用于存储和管理数据的数据库管理系统.MySQL 是一个关系数据库系统,以结构化表格格式存储数据.相比之下,MongoDB 以更灵活的格式将数据存储为 JSON ...

  7. 体验有礼 | 1 分钟 Serverless 极速部署个人网盘,真网盘真好用!

    你想自己搭一个无敌好用的网盘吗? 想 接着看,还有奖品呢! -- 当前,网盘几乎已成为现代人的标配,而市面上的网盘功能.费用各异,让用户们陷入了对比价格和功能的迷阵中.别对比了,动手吧!作为对存储.流 ...

  8. vue tabBar导航栏设计实现4-再次抽取MainTabBar

    系列导航 一.vue tabBar导航栏设计实现1-初步设计 二.vue tabBar导航栏设计实现2-抽取tab-bar 三.vue tabBar导航栏设计实现3-进一步抽取tab-item 四.v ...

  9. STM32CubeMX教程18 DAC - DMA输出自定义波形

    1.准备材料 开发板(正点原子stm32f407探索者开发板V2.4) STM32CubeMX软件(Version 6.10.0) 野火DAP仿真器 keil µVision5 IDE(MDK-Arm ...

  10. P2196-DP【黄】

    清醒了一点后我又写了一道黄色DP题,做出来了,还行,开心不少了... 中途暴露出一些问题 1.深搜过程中既然用了二维数组,那么深搜时就应该用二维循环取最优解,而不是只从最后一行中进行一维循环取最优解. ...