JAVA 服务日志打印不全?Logback Async 异步队列容量与丢弃策略

JAVA 服务日志打印不全?Logback Async 异步队列容量与丢弃策略

各位同学,大家好。今天我们来聊聊 Java 服务日志打印不全的问题,以及如何利用 Logback 的 Async Appender 结合队列容量和丢弃策略来解决这个问题。日志是诊断线上问题的关键,如果日志打印不全,那无疑给排查问题带来了巨大的困难。

日志打印不全的常见原因

日志打印不全的原因有很多,但常见的包括:

  1. 同步阻塞: 日志打印是同步操作,如果日志量过大,或者日志写入磁盘的速度跟不上,会导致应用线程阻塞,进而影响应用的性能,甚至导致请求超时。在高并发场景下,这种阻塞会变得更加明显,导致部分日志未能及时写入。
  2. 缓冲区溢出: 有些日志框架或者配置不当,会导致缓冲区溢出,从而丢失部分日志。
  3. 异步丢失: 使用异步日志时,如果异步队列满了,且没有合适的丢弃策略,新的日志消息会被丢弃,导致日志不全。
  4. 异常中断: 在日志写入过程中,如果发生异常,比如磁盘空间不足,权限问题等,可能会导致日志写入中断,从而丢失部分日志。
  5. 配置错误: Logback 配置错误,例如配置了错误的日志级别,或者错误的 Appender,也可能导致日志打印不全。

Logback Async Appender 的作用

Logback Async Appender 的核心作用是将日志事件异步写入到目标 Appender。它通过一个内存队列来缓冲日志事件,然后由一个独立的线程从队列中取出事件并写入到目标 Appender。

优点:

  • 解耦: 应用线程不再直接负责日志写入,降低了应用线程的阻塞时间。
  • 吞吐量: 提高了日志的吞吐量,在高并发场景下表现更佳。
  • 容错性: 一定程度上提高了容错性,即使目标 Appender 出现故障,也不会直接影响应用线程。

缺点:

  • 延迟: 日志写入存在延迟,不是实时写入。
  • 丢失风险: 如果异步队列满了,可能会丢失日志。

Logback Async Appender 的配置

下面是一个 Logback Async Appender 的基本配置示例:

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>512</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <appender-ref ref="FILE"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="ASYNC"/>
    </root>
</configuration>

配置项解释:

  • queueSize 异步队列的大小,默认值为 256。
  • discardingThreshold 当队列剩余容量低于该值时,会丢弃 TRACEDEBUGINFO 级别的日志,保留 WARNERROR 级别的日志。 默认值为队列大小的 20%。设置为 0 表示不丢弃任何日志。
  • appender-ref 指定实际写入日志的 Appender。

异步队列容量与丢弃策略

在高并发的场景下,合理设置异步队列的容量和丢弃策略至关重要,直接关系到日志的完整性。

1. 队列容量 (queueSize)

  • 过小: 队列很容易被填满,导致大量日志被丢弃。
  • 过大: 占用过多的内存资源,可能影响应用的性能。

如何选择合适的队列容量?

这需要根据应用的实际情况进行评估,包括:

  • 日志产生速度: 评估应用在高峰期每秒产生的日志数量。
  • 日志写入速度: 评估日志写入目标 Appender 的速度。
  • 可接受的延迟: 评估可接受的日志写入延迟。

公式:

一个简单的估算公式:

queueSize = (日志产生速度 - 日志写入速度) * 可接受的延迟

举例:

假设应用高峰期每秒产生 1000 条日志,日志写入速度为每秒 800 条,可接受的延迟为 1 秒,那么:

queueSize = (1000 - 800) * 1 = 200

考虑到一些突发情况,建议将队列容量设置为 200 的倍数,例如 256 或 512。

实际操作中,可以先设置一个初始值,然后通过监控日志丢弃情况来调整队列容量。 如果发现有大量日志被丢弃,则需要增加队列容量。

2. 丢弃策略 (discardingThreshold)

discardingThreshold 参数控制着当队列接近满时,哪些级别的日志会被丢弃。

  • discardingThreshold = 0 不丢弃任何日志,即使队列满了,新的日志也会被阻塞,直到队列有空闲位置。
  • discardingThreshold > 0 当队列剩余容量低于 discardingThreshold 时,会根据日志级别进行丢弃。

丢弃策略的选择:

  • 关注关键信息: 通常情况下,WARNERROR 级别的日志包含关键的错误信息,应该尽量保留。
  • 灵活调整: 可以根据实际需求调整 discardingThreshold 的值。如果需要保留所有的日志,可以将 discardingThreshold 设置为 0。如果可以容忍丢失一些 TRACEDEBUG 级别的日志,可以将 discardingThreshold 设置为一个较大的值。

丢弃日志的级别优先级:

Logback 丢弃日志的优先级顺序为:TRACE < DEBUG < INFO。 也就是说,当队列接近满时,会优先丢弃 TRACE 级别的日志,然后是 DEBUG 级别的日志,最后是 INFO 级别的日志。WARNERROR 级别的日志不会被丢弃。

3. 自定义丢弃策略 (实现 DiscardingThresholdEvaluator)

虽然 Logback 内置了基于日志级别的丢弃策略,但在某些情况下,我们可能需要自定义丢弃策略。 例如,根据日志内容来决定是否丢弃日志。

要实现自定义丢弃策略,需要实现 DiscardingThresholdEvaluator 接口。

package com.example;

import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.DiscardingThresholdEvaluator;

public class MyDiscardingThresholdEvaluator extends ContextAwareBase implements DiscardingThresholdEvaluator<ILoggingEvent> {

    private int discardingThreshold;

    public void setDiscardingThreshold(int discardingThreshold) {
        this.discardingThreshold = discardingThreshold;
    }

    @Override
    public boolean isDiscarded(ILoggingEvent event, int queueSize) {
        // 自定义丢弃逻辑,例如,如果日志内容包含 "sensitive data",则丢弃
        if (event.getFormattedMessage().contains("sensitive data") && queueSize <= discardingThreshold) {
            return true;
        }
        return false;
    }
}

配置自定义丢弃策略:

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>512</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <discardingThresholdEvaluator class="com.example.MyDiscardingThresholdEvaluator">
            <discardingThreshold>100</discardingThreshold>
        </discardingThresholdEvaluator>
        <appender-ref ref="FILE"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="ASYNC"/>
    </root>
</configuration>

注意: 自定义丢弃策略会增加日志处理的复杂度,需要谨慎使用。

监控与调优

配置好 Async Appender 之后,需要进行监控,及时发现问题并进行调优。

监控指标:

  • 队列大小: 监控队列的剩余容量,如果队列经常接近满,则需要增加队列容量。
  • 丢弃日志数量: 监控丢弃的日志数量,如果丢弃的日志数量过多,则需要调整队列容量或丢弃策略。
  • 日志延迟: 监控日志写入的延迟,如果延迟过高,则需要检查目标 Appender 的性能,或者调整队列容量。

调优策略:

问题 可能原因 解决方案
大量日志被丢弃 队列容量过小,日志产生速度远大于写入速度 增加队列容量 (queueSize),优化目标 Appender 的性能,降低日志产生速度(例如,降低日志级别),调整丢弃策略 (discardingThreshold)
日志延迟过高 目标 Appender 性能瓶颈,队列容量过大 优化目标 Appender 的性能(例如,使用更快的存储介质),降低队列容量 (queueSize),检查是否有其他资源竞争
内存占用过高 队列容量过大 降低队列容量 (queueSize),评估是否真的需要这么大的队列
CPU 占用过高(自定义策略) 自定义丢弃策略逻辑复杂 优化自定义丢弃策略的逻辑,避免复杂的计算

监控工具:

可以使用 Logback 的 JMX 接口来监控 Async Appender 的状态。 也可以使用 Micrometer 等监控框架将 Async Appender 的指标暴露出来,然后使用 Prometheus 和 Grafana 等工具进行监控。

代码示例:模拟高并发场景

为了演示 Async Appender 的效果,我们可以编写一个简单的程序来模拟高并发场景。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LogExample {

    private static final Logger logger = LoggerFactory.getLogger(LogExample.class);

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 1000; i++) {
            int taskId = i;
            executor.submit(() -> {
                for (int j = 0; j < 100; j++) {
                    logger.info("Task: {}, Message: {}", taskId, j);
                    try {
                        Thread.sleep(1); // 模拟一些处理时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待所有任务完成
        }

        System.out.println("All tasks finished.");
    }
}

运行程序,并观察日志输出。 可以修改 Logback 的配置,调整队列容量和丢弃策略,观察对日志输出的影响。

最佳实践

  • 选择合适的队列容量: 根据应用的实际情况进行评估,并进行监控和调整。
  • 合理设置丢弃策略: 根据业务需求,选择合适的丢弃策略,确保关键信息不丢失。
  • 监控 Async Appender 的状态: 及时发现问题并进行调优。
  • 优化目标 Appender 的性能: 目标 Appender 的性能是整个日志系统的瓶颈,需要进行优化。
  • 避免过度使用异步: 异步虽然可以提高吞吐量,但也会增加复杂度,需要根据实际情况进行权衡。

总结:理解异步日志,优化配置,保障关键日志不丢失

Java 服务日志打印不全是一个常见的问题,Logback Async Appender 提供了一种异步写入日志的解决方案。通过合理配置队列容量和丢弃策略,可以有效地解决日志打印不全的问题。 理解异步日志的原理,针对性优化配置,并进行监控,可以最大程度地保障关键日志不丢失,为线上问题排查提供保障。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注