实战:Java web应用性能分析之【spring boot 高性能日志:UDP+异步日志log4j2+logstash】

概叙

实战: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流程

  1. 日志事件捕获‌:当应用程序调用日志记录方法时,Log4j2会捕获这个事件,并将其封装为一个日志事件对象。
  2. 事件投递‌:这个对象被投递到Disruptor的环形缓冲区中。
  3. 异步处理‌:多个消费者线程从环形缓冲区中取出事件,进行具体的日志格式转换和输出操作。
  4. 日志输出‌:最终将日志输出到指定的目标,如文件、控制台等‌。

Disruptor优缺点

优点‌:

  • 高性能‌:Disruptor通过无锁设计和环形缓冲区,显著提高了日志处理的吞吐量和降低了延迟。
  • 线程安全‌:环形缓冲区是线程安全的,多个消费者线程可以同时从缓冲区中取出事件进行处理,不会发生竞态条件‌。
  • 配置灵活‌:Disruptor提供了丰富的配置选项,如队列长度、等待策略等,以满足不同场景下的性能需求‌。

缺点‌:

  • 复杂性‌:Disruptor的使用增加了系统的复杂性,需要更多的配置和维护工作。
  • 资源消耗‌:虽然提高了性能,但在某些情况下可能会增加系统的内存和CPU消耗‌。

Disruptor注意事项

  1. 资源管理‌:在使用Disruptor时,需要注意合理配置环形缓冲区的长度和等待策略,以避免资源浪费或性能瓶颈。
  2. 错误处理‌:确保在异步处理过程中对错误进行妥善处理,避免日志丢失或处理不一致的问题。
  3. 调试难度‌:由于异步处理的存在,调试和排查问题可能会更加复杂,需要更多的日志和监控手段。

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> &lt;!&ndash; 最低 3.4.2 &ndash;&gt;-->
        </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>

<!--        &lt;!&ndash; 本地 Fallback Appender &ndash;&gt;-->
<!--        <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.看效果

客户端日志

接收端日志

基本符合要求,生成的日志能够区分服务多点集群部署时的日志区分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

01Byte空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值