JAVA 服务日志打印不全?Logback Async 异步队列容量与丢弃策略
各位同学,大家好。今天我们来聊聊 Java 服务日志打印不全的问题,以及如何利用 Logback 的 Async Appender 结合队列容量和丢弃策略来解决这个问题。日志是诊断线上问题的关键,如果日志打印不全,那无疑给排查问题带来了巨大的困难。
日志打印不全的常见原因
日志打印不全的原因有很多,但常见的包括:
- 同步阻塞: 日志打印是同步操作,如果日志量过大,或者日志写入磁盘的速度跟不上,会导致应用线程阻塞,进而影响应用的性能,甚至导致请求超时。在高并发场景下,这种阻塞会变得更加明显,导致部分日志未能及时写入。
- 缓冲区溢出: 有些日志框架或者配置不当,会导致缓冲区溢出,从而丢失部分日志。
- 异步丢失: 使用异步日志时,如果异步队列满了,且没有合适的丢弃策略,新的日志消息会被丢弃,导致日志不全。
- 异常中断: 在日志写入过程中,如果发生异常,比如磁盘空间不足,权限问题等,可能会导致日志写入中断,从而丢失部分日志。
- 配置错误: 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: 当队列剩余容量低于该值时,会丢弃TRACE、DEBUG、INFO级别的日志,保留WARN、ERROR级别的日志。 默认值为队列大小的 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时,会根据日志级别进行丢弃。
丢弃策略的选择:
- 关注关键信息: 通常情况下,
WARN和ERROR级别的日志包含关键的错误信息,应该尽量保留。 - 灵活调整: 可以根据实际需求调整
discardingThreshold的值。如果需要保留所有的日志,可以将discardingThreshold设置为 0。如果可以容忍丢失一些TRACE和DEBUG级别的日志,可以将discardingThreshold设置为一个较大的值。
丢弃日志的级别优先级:
Logback 丢弃日志的优先级顺序为:TRACE < DEBUG < INFO。 也就是说,当队列接近满时,会优先丢弃 TRACE 级别的日志,然后是 DEBUG 级别的日志,最后是 INFO 级别的日志。WARN 和 ERROR 级别的日志不会被丢弃。
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 提供了一种异步写入日志的解决方案。通过合理配置队列容量和丢弃策略,可以有效地解决日志打印不全的问题。 理解异步日志的原理,针对性优化配置,并进行监控,可以最大程度地保障关键日志不丢失,为线上问题排查提供保障。