线上日志故障排查实战:磁盘满与日志框架调优
线上日志故障排查实战:磁盘满与日志框架调优
线上日志故障排查实战:磁盘满与日志框架调优是一个重要的技术主题,它在现代软件开发中扮演着关键角色。
本文系统介绍了线上日志故障排查实战:磁盘满与日志框架调优的核心概念和实践经验,帮助你深入理解这一技术领域。
┌─────────────────────────────────────────────┐
│ 日志故障全景图 │
├─────────────────────────────────────────────┤
│ 1. 磁盘满 — 日志文件占满磁盘 │
│ 2. 异步日志阻塞 — 队列满了阻塞业务线程 │
│ 3. 日志性能差 — 同步写日志拖慢接口 │
│ 4. 日志丢失 — 异常情况下日志没写出来 │
└─────────────────────────────────────────────┘每一种都可能导致线上故障,下面逐一排查。
二、磁盘满排查
2.1 发现问题
# 磁盘使用率告警
df -h
# 输出:
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda1 50G 49G 0.5G 99% / ← 99%!
# /dev/sdb1 200G 100G 100G 50% /data2.2 找到大文件
# 方法一:查找大于 1GB 的文件
find / -type f -size +1G -exec ls -lh {} \; 2>/dev/null | sort -k5 -hr
# 方法二:查看目录大小(推荐 ncdu)
du -sh /* 2>/dev/null | sort -hr | head -20
# 方法三:快速定位大目录
du -h --max-depth=2 /var/log/ | sort -hr
# 输出示例:
# 45G /var/log/myapp/
# 3.2G /var/log/myapp/app.log
# 2.8G /var/log/myapp/app.2026-06-26.log
# 1.5G /var/log/myapp/app.2026-06-25.log.gz
# ...2.3 日志文件分析
# 查看日志文件详情
ls -lh /var/log/myapp/
# 输出:
# -rw-r--r-- 1 root root 3.2G Jun 27 10:00 app.log ← 当前日志 3.2GB!
# -rw-r--r-- 1 root root 2.8G Jun 26 23:59 app.2026-06-26.log ← 昨天的没压缩
# -rw-r--r-- 1 root root 2.5G Jun 25 23:59 app.2026-06-25.log
# -rw-r--r-- 1 root root 1.2G Jun 24 23:59 app.2026-06-24.log
# 总计: 45GB 的日志文件!2.4 紧急处理
# 1. 压缩历史日志(不删除,先压缩)
gzip /var/log/myapp/app.2026-06-26.log
# 2.8GB → 约 200MB
# 2. 清空当前日志(不是删除!删除会导致句柄不释放)
> /var/log/myapp/app.log
# 或用 truncate
truncate -s 0 /var/log/myapp/app.log
# 3. 如果删除了文件但空间没释放(进程还持有句柄)
lsof | grep deleted
# 找到持有句柄的进程,需要重启该进程或发送 HUP 信号
kill -HUP <PID> # 让日志框架重新打开日志文件重要:不要直接 rm 大日志文件! 删除文件不会释放空间,因为进程还在写入。应该用 truncate 清空文件内容。
三、Logback 日志配置调优
3.1 问题配置(常见反面教材)
<!-- 问题配置:没有轮转、没有压缩、没有大小限制 -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>/var/log/myapp/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>这个配置的问题:
- 用
FileAppender(不会轮转)→ 文件无限增长 - 日志级别 DEBUG → 日志量巨大
- 同步写入 → 影响性能
- 没有压缩 → 历史日志占大量空间
3.2 推荐配置
<configuration>
<!-- 属性配置 -->
<property name="LOG_PATH" value="/var/log/myapp" />
<property name="APP_NAME" value="myapp" />
<!-- 控制台输出(开发环境用) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 滚动文件 Appender -->
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<!-- 滚动策略:按时间 + 大小 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 文件名模式:日期 + 序号 + gzip 压缩 -->
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!-- 单个文件最大 200MB -->
<maxFileSize>200MB</maxFileSize>
<!-- 保留 30 天日志 -->
<maxHistory>30</maxHistory>
<!-- 总大小限制 10GB -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<!-- 保留 caller 信息(较慢,生产可去掉) -->
<!-- <pattern>%d [%thread] %-5level %logger{36} - %msg%n</pattern> -->
</encoder>
<!-- 过滤掉低于 WARN 的日志(可选,对某些 appender) -->
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter> -->
</appender>
<!-- 异步 Appender -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 异步队列大小 -->
<queueSize>8192</queueSize>
<!-- 队列剩余容量低于此值时丢弃 DEBUG/TRACE 日志 -->
<discardingThreshold>1024</discardingThreshold>
<!-- 队列满时不阻塞业务线程 -->
<neverBlock>true</neverBlock>
<!-- 包含 caller data(较慢,生产关闭) -->
<includeCallerData>false</includeCallerData>
<appender-ref ref="ROLLING_FILE" />
</appender>
<!-- 错误日志单独文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 应用日志级别 -->
<logger name="com.example" level="INFO" />
<logger name="com.example.mapper" level="DEBUG" /> <!-- MyBatis SQL 日志 -->
<!-- 框架日志调高 -->
<logger name="org.springframework" level="WARN" />
<logger name="org.hibernate" level="WARN" />
<logger name="com.zaxxer.hikari" level="WARN" />
<logger name="org.apache.kafka" level="WARN" />
<!-- Root -->
<root level="INFO">
<appender-ref ref="ASYNC_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>3.3 关键配置项详解
maxFileSize:单个日志文件最大大小
太小(10MB):文件太多,轮转频繁,IO 开销大
太大(2GB):单文件太大,grep 查看不方便
推荐:200MBmaxHistory:保留天数
太短(3天):排查历史问题时日志不够
太长(365天):磁盘空间不够
推荐:30天(普通日志),90天(错误日志)totalSizeCap:总大小上限
这是最重要的磁盘保护参数!
设置后 Logback 会自动清理旧日志,不会撑满磁盘。
推荐:磁盘总容量的 20%-30%queueSize:异步队列大小
太小(256):高并发时容易满,丢弃日志
太大(65536):占用内存,队列积压时延迟大
推荐:4096-8192neverBlock:队列满时是否阻塞
true:不阻塞,丢弃日志(业务优先)
false:阻塞业务线程直到队列有空间(日志优先)
生产推荐:true(不能因为日志阻塞业务)四、异步日志阻塞问题
4.1 问题现象
接口偶发性变慢,日志中出现:
WARN AsyncAppender-1 - Discarded 1024 log events due to queue overflow或者业务线程阻塞在日志写入:
jstack <PID> | grep -A 5 "AsyncAppender""http-nio-8080-exec-5" #42 prio=5
java.lang.Thread.State: WAITING (on monitor)
at ch.qos.logback.classic.AsyncAppender.put(AsyncAppender.java:285)
- waiting to lock <0x000000076b123456> (a java.util.concurrent.ArrayBlockingQueue)
at com.example.service.OrderService.createOrder(OrderService.java:87)4.2 原因分析
异步日志的工作原理:
业务线程 → 写入队列 → AsyncAppender 线程 → 写入磁盘
如果磁盘 IO 慢(如 HDD 或磁盘满),AsyncAppender 线程写盘慢:
- 队列积压 → 队列满
- neverBlock=false → 业务线程被阻塞
- neverBlock=true → 日志被丢弃4.3 排查步骤
第一步:检查队列状态
// 如果使用 Logback,可以通过 JMX 监控
// 或自定义 Appender 统计队列状态
public class MonitoringAsyncAppender extends AsyncAppender {
@Override
protected void append(E e) {
int queueSize = getQueueSize();
if (queueSize > getQueueSize() * 0.8) {
System.err.println("日志队列使用率超过 80%: " + queueSize);
}
super.append(e);
}
}第二步:检查磁盘 IO
iostat -x 1
# 如果 %util > 80% 且 await > 50ms → 磁盘 IO 是瓶颈第三步:检查日志量
# 统计每秒日志写入量
tail -f /var/log/myapp/app.log | pv -l -i 1 > /dev/null
# 输出: 5000 lines/秒 → 每秒 5000 行日志!4.4 解决方案
方案一:减少日志量
// 问题:DEBUG 日志全开
<logger name="com.example" level="DEBUG" />
// 修复:生产环境用 INFO
<logger name="com.example" level="INFO" />
// 修复:MyBatis SQL 日志在开发环境开,生产关闭
// 开发: <logger name="com.example.mapper" level="DEBUG" />
// 生产: <logger name="com.example.mapper" level="INFO" />方案二:使用更快的日志框架
<!-- Log4j2 异步日志(性能比 Logback AsyncAppender 高 10 倍) -->
<!-- Log4j2 使用 Disruptor 无锁队列 -->
<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>
</dependency><!-- log4j2.xml -->
<Configuration>
<Appenders>
<RollingFile name="File" fileName="/var/log/myapp/app.log"
filePattern="/var/log/myapp/app.%d{yyyy-MM-dd}.%i.log.gz">
<PatternLayout pattern="%d [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="200MB"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
</Appenders>
<!-- 全局异步日志 -->
<Loggers>
<AsyncRoot level="info">
<AppenderRef ref="File"/>
</AsyncRoot>
</Loggers>
</Configuration>
<!-- 或在 JVM 参数中开启全局异步 -->
<!-- -Dlog4j2.asyncLoggerRingBufferSize=262144 -->
<!-- -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -->Logback vs Log4j2 异步性能对比:
| 指标 | Logback AsyncAppender | Log4j2 AsyncLogger |
|---|---|---|
| 底层实现 | ArrayBlockingQueue | LMAX Disruptor |
| 锁 | 有锁 | 无锁 |
| 吞吐量 | ~50,000 msg/s | ~500,000 msg/s |
| 延迟 | 微秒级 | 纳秒级 |
如果日志量特别大(>10 万条/秒),Log4j2 的异步日志是更好的选择。
方案三:日志写入单独的磁盘
<!-- 日志写入独立的日志磁盘,不影响系统和数据 -->
<property name="LOG_PATH" value="/data/logs" />
<!-- /data 挂载在独立磁盘 -->五、日志丢失问题
5.1 问题场景
应用崩溃时,最后的日志没有写出来——可能是最关键的错误信息。
5.2 原因
异步日志:日志在队列中还没写盘,应用就崩溃了 → 丢失
同步日志:在应用退出前缓冲区没 flush → 丢失5.3 解决方案
JVM 关闭钩子确保日志 flush
// 注册关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop(); // 停止 Logback,flush 所有缓冲区
}));Logback 配置 shutdown hook
<configuration>
<!-- 开启 shutdown hook -->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook">
<delay>500</delay> <!-- 延迟 500ms 确保日志写完 -->
</shutdownHook>
<!-- 其他配置 -->
</configuration>重要日志同步写
// 关键错误日志用同步写(确保不丢)
// 在 Logback 中为 ERROR 级别配置同步 Appender
<appender name="SYNC_ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/app-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<immediateFlush>true</immediateFlush> <!-- 立即 flush -->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>六、日志内容最佳实践
6.1 日志格式
// 好的日志:包含上下文信息
log.info("订单创建成功 | orderId={} | userId={} | amount={} | payMethod={} | cost={}ms",
order.getId(), order.getUserId(), order.getAmount(),
order.getPayMethod(), cost);
// 坏的日志:缺乏上下文
log.info("订单成功了");
log.info("error: " + e.getMessage()); // 没有 traceId,没有堆栈6.2 不要打印的内容
// 1. 不要打印密码、密钥、token
log.info("用户登录: password={}", password); // 绝对不行!
log.info("用户登录: userId={}", userId); // 正确
// 2. 不要打印大对象
log.debug("响应数据: {}", objectMapper.writeValueAsString(hugeList)); // 序列化大对象
// 如果确实需要,截断
log.debug("响应数据大小: {} items", hugeList.size());
// 3. 不要在循环中打日志
for (Item item : items) {
log.debug("处理: {}", item); // 10000 条 → 10000 行日志
}
// 改为
log.info("批量处理完成: count={}", items.size());
// 4. 不要打印敏感用户信息
log.info("用户信息: {}", user); // user 可能包含身份证号
// 使用脱敏
log.info("用户信息: userId={}, phone={}", user.getId(), maskPhone(user.getPhone()));6.3 异常日志的正确姿势
// 坏:丢失堆栈
try {
doSomething();
} catch (Exception e) {
log.error("出错: {}", e.getMessage()); // 只有 message,没有堆栈!
}
// 坏:log.error 但异常被吞
try {
doSomething();
} catch (Exception e) {
log.error("出错", e); // 有堆栈但没有上下文
// 没有 throw,异常被吞
}
// 好:上下文 + 堆栈 + 正确处理
try {
doSomething(orderId);
} catch (Exception e) {
log.error("处理订单失败 | orderId={} | userId={}", orderId, userId, e);
throw new BusinessException("订单处理失败", e);
}七、日志监控与告警
7.1 日志磁盘监控
# Prometheus 告警规则
groups:
- name: log
rules:
- alert: DiskUsageHigh
expr: (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 > 85
for: 5m
annotations:
summary: "磁盘使用率超过 85%"
- alert: LogFileTooLarge
expr: node_filesystem_size_bytes{mountpoint="/var/log"} > 50_000_000_000
annotations:
summary: "日志目录超过 50GB"7.2 ELK / Loki 日志收集
# 使用 Filebeat 收集日志到 ELK
# filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/log/myapp/*.log
fields:
app: myapp
env: production
output.logstash:
hosts: ["logstash:5044"]
# 或使用 Loki + Promtail
# promtail-config.yml
scrape_configs:
- job_name: myapp
static_configs:
- targets: [localhost]
labels:
job: myapp
__path__: /var/log/myapp/*.log7.3 日志告警规则
# 错误日志告警
groups:
- name: app_logs
rules:
- alert: ErrorLogSpike
expr: increase(log_messages_total{level="ERROR"}[5m]) > 100
annotations:
summary: "5 分钟内错误日志超过 100 条"
- alert: OOMInLog
expr: increase(log_messages_total{message=~".*OutOfMemoryError.*"}[1m]) > 0
annotations:
summary: "日志中出现 OOM"八、面试要点总结
Q1:日志磁盘满了怎么处理?
紧急处理:
truncate清空当前日志(不是rm)gzip压缩历史日志- 检查日志框架是否配置了轮转和大小限制
长期方案:
- 配置
SizeAndTimeBasedRollingPolicy(按时间+大小轮转) - 设置
totalSizeCap(总大小上限) - 历史日志压缩(.gz)
- 接入 ELK/Loki 日志收集
Q2:同步日志和异步日志的区别?
- 同步:业务线程直接写磁盘,IO 慢会阻塞业务
- 异步:业务线程写入内存队列,后台线程写磁盘
- 异步的
neverBlock=true时队列满会丢弃日志,不影响业务 - 推荐生产环境用异步,但 ERROR 日志可同步写
Q3:Logback 的 totalSizeCap 有什么用?
设置日志文件的总大小上限。超过后 Logback 自动删除最老的日志文件。这是防止磁盘满的最重要的配置项。
Q4:日志丢失怎么解决?
- 配置 shutdown hook,应用退出时 flush 日志
- 重要日志(ERROR)用同步写 +
immediateFlush=true - 异步队列设置合理的
discardingThreshold,优先保留高级别日志 - 不要设置
neverBlock=true对 ERROR 日志
Q5:Log4j2 的异步日志为什么比 Logback 快?
Log4j2 的 AsyncLogger 使用 LMAX Disruptor 无锁队列,而 Logback 的 AsyncAppender 使用 ArrayBlockingQueue(有锁)。Disruptor 通过环形缓冲区和 CAS 操作实现无锁写入,吞吐量高 10 倍以上。
九、总结
日志故障排查的核心原则:
- 磁盘保护是第一要务:
totalSizeCap+maxHistory+ 压缩,三重保险 - 异步优先:生产环境必须用异步日志
- 级别合理:生产 INFO,框架 WARN,关键路径 DEBUG 需要采样
- 内容规范:有上下文、有 traceId、不泄露敏感信息
- 监控告警:磁盘使用率、错误日志量、日志队列状态
日志配置检查清单:
□ 使用 RollingFileAppender(不是 FileAppender)
□ 配置了 SizeAndTimeBasedRollingPolicy
□ 设置了 totalSizeCap(总大小上限)
□ 历史日志使用 .gz 压缩
□ 使用了 AsyncAppender
□ 生产日志级别为 INFO
□ 框架日志调高到 WARN
□ ERROR 日志单独文件
□ 配置了 shutdownHook
□ 不打印密码/密钥/大对象记住:日志是排查问题的工具,不要让日志本身成为问题。好的日志配置应该是"设置好就忘了",不需要人工干预。