一、问题复现:从日志告警到服务崩溃

背景:某日凌晨,运维监控系统触发两条告警:

  1. **磁盘使用率95%**:日志目录/app/logs占用200GB

  2. 服务内存超限:Java堆内存持续增长至8GB(上限为4GB)

现象
• 日志文件app.log大小异常膨胀至180GB(正常每日1GB)
• 日志内容出现大量重复错误:AsyncAppender queue is full
• 部分服务线程阻塞在日志写入操作,导致HTTP请求超时


二、排查过程:异步日志的“雪崩效应”
1. 初步定位:日志滚动策略失效

检查logback-spring.xml配置:

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">  
    <queueSize>1024</queueSize>  <!-- 默认队列大小 -->  
    <discardingThreshold>0</discardingThreshold>  <!-- 队列满时丢弃日志 -->  
    <appender-ref ref="FILE"/>  
</appender>  

问题点
discardingThreshold=0导致队列满时直接丢弃日志,但磁盘已存在180GB日志
• 矛盾点:日志被丢弃,为何磁盘仍被写满?

2. 关键发现:同步依赖引发的死锁

日志依赖链路

AsyncAppender (内存队列) → FileAppender (写磁盘)  
                          ↓  
                          KafkaAppender (同步发送日志到Kafka)  

根因
• Kafka集群故障导致KafkaAppender阻塞
FileAppenderKafkaAppender阻塞而变慢
AsyncAppender队列积压 → 内存飙升 + 磁盘写入失控

3. 复现实验:队列积压的连锁反应
// 强制制造队列阻塞  
for (int i = 0; i < 100_0000; i++) {  
    log.info("Stress test log: {}", i);  
}  

监控指标
内存AsyncAppender队列占用1.2GB堆内存
磁盘:日志文件以10MB/s速度增长
线程:KafkaAppender线程状态BLOCKED


三、解决方案:三层防御体系
1. 紧急处理(止损)
# 临时释放磁盘空间  
log_file=/app/logs/app.log  
echo "" > $log_file  # 清空日志(慎用!可能丢失数据)  

# 修改日志级别为ERROR  
curl -X POST http://localhost:8080/actuator/loggers/root --data '{"configuredLevel":"ERROR"}'  
2. 配置优化(治标)
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">  
    <queueSize>2048</queueSize>  
    <discardingThreshold>20</discardingThreshold>  <!-- 队列80%满时丢弃INFO以下日志 -->  
    <neverBlock>true</neverBlock>  <!-- 队列满时非阻塞写入 -->  
    <appender-ref ref="FILE"/>  
</appender>  

<!-- 移除KafkaAppender的同步依赖 -->  
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
    <file>app.log</file>  
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">  
        <fileNamePattern>app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>  
        <maxFileSize>500MB</maxFileSize>  <!-- 单文件最大500MB -->  
        <maxHistory>7</maxHistory>  
    </rollingPolicy>  
</appender>  
3. 链路监控(治本)
# Prometheus监控配置  
metrics:  
  logs:  
    async_queue_size:  
      enabled: true  
    logback_events:  
      enabled: true  

# Grafana告警规则  
- alert: LogQueueOverflow  
  expr: logback_async_queue_size > 1500  
  for: 5m  
  labels:  
    severity: critical  
  annotations:  
    summary: "异步日志队列积压 (instance {{ $labels.instance }})"  

四、经验总结:异步日志的6条军规
  1. 队列容量与内存平衡
    queueSize ≤ 可用堆内存 / 每条日志预估大小(建议≤2048)

  2. 谨慎使用同步Appender
    • 避免在异步链路中调用网络IO操作(如Kafka/DB写入)

  3. 防御式丢弃策略
    • 设置discardingThreshold=20%(保留ERROR日志)

  4. 滚动策略硬限制
    • 单日志文件≤500MB,保留≤7天

  5. 监控埋点全覆盖
    • 采集队列大小、丢弃日志数、写入耗时

  6. 定期压测验证
    • 模拟日志洪峰,观察内存/磁盘/线程状态


五、推荐书籍
  1. 《Java性能权威指南(第2版)》(豆瓣9.1)
    • 第8章“日志与监控”详解异步日志性能陷阱
    • 含Grafana监控模板配置指南

  2. 《Logback中文手册》(GitHub开源文档)
    • 第6章“Appender深度解析”对比AsyncAppender实现方案
    • 提供队列阻塞的数学建模公式

  3. 《分布式系统:概念与设计》(豆瓣9.4)
    • 第12章“容错与可靠性”分析日志系统级联故障
    • 包含RAFT算法在日志同步中的应用


讨论:你在日志系统设计中还踩过哪些坑?

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐