3. 常见日志缺失原因分析

日志丢了,别急着骂中间件。

我排查过上百个日志缺失的案子,九成以上都出在下面这四个坑里。这四种原因听着简单,但每次出现都带着不同的面具。咱们一个个过。

3.1 代码没打日志

最直白的原因——压根没写。

别笑。我见过不少线上服务,核心业务逻辑几十行,一句 print 都没有。开发时靠 IDE 断点调试,上线后两眼一抹黑。

为什么会这样?

  • 赶进度——功能优先,日志后补,然后就没有然后了。
  • 觉得没必要——“这个分支肯定不会进”,结果生产上触发了。
  • 日志打在错误级别——用了 logger.debug,生产配置只开 INFO,等于没打。

我的习惯:每写一个 if 分支,至少打一行日志。异常捕获必须打印堆栈。别偷懒,生产环境不会给你第二个机会。

看个典型例子:

// 问题代码:安静地吞异常
try {
    process(data);
} catch (Exception e) {
    // 啥也不打印,连堆栈都不留
}

// 正确做法:
try {
    process(data);
} catch (Exception e) {
    log.error("数据处理失败,dataId={}", data.getId(), e);  // 第三个参数传异常!
}

我曾在项目中遇到一个诡异的内存泄漏,查了两天,最后发现某个异步回调里 catch 了异常但没打任何日志。线上症状就是偶尔丢消息,根本无从下手。

3.2 缓冲区溢出

日志打好了,但输出通道满了,写不进去。

缓冲区溢出的本质:生产者(业务线程)比消费者(日志写盘/网络发送)快太多。队列满了之后,常见的处理策略是丢弃——日志就丢了。

框架/组件溢出策略后果
Logback AsyncAppender丢弃最旧 / 丢弃当前10% ~ 30% 日志丢失
Java Util Logging阻塞写入业务线程卡死
Syslog (UDP)静默丢弃无反馈,最坑
自定义内存队列offer 超时丢弃日志“假成功”

你应该关注两个指标:队列积压率和丢弃计数器。Logback 的 DiscardingPolicy 会打印一条 WARN,但很多人根本没开监控看。

我曾经踩过的坑:给一个高并发服务配了异步日志,队列大小设为 256。压测发现业务延迟正常,但线上始终缺日志。后来一查,每秒产生 2000 条日志,队列被塞满后疯狂丢弃。恢复队列后,写入线程又成了瓶颈——磁盘 IO 扛不住。

教训:异步日志不是银弹。必须结合流量压测来配队列长度和丢弃策略。

3.3 磁盘满了

日志写磁盘,磁盘满了,日志自然写不进去。这是最“物理”的原因。

但有趣的是,很多时候不是日志所在的磁盘满了,而是其他进程把空间占了。比如临时文件、Docker 容器日志、core dump 文件。

怎么快速定位?

  1. 执行 df -h 看各分区使用率。
  2. 用 lsof | grep deleted | grep log 找已经被删但进程还占着句柄的文件——这种情况常见于日志轮转后没重启应用。
  3. 检查 /var/log 下是否有 messages 或 syslog 的 .gz 归档没清理。

我参与过一次事故复盘:某个监控系统突然不报日志了,查了两小时,最后发现 / 分区 100%。罪魁祸首是 Docker overlay2 目录里残留的容器日志,一个容器攒了 20GB。你想想看,日志框架尝试写文件时返回 ENOSPC,很多框架的处理方式很粗暴——直接放弃写,连错误都不往外抛。

预防建议:

  • 日志单独挂盘,比如 /data/logs,和系统盘分离。
  • 设置磁盘水位告警,80% 通知,95% 紧急。
  • 使用 logrotate 并配置 maxage 或 maxsize,保证历史日志不撑爆。

3.4 网络丢包

日志要发到远端(Kafka、ELK、Syslog 服务器)时,网络链路本身不可靠。

UDP 传输最常见也最惨。Syslog (UDP)、某些 metrics agent 默认用 UDP,丢包了发送端完全不知道。TCP 会好一些,但连接中断、重连期间的日志也会丢——尤其是在长连接频繁重建、缓冲区又没持久化的时候。

我曾经遇到一个典型场景:

  • 应用打印日志到本地文件,同时用 Filebeat 发到 Logstash。
  • 某次网络抖动,Filebeat 和 Logstash 之间的 TCP 连接断了。Filebeat 的 registry 文件记录了读取偏移量,但连接恢复后,重传期间新产生的日志写入本地文件,Filebeat 还没来得及读——这段时间的日志就没有被发送。
  • 表面上看 Filebeat 有重传机制,但实际丢包窗口只有 1~2 秒,却足以漏掉关键错误。

解决方案不是单一的:

方案优缺点
TCP + ACK 确认可靠,但写放大,延迟增加
本地文件 + 审计监控本地不丢,但采集端可能丢;需要比对文件内行号
消息队列 + 持久化最可靠,但架构复杂
数据完整性校验接收端对每批日志计算哈希,发送端重传缺失批次

我个人习惯:日志链路设计时遵循“本地优先”原则。所有关键日志先写本地磁盘,再考虑远程传输。远程传输如果丢包了,至少本地还有一份兜底数据。事后回溯可以直接从本地文件恢复。

原因总结与排查路径

下面这张图梳理了四种缺失原因以及对应的排查线索,你可以把它当作快速诊断手册。

日志缺失原因分类与排查路径 日志缺失 代码未打日志 排查:搜 .log/.print 注意 catch 块 缓冲区溢出 排查:队列丢弃计数 看 AsyncAppender 配置 磁盘满 排查:df -h → ENOSPC 检查 deleted 句柄 网络丢包 排查:抓包看重传 对比收发条数 代码未打日志 • 检查所有 catch 块 • 看 log level 配置 缓冲区溢出 • 查看丢弃策略 • 队列深度监控 磁盘满 • 磁盘水位告警 • 日志轮转配置 网络丢包 • 传输协议可靠性 • 接收端校验机制 优先排查顺序:磁盘满 → 缓冲区 → 代码 → 网络 成本从低到高,影响面从大到小

每次遇到日志缺失,别急着怀疑环境。先问问自己:代码里是不是忘了打?队列是不是被塞满了?磁盘是不是爆了?网络是不是飘了?按这个顺序查,九成的问题在十分钟内就能定位。

今天这四种原因在实际项目中往往叠加出现。缓冲区溢出可能是瞬间流量导致,磁盘满可能是另一个日志轮转配置的锅,网络丢包又可能暴露了代码根本没重试。这也是为什么我强调先用低成本手段排除物理原因,再深入代码。