如何监控 Log4j2 异步日志遇到写入瓶颈
如何监控 Log4j2 异步日志遇到写入瓶颈
在之前的一篇文章中(一次鞭辟入里的 Log4j2 异步日志输出阻塞问题的定位),我们详细分析了一个经典的 Log4j2 异步日志阻塞问题的定位,主要原因还是日志文件写入慢了。并且比较深入的分析了 Log4j2 异步日志的原理,最后给出了一些解决方案。
新的问题 - 如何更好的应对这种情况?
之前提出的解决方案仅仅是针对之前定位的问题的优化,但是随着业务发展,日志量肯定会更多,大量的日志可能导致写入日志成为新的性能瓶颈。对于这种情况,我们需要监控。
首先想到的是进程外部采集系统指标监控:现在服务都提倡上云,并实现云原生服务。对于云服务,存储日志很可能使用 NFS(Network File System),例如 AWS 的 EFS。这种 NFS 一动都可以动态的控制 IO 最大承载,但是服务的增长是很难预估完美的,并且高并发业务流量基本都是一瞬间到达,仅通过 IO 定时采集很难评估到真正的流量尖峰(例如 IO 定时采集是 5s 一次,但是在某一秒内突然到达很多流量,导致进程内大多线程阻塞,这之后采集 IO 看到 IO 压力貌似不大的样子)。并且,由于线程的阻塞,导致可能我们看到的 CPU 占用貌似也不高的样子。所以,外部定时采集指标,很难真正定位到日志流量问题。
然后我们考虑进程自己监控,暴露接口给外部监控定时检查,例如 K8s 的 pod 健康检查等等。在进程的日志写入压力过大的时候,新扩容一个实例;启动完成后,在注册中心将这个日志压力大的进程的状态设置为暂时下线(例如 Eureka 置为 OUT_OF_SERVICE,Nacos 置为 PAUSED),让流量转发到其他实例。待日志压力小之后,再修改状态为 UP,继续服务。
那么如何实现这种监控呢?
监控 Log4j2 异步日志的核心 - 监控 RingBuffer
根据之前我们分析 Log4j2 异步日志的原理,我们知道其核心是 RingBuffer 这个数据结构作为缓存。我们可以监控其剩余大小的变化来判断当前日志压力。那么怎么能拿到呢?
Log4j2 异步日志与 RingBuffer 的关系
Log4j2 对于每一个 AsyncLogger 配置,都会创建一个独立的 RingBuffer,例如下面的 Log4j2 配置:
<!--省略了除了 loggers 以外的其他配置-->
<loggers>
<!--default logger -->
<Asyncroot level="info" includeLocation="true">
<appender-ref ref="console"/>
</Asyncroot>
<AsyncLogger name="RocketmqClient" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
<AsyncLogger name="com.alibaba.druid.pool.DruidDataSourceStatLoggerImpl" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
<AsyncLogger name="org.mybatis" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
</loggers>
这个配置包含 4 个 AsyncLogger,对于每个 AsyncLogger 都会创建一个 RingBuffer。Log4j2 也考虑到了监控 AsyncLogger 这种情况,所以将 AsyncLogger 的监控暴露成为一个 MBean(JMX Managed Bean)。
相关源码如下:
private static void registerLoggerConfigs(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
//获取 log4j2.xml 配置中的 loggers 标签下的所有配置值
final Map<String, LoggerConfig> map = ctx.getConfiguration().getLoggers();
//遍历每个 key,其实就是 logger 的 name
for (final String name : map.keySet()) {
final LoggerConfig cfg = map.get(name);
final LoggerConfigAdmin mbean = new LoggerConfigAdmin(ctx, cfg);
//对于每个 logger 注册一个 LoggerConfigAdmin
register(mbs, mbean, mbean.getObjectName());
//如果是异步日志配置,则注册一个 RingBufferAdmin
if (cfg instanceof AsyncLoggerConfig) {
final AsyncLoggerConfig async = (AsyncLoggerConfig) cfg;
final RingBufferAdmin rbmbean = async.createRingBufferAdmin(ctx.getName());
register(mbs, rbmbean, rbmbean.getObjectName());
}
}
}
创建的 MBean 的类源码:RingBufferAdmin.java
public class RingBufferAdmin implements RingBufferAdminMBean {
private final RingBuffer<?> ringBuffer;
private final ObjectName objectName;
//... 省略其他我们不关心的代码
public static final String DOMAIN = "org.apache.logging.log4j2";
String PATTERN_ASYNC_LOGGER_CONFIG = DOMAIN + ":type=%s,component=Loggers,name=%s,subtype=RingBuffer";
//创建 RingBufferAdmin,名称格式符合 Mbean 的名称格式
public static RingBufferAdmin forAsyncLoggerConfig(final RingBuffer<?> ringBuffer,
final String contextName, final String configName) {
final String ctxName = Server.escape(contextName);
//对于 RootLogger,这里 cfgName 为空字符串
final String cfgName = Server.escape(configName);
final String name = String.format(PATTERN_ASYNC_LOGGER_CONFIG, ctxName, cfgName);
return new RingBufferAdmin(ringBuffer, name);
}
//获取 RingBuffer 的大小
@Override
public long getBufferSize() {
return ringBuffer == null ? 0 : ringBuffer.getBufferSize();
}
//获取 RingBuffer 剩余的大小
@Override
public long getRemainingCapacity() {
return ringBuffer == null ? 0 : ringBuffer.remainingCapacity();
}
public ObjectName getObjectName() {
return objectName;
}
}
我们可以通过 JConsole 查看对应的 MBean:

其中 2f0e140b 为 LoggerContext 的 name。
Spring Boot + Prometheus 监控 Log4j2 RingBuffer 大小
我们的微服务项目中使用了 spring boot,并且集成了 prometheus。我们可以通过将 Log4j2 RingBuffer 大小作为指标暴露到 prometheus 中,通过如下代码:
import io.micrometer.core.instrument.Gauge;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.jmx.RingBufferAdminMBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import javax.annotation.PostConstruct;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
@Log4j2
@Configuration(proxyBeanMethods = false)
//需要在引入了 prometheus 并且 actuator 暴露了 prometheus 端口的情况下才加载
@ConditionalOnEnabledMetricsExport("prometheus")
public class Log4j2Configuration {
@Autowired
private ObjectProvider<PrometheusMeterRegistry> meterRegistry;
//只初始化一次
private volatile boolean isInitialized = false;
//需要在 ApplicationContext 刷新之后进行注册
//在加载 ApplicationContext 之前,日志配置就已经初始化好了
//但是 prometheus 的相关 Bean 加载比较复杂,并且随着版本更迭改动比较多,所以就直接偷懒,在整个 ApplicationContext 刷新之后再注册
// ApplicationContext 可能 refresh 多次,例如调用 /actuator/refresh,还有就是多 ApplicationContext 的场景
// 这里为了简单,通过一个简单的 isInitialized 判断是否是第一次初始化,保证只初始化一次
@EventListener(ContextRefreshedEvent.class)
public synchronized void init() {
if (!isInitialized) {
//通过 LogManager 获取 LoggerContext,从而获取配置
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
org.apache.logging.log4j.core.config.Configuration configuration = loggerContext.getConfiguration();
//获取 LoggerContext 的名称,因为 Mbean 的名称包含这个
String ctxName = loggerContext.getName();
configuration.getLoggers().keySet().forEach(k -> {
try {
//针对 RootLogger,它的 cfgName 是空字符串,为了显示好看,我们在 prometheus 中将它命名为 root
String cfgName = StringUtils.isBlank(k) ? "" : k;
String gaugeName = StringUtils.isBlank(k) ? "root" : k;
Gauge.builder(gaugeName + "_logger_ring_buffer_remaining_capacity", () ->
{
try {
return (Number) ManagementFactory.getPlatformMBeanServer()
.getAttribute(new ObjectName(
//按照 Log4j2 源码中的命名方式组装名称
String.format(RingBufferAdminMBean.PATTERN_ASYNC_LOGGER_CONFIG, ctxName, cfgName)
//获取剩余大小,注意这个是严格区分大小写的
), "RemainingCapacity");
} catch (Exception e) {
log.error("get {} ring buffer remaining size error", k, e);
}
return -1;
}).register(meterRegistry.getIfAvailable());
} catch (Exception e) {
log.error("Log4j2Configuration-init error: {}", e.getMessage(), e);
}
});
isInitialized = true;
}
}
}
增加这个代码之后,请求 /actuator/prometheus 之后,可以看到对应的返回:
//省略其他的
# HELP root_logger_ring_buffer_remaining_capacity
# TYPE root_logger_ring_buffer_remaining_capacity gauge
root_logger_ring_buffer_remaining_capacity 262144.0
# HELP org_mybatis_logger_ring_buffer_remaining_capacity
# TYPE org_mybatis_logger_ring_buffer_remaining_capacity gauge
org_mybatis_logger_ring_buffer_remaining_capacity 262144.0
# HELP com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity
# TYPE com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity gauge
com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity 262144.0
# HELP RocketmqClient_logger_ring_buffer_remaining_capacity
# TYPE RocketmqClient_logger_ring_buffer_remaining_capacity gauge
RocketmqClient_logger_ring_buffer_remaining_capacity 262144.0
这样,当这个值为 0 持续一段时间后(就代表 RingBuffer 满了,日志生成速度已经远大于消费写入 Appender 的速度了),我们就认为这个应用日志负载过高了。
如何监控 Log4j2 异步日志遇到写入瓶颈的更多相关文章
- 一次鞭辟入里的 Log4j2 异步日志输出阻塞问题的定位
一次鞭辟入里的 Log4j2 日志输出阻塞问题的定位 问题现象 线上某个应用的某个实例突然出现某些次请求服务响应极慢的情况,有几次请求超过 60s 才返回,并且通过日志发现,服务线程并没有做什么很重的 ...
- log4j2异步日志解读(二)AsyncLogger
前文已经讲了log4j2的AsyncAppender的实现[log4j2异步日志解读(一)AsyncAppender],今天我们看看AsyncLogger的实现. 看了这个图,应该很清楚AsyncLo ...
- log4j2异步日志配置及官方文档的问题澄清
配置及demo 方法一全部打开 加启动参数 -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextS ...
- log4j2异步日志解读(一)AsyncAppender
log4j.logback.log4j2 历史和关系,我们就在这里不展开讲了.直接上干货,log4j2突出于其他日志的优势,异步日志实现. 看一个东西,首先看官网文档 ,因为前面文章已经讲解了disr ...
- Log4j2中的同步日志与异步日志
1.背景 Log4j 2中记录日志的方式有同步日志和异步日志两种方式,其中异步日志又可分为使用AsyncAppender和使用AsyncLogger两种方式. 2.Log4j2中的同步日志 所谓同步日 ...
- 近期业务大量突增微服务性能优化总结-3.针对 x86 云环境改进异步日志等待策略
最近,业务增长的很迅猛,对于我们后台这块也是一个不小的挑战,这次遇到的核心业务接口的性能瓶颈,并不是单独的一个问题导致的,而是几个问题揉在一起:我们解决一个之后,发上线,之后发现还有另一个的性能瓶颈问 ...
- log4j 异步日志问题分析
1. 常用的DailyRollingFileAppender与RollingFileAppender是否同步? 1.1 代码分析 2. log4j 1.2.x提供了异步appender是什么?Asyn ...
- spring boot:配置druid数据库连接池(开启sql防火墙/使用log4j2做异步日志/spring boot 2.3.2)
一,druid数据库连接池的功能? 1,Druid是阿里巴巴开发的号称为监控而生的数据库连接池 它的优点包括: 可以监控数据库访问性能 SQL执行日志 SQL防火墙 2,druid的官方站: http ...
- spring boot:使用log4j2做异步日志打印(spring boot 2.3.1)
一,为什么要使用log4j2? log4j2是log4j的升级版, 升级后更有优势: 性能更强/吞吐量大/支持异步 功能扩展/支持插件/支持自定义级别等 这些优 ...
随机推荐
- 3D点云完美匹配
3D点云完美匹配 The Perfect Match: 3D Point Cloud Matching with Smoothed Densities 地址链接: http://openaccess. ...
- nvGRAPH API参考分析(二)
nvGRAPH API参考分析(二) nvGRAPH Code Examples 本文提供了简单的示例. 1. nvGRAPH convert topology example void check( ...
- jemeter压测, 高级应用: 发1万个请求,每个请求参数都不同, 使用CSV数据文件配置
今天接到一个压测任务, 数据源需要自己从测试环境库中取, 并且使用jemeter 请求, 每个请求参数都不相同 这里使用jemeter的 CSV数据文件来配置: 这样配置好后, 开始发送请求: csv ...
- 反汇编EXE添加一个启动时的消息框
反汇编EXE添加一个启动时的消息框 最近有一个要修改PE文件的需求,就先从EXE文件下手吧,我也是初学一个小时而已,不过之前接触过一点汇编罢了,这篇文章算是个DEMO,主要的思路是将其反汇编得到汇编代 ...
- 【模拟】10-15 题解 trans
Trans 题目描述 Tgopknight决定使用他的幸运数字2和3来进行这个游戏,他一开始有n个数字,记为{dn}需要 进行k次操作,每次操作找到最小的x使得dx = 2并且dx+1 = 3,此时如 ...
- c#json将字符串反序列化成对象时不新建类的做法
在服务端代码文件中加上struct结构体就能解决 struct LocationInfo { public string LocationID { get; set; } public string ...
- python之str 字符串
str,字符串,有序的,存储多个字符,不可修改,通过下标值访问快捷方式 1, 定义一个空字符串 2,定义一个非空字符串 3,索引,又名下标值 4,切片与延长 5,转义字符 6, python 属于面向 ...
- 使用pdb进行Python调试
调试应用有时是一个不受欢迎的工作,当你长期编码之后,只希望写的代码顺利运行.但是,很多情况下,我们需要学习一个新的语言功能或者实验检测新的方法,从而去理解其中运行的机制原理. 即使不考虑这样的场景,调 ...
- Zabbix5.0Yum安装
1.1.操作系统配置: 1.操作系统属性(虚拟机下环境): (1)操作系统版本: [root@localhost ~]# cat /etc/redhat-release CentOS Linux re ...
- 32、sed命令详解
32.1.sed介绍: 1.sed(sed软件常称做)是流编辑器,是操作.过滤.和转换文本内容的工具: 2.sed的模式空间和保持空间介绍: (1)模式空间:sed处理文本内容行的一个临时缓冲区,模式 ...