概叙
实战:Java web应用性能分析之【Arthas性能分析trace监控后端性能】-CSDN博客
实战:Java web应用性能分析之【异步日志:性能优化的金钥匙】-CSDN博客
实战:Java web应用性能分析之【springboot日志log4j2.xml中获取配置文件application.yaml中的配置】-CSDN博客
科普文:Java web应用性能分析之【UDP+异步日志log4j2,性能马上飞】-CSDN博客
科普文:Java基础系列之【梳理 Java 日志框架:Log4j、Log4j2、Logback、java.util.logging (JUL) 等】_log4j 版本-CSDN博客
通过 Log4j2异步日志 + UDP传输 + Logstash聚合 的三层优化,可实现百万级TPS的日志处理能力,同时将主线程日志记录耗时控制在微秒级。
Log4j2的异步日志机制,如AsyncAppender或AsyncLogger,能够减少日志记录对主线程的影响,提升应用性能。而UDP协议相比TCP,传输速度快,但不可靠,不过对于日志这种允许部分丢失的场景,UDP是合适的选择。Logstash作为日志收集工具,需要配置对应的输入插件来接收UDP传输的日志。
- Log4j2 异步日志:通过 AsyncAppender 或 AsyncLogger 实现非阻塞日志记录。
- UDP 传输:牺牲部分可靠性换取更高吞吐量,适用于日志容忍少量丢失的场景。
- Logstash 聚合:接收 UDP 日志并过滤、格式化后转发至存储系统。
备注:需根据业务容忍度平衡可靠性与性能,建议关键业务补充本地日志落盘机制。
LOG4J2
- 无锁队列设计:基于 LMAX Disruptor 技术实现异步缓冲,多线程同步日志写入时性能稳定,吞吐量显著高于 Logback 和 Log4j。
- 零 GC 优化:通过对象复用减少内存分配,降低 GC 频率,提升实时响应能力。
- 高性能异步队列:支持动态扩容的无锁环形队列,多线程下吞吐量可达 180w/s(64 线程测试数据),性能提升近 10 倍。
- 灵活等待策略:提供 Yield、Block 等策略平衡吞吐与延迟,避免线程饥饿。
Disruptor原理
Log4j2使用Disruptor来实现异步日志处理。
Disruptor是一个高性能的并发框架,基于事件的编程模型,通过环形缓冲区实现线程间的通信。
当应用程序发出日志记录请求时,Log4j2会捕获这个事件,并将其封装为一个日志事件对象。然后,这个对象会被投递到Disruptor的环形缓冲区中。在缓冲区中,事件会被多个消费者线程异步地取出并处理,处理过程包括将事件转换为具体的日志格式、输出到指定的目标等。
Disruptor使用RingBuffer来作为队列的数据结构,RingBuffer就是一个可自定义大小的环形数组
。除数组外还有一个序列号(sequence),用以指向下一个可用的元素
,供生产者与消费者使用。
Disruptor原理图如上图所示:
- Disruptor要求设置数组长度为2的n次幂。在知道索引(index)下标的情况下,存与取数组上的元素时间复杂度只有O(1),而这个index我们可以通过序列号与数组的长度取模来计算得出,index=sequence % entries.length。也可以用位运算来计算效率更高,此时array.length必须是2的幂次方,index=sequece&(entries.length-1)
- 当所有位置都放满了,再放下一个时,就会把0号位置覆盖掉
Disruptor流程
- 日志事件捕获:当应用程序调用日志记录方法时,Log4j2会捕获这个事件,并将其封装为一个日志事件对象。
- 事件投递:这个对象被投递到Disruptor的环形缓冲区中。
- 异步处理:多个消费者线程从环形缓冲区中取出事件,进行具体的日志格式转换和输出操作。
- 日志输出:最终将日志输出到指定的目标,如文件、控制台等。
Disruptor优缺点
优点:
- 高性能:Disruptor通过无锁设计和环形缓冲区,显著提高了日志处理的吞吐量和降低了延迟。
- 线程安全:环形缓冲区是线程安全的,多个消费者线程可以同时从缓冲区中取出事件进行处理,不会发生竞态条件。
- 配置灵活:Disruptor提供了丰富的配置选项,如队列长度、等待策略等,以满足不同场景下的性能需求。
缺点:
- 复杂性:Disruptor的使用增加了系统的复杂性,需要更多的配置和维护工作。
- 资源消耗:虽然提高了性能,但在某些情况下可能会增加系统的内存和CPU消耗。
Disruptor注意事项
- 资源管理:在使用Disruptor时,需要注意合理配置环形缓冲区的长度和等待策略,以避免资源浪费或性能瓶颈。
- 错误处理:确保在异步处理过程中对错误进行妥善处理,避免日志丢失或处理不一致的问题。
- 调试难度:由于异步处理的存在,调试和排查问题可能会更加复杂,需要更多的日志和监控手段。
spring boot 高性能日志:UDP+异步日志log4j2+logstash详细配置
1. 依赖配置(pom.xml)
排除默认的 Logback 依赖,同时添加log4j2的依赖、nettty、日志json格式化等依赖包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion><!-- 排除默认的 Logback 依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<exclusions>
<exclusion><!-- 排除默认的 Logback 依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j2 Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<!-- <version>3.4.4</version> <!– 最低 3.4.2 –>-->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- 可选:JSON 日志格式化支持 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<!-- UDP 传输依赖 log4j-core-->
<!-- UDP传输支持 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<!-- <version>4.1.68.Final</version>-->
</dependency>
2. log4j2.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" shutdownHook="disable">
<Properties>
<!-- 方式一:直接引用系统属性 -->
<Property name="APP_IP">${sys:app.ip}</Property>
<Property name="APP_PORT">${sys:app.port}</Property>
<Property name="APP_NAME">${sys:app.name}</Property>
<!-- 定义日志存储路径 -->
<Property name="LOG_DIR">./logs/Ecommerce</Property>
<!-- 方式一:直接引用系统属性 -->
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{traceId}][${APP_NAME}-${APP_IP}-${APP_PORT}][%thread] %-5level %logger{36}-[%method,%line]- %msg%n</Property>
<!-- 增强的JSON字段模板 -->
<Property name="JSON_TEMPLATE">
{
"app": "${APP_NAME}",
"host": "${APP_IP}",
"port": "${APP_PORT}",
"timestamp": "%d{yyyy-MM-dd HH:mm:ss.SSS}",
"thread": "%thread",
"level": "%level",
"logger": "%logger{36}",
"method": "%method",
"line": "%line",
"message": "%message",
"traceId": "%X{traceId}"
}
</Property>
</Properties>
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
<!-- <PatternLayout pattern="${LOG_PATTERN}" includeLocation="true"/>
ERROR PatternLayout contains an invalid element or attribute "includeLocation"-->
<!-- <JsonTemplateLayout-->
<!-- eventTemplateUri="classpath:LogEvent.json"-->
<!-- properties="true"-->
<!-- stacktraceAsString="true"/>-->
</Console>
<!-- UDP 核心 Appender -->
<!-- <Socket name="UDP_LOG" protocol="UDP" host="${env:LOGSTASH_HOST:-localhost}"-->
<!-- port="5044" immediateFlush="false" connectTimeoutMillis="5000">-->
<!-- <JsonLayout compact="true" eventEol="true" properties="true">-->
<!-- <KeyValuePair key="app" value="${spring:spring.application.name}"/>-->
<!-- <KeyValuePair key="host" value="${spring:server.address}"/>-->
<!-- <KeyValuePair key="port" value="${spring:server.port}"/>-->
<!-- <KeyValuePair key="profile" value="${env:SPRING_PROFILES_ACTIVE:-default}"/>-->
<!-- </JsonLayout>-->
<!-- </Socket>-->
<Socket name="UDP_LOG" protocol="UDP" host="127.0.0.1"
port="5568" immediateFlush="false" connectTimeoutMillis="5000">
<!-- <JsonLayout compact="true" eventEol="true" properties="true">-->
<!-- <KeyValuePair key="app" value="${spring:spring.application.name}"/>-->
<!-- <KeyValuePair key="host" value="${spring:server.address}"/>-->
<!-- <KeyValuePair key="port" value="${spring:server.port}"/>-->
<!-- <KeyValuePair key="profile" value="${env:SPRING_PROFILES_ACTIVE:-default}"/>-->
<!-- </JsonLayout>-->
<JsonLayout compact="true" eventEol="true" properties="true">
<KeyValuePair key="app" value="${APP_NAME}"/>
<KeyValuePair key="host" value="${APP_IP}"/>
<KeyValuePair key="port" value="${APP_PORT}"/>
</JsonLayout>
</Socket>
<!-- 分级异步队列 -->
<Async name="ASYNC_ERROR" bufferSize="2048" blocking="false">
<AppenderRef ref="UDP_LOG"/>
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
</Async>
<Async name="ASYNC_DEBUG" bufferSize="2048" blocking="false">
<AppenderRef ref="UDP_LOG"/>
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
</Async>
<Async name="ASYNC_INFO" bufferSize="8192" blocking="false">
<AppenderRef ref="UDP_LOG"/>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Async>
<!-- <!– 本地 Fallback Appender –>-->
<!-- <RollingFile name="LOCAL_FALLBACK" fileName="logs/app.log"-->
<!-- filePattern="logs/app-%d{yyyy-MM-dd}-%i.log">-->
<!-- <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>-->
<!-- <Policies>-->
<!-- <TimeBasedTriggeringPolicy interval="1" modulate="true"/>-->
<!-- <SizeBasedTriggeringPolicy size="100 MB"/>-->
<!-- </Policies>-->
<!-- </RollingFile>-->
</Appenders>
<Loggers>
<!-- 框架日志处理 -->
<Logger name="com.zaxxer.hikari.HikariDataSource" level="DEBUG" additivity="false">
<AppenderRef ref="ASYNC_DEBUG"/>
</Logger>
<Logger name="com.zaxxer.hikari.HikariDataSource" level="DEBUG" additivity="false">
<AppenderRef ref="ASYNC_DEBUG"/>
</Logger>
<!-- 屏蔽无关日志 -->
<Logger name="org.apache.catalina" level="ERROR"/>
<Logger name="org.springframework" level="WARN"/>
<!-- 自定义业务包日志级别 -->
<Logger name="com.zxx.study" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="ASYNC_DEBUG"/>
</Logger>
<!-- 主日志配置 -->
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="ASYNC_ERROR"/>
<AppenderRef ref="ASYNC_INFO"/>
<AppenderRef ref="ASYNC_DEBUG"/>
<!-- <AppenderRef ref="LOCAL_FALLBACK" level="WARN"/>-->
</Root>
</Loggers>
</Configuration>
关键参数:
bufferSize
:建议设置为 2^18(262144)以平衡内存和吞吐。protocol="UDP"
:指定传输协议为 UDP。JsonLayout
:使用结构化日志格式便于 Logstash 解析。
3. Logstash 接收端配置
input {
udp {
port => 5140
codec => json
buffer_size => 65535
}
}
filter {
mutate {
add_field => {
"[@metadata][index_suffix]" => "%{app}-%{+YYYY.MM.dd}"
}
}
}
output {
# 输出到Elasticsearch(按应用名分索引)
elasticsearch {
hosts => ["http://localhost:9200"]
index => "logs-%{[@metadata][index_suffix]}"
}
# 备份原始数据到文件
file {
path => "/var/log/logstash/%{app}/%{level}/%{+YYYY-MM-dd}.log"
codec => line { format => "%{message}" }
}
}
- 优化点:
workers
:与 CPU 核数对齐提升并发处理能力。buffer_size
:适当增大减少 UDP 丢包率。
4. 模拟Logstash 接收UdpLogServer
public class UdpLogServer1 {
private static final Map<String, Logger> loggers = new ConcurrentHashMap<>();
private static final ObjectMapper mapper = new ObjectMapper();
private static final AtomicLong errorCount = new AtomicLong();
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_RCVBUF, 1024 * 1024)
.handler(new SimpleChannelInboundHandler<DatagramPacket>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
String json = packet.content().toString(StandardCharsets.UTF_8);
processLog(json);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println("UDP处理异常: " + cause.getMessage());
ctx.close();
}
});
Channel channel = bootstrap.bind(5568).sync().channel();
System.out.println("UDP日志服务已启动,监听端口:5568");
channel.closeFuture().await();
} finally {
group.shutdownGracefully();
}
}
private static void processLog(String json) {
try {
JsonNode log = mapper.readTree(json);
if (!validateLogStructure(log)) {
handleInvalidLog();
return;
}
String app = log.get("app").asText();
String level = log.get("level").asText().toLowerCase();
String host = log.get("host").asText();
String port = log.get("port").asText();
String message = log.get("message").asText();
// todo UdpLogServer1和UdpLogServer 只能打印message,其他信息不能打印。 有问题,待解决。
Logger logger = getOrCreateLogger(app, level, host, port);
// 打印日志文件
logMessage(logger, level, message);
} catch (Exception e) {
System.err.println("日志处理失败: " + e.getMessage());
}
}
private static boolean validateLogStructure(JsonNode log) {
return log.has("app") && log.has("level") && log.has("message")
&& log.has("host") && log.has("port");
}
private static void handleInvalidLog() {
long count = errorCount.incrementAndGet();
if (count % 100 == 0) {
System.err.println("非法日志累计警告: " + count + "条");
}
}
private static Logger getOrCreateLogger(String app, String level, String host, String port) {
String loggerKey = String.format("%s|%s|%s|%s", app, level, host, port);
String loggerName = String.format("logs/%s/%s_%s_%s_%s_current.log", app, app, level, host, port);
String historyloggerName = String.format("logs/%s/%s_%s_%s_%s_%%d{yyyy-MM-dd}-%%i.log", app, app, level, host, port);
return loggers.computeIfAbsent(loggerKey, key -> {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
synchronized (config) {
try {
// 0. 避免重复创建
if (config.getLoggerConfig(loggerKey) != null) {
return LogManager.getLogger(loggerKey);
}
// 1. 创建 RollingFileAppender
RollingFileAppender appender = RollingFileAppender.newBuilder()
.setName("DynamicAppender-" + loggerKey)
//.withFileName(String.format("logs/%s/%s/current.log", app, level))
//.withFilePattern(String.format("logs/%s/%s/%%d{yyyy-MM-dd}-%%i.log", app, level))
.withFileName(loggerName)
.withFilePattern(historyloggerName)
.withPolicy(CompositeTriggeringPolicy.createPolicy(
TimeBasedTriggeringPolicy.newBuilder().withInterval(1).build(),
SizeBasedTriggeringPolicy.createPolicy("50 MB")
))
.setLayout(PatternLayout.newBuilder().withPattern("%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{traceId}][%thread] %-5level %logger{36}-[%method,%line]- %m%n").build())
.withStrategy(DefaultRolloverStrategy.newBuilder().withMax("30").build())
//.setLayout(PatternLayout.newBuilder().withPattern("%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{traceId}][${APP_NAME}-${APP_IP}-${APP_PORT}][%thread] %-5level %logger{36}-[%method,%line]- %msg%n").build())
.build();
// 2. 注册 Appender
appender.start();
config.addAppender(appender);
// 3. 创建基础 LoggerConfig
LoggerConfig loggerConfig = new LoggerConfig(
loggerKey, // loggerName
Level.toLevel(level), // level
false // additivity
);
// 4. 手动添加 Appender(关键修改点)
loggerConfig.addAppender(appender, Level.toLevel(level), null);
// 5. 更新配置
config.addLogger(loggerKey, loggerConfig);
ctx.updateLoggers(config);
return LogManager.getLogger(loggerKey);
} catch (Exception e) {
// 异常回退逻辑
return LogManager.getLogger("FallbackLogger");
}
}
});
}
private static void logMessage(Logger logger, String level, String message) {
switch (level.toLowerCase()) {
case "debug":
logger.debug(message);
break;
case "warn":
logger.warn(message);
break;
case "error":
logger.error(message);
break;
default:
logger.info(message);
}
}
// "instant":{"epochSecond":1745295170,"nanoOfSecond":153416400}
private static String getLogTime(JsonNode instantJson) {
// 提取字段(假设已通过 JSON 解析库获取)
long epochSecond = 1745295170L;
int nanoOfSecond = 153416400;
// 构造 Instant 对象
Instant instant = Instant.ofEpochSecond(instantJson.get("epochSecond").asLong(),
instantJson.get("nanoOfSecond").asInt());
// 定义日期格式(含时区)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.of("Asia/Shanghai")); // 例如:上海时区
// 格式化输出
String formattedTime = formatter.format(instant);
ZhouxxTool.printTimeAndThread("日志时间:" + formattedTime); // 输出:2025-04-20 22:12:50.153
return formattedTime;
}
}
5.看效果
客户端日志
接收端日志
基本符合要求,生成的日志能够区分服务多点集群部署时的日志区分