4. 日志切割与归档:别让日志撑爆你的磁盘

日志这东西,不切不行。我见过太多线上事故,最后查下来根因居然是「磁盘写满了,服务挂了」。你想想看,一个不设防的日志文件,在高并发下一天能涨到几十个 GB。所以今天咱们聊聊怎么管好这些日志——切割、归档、保留、收集,一套组合拳打下来,磁盘再也不会报警。

4.1 为什么要做日志切割?

说白了,就是一个文件不能无限写下去。原因有三:

  • 磁盘空间有限——单个日志文件撑爆分区,服务直接宕机。
  • 查询效率低下——一个 10GB 的文本文件,你用 grep 搜个关键词,等得花儿都谢了。
  • 归档管理困难——没有切割,你就没法按天或按小时做历史回溯。

我在项目中遇到过一台 Nginx 服务器,access.log 三个月没切,最后涨到 47GB。运维同学想用 tail 看实时日志,结果 tail 直接卡死。嗯,从那以后我养成了习惯:日志切割必须写在部署脚本里,谁忘了谁请下午茶。

4.2 按时间切割:最常用的策略

按时间切,是最直观的做法。常见的有按小时、按天、按周。我个人习惯按天切,因为粒度适中,既不会产生太多小文件,也不会让单文件太大。

Linux 下最常用的工具是 logrotate。它的配置非常灵活,我贴一个我常用的模板:

/var/log/myapp/*.log {
    daily                  # 按天切割
    rotate 30              # 保留最近 30 份
    compress               # 压缩旧日志(gzip)
    delaycompress          # 延迟一天压缩,方便当天查
    missingok              # 日志文件不存在也不报错
    notifempty             # 空文件不切割
    dateext                # 用日期作为后缀
    dateformat -%Y%m%d     # 日期格式:-20250321
    sharedscripts          # 多个日志文件共享一个脚本
    postrotate
        /usr/bin/killall -HUP myapp 2>/dev/null || true
    endscript
}
小提示:delaycompress 这个参数很实用。切割当天生成的旧日志还是文本格式,方便你临时查问题。第二天才会被压缩成 .gz,既省空间又不影响排查。

为什么用 killall -HUP?因为很多应用(比如 syslog-ng、Nginx)收到 HUP 信号后会重新打开日志文件句柄。如果不做这一步,切割后日志还会继续往旧文件里写,等于白切了。

4.3 按大小切割:应对突发流量

按时间切有个问题:如果某天流量暴增,日志量可能是平时的 10 倍。按天切的话,单文件还是会很大。这时候就需要按大小切割作为补充。

还是 logrotate,配置稍微改一下:

/var/log/myapp/*.log {
    size 100M              # 日志超过 100MB 就切
    rotate 50              # 保留 50 份
    compress
    missingok
    notifempty
    dateext
    dateformat -%Y%m%d-%H%M%S  # 精确到秒,避免重名
    postrotate
        /usr/bin/killall -HUP myapp 2>/dev/null || true
    endscript
}

这里有个坑:size 和 daily 可以同时配置,logrotate 会按「谁先触发就切谁」的原则执行。我曾经踩过这个坑——同时配了 size 100M 和 daily,结果一天切了 80 多次,生成了 80 多个小文件,归档系统直接报警。后来我改成只按大小切,或者只按时间切,二选一,别贪心。

策略 适用场景 优点 缺点
按时间切割 流量平稳、可预测 文件命名规律,便于回溯 突发流量下文件可能过大
按大小切割 流量波动大、不可预测 单文件大小可控 文件数量多,命名杂乱
时间+大小混合 需要双重保障 兼顾规律性和可控性 配置复杂,可能产生过多文件

4.4 日志保留策略:该删就删

日志不能无限保留。我见过有些团队把日志保留 365 天,结果磁盘成本比服务器还高。合理的保留策略应该根据业务需求来定:

  • 调试日志(DEBUG/TRACE):保留 3-7 天,主要用于排查近期问题。
  • 业务日志(INFO/WARN):保留 30 天,满足大部分审计和回溯需求。
  • 错误日志(ERROR/FATAL):保留 90 天,用于长期趋势分析。
  • 安全审计日志:保留 180 天以上,合规要求。

在 logrotate 里,rotate 30 就是保留最近 30 份。配合 maxage 30 可以按天数删除:

/var/log/myapp/*.log {
    daily
    rotate 30
    maxage 30              # 超过 30 天的日志直接删除
    compress
    ...
}
注意:rotate 和 maxage 是「或」的关系,谁先满足就触发删除。比如你配了 rotate 30 和 maxage 60,当文件数量超过 30 份时就会删除最旧的,即使它还没到 60 天。

4.5 集中式日志收集:告别 SSH 到每台机器

服务器一多,每台机器上去 tail -f 就不现实了。你需要一个集中式日志收集系统。常见的方案有 ELK(Elasticsearch + Logstash + Kibana)、Loki + Promtail、以及轻量级的 Filebeat + Kafka + Logstash。

我个人比较推荐 Filebeat + Kafka + Logstash 这个链路,原因有三:

  1. Filebeat 足够轻量——资源占用极低,不会影响业务进程。
  2. Kafka 做缓冲——即使下游 Logstash 挂了,日志也不会丢。
  3. Logstash 做过滤和转换——可以解析 JSON、提取字段、过滤掉无用的 DEBUG 日志。

一个典型的 Filebeat 配置长这样:

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/myapp/*.log
  multiline.pattern: '^\d{4}-\d{2}-\d{2}'
  multiline.negate: true
  multiline.match: after

output.kafka:
  hosts: ["kafka1:9092", "kafka2:9092"]
  topic: "myapp-logs"
  partition.round_robin:
    reachable_only: true

这里有个细节:multiline 配置。Java 应用的异常栈是跨多行的,如果不做多行合并,一条异常会被拆成几十条日志,查问题的时候根本没法看。我当初第一次配 Filebeat 时就忘了这个,结果 Kibana 里看到一堆碎片化的堆栈信息,排查了半小时才发现是配置问题。

4.6 整体架构图:从切割到收集

下面这张图展示了从日志产生到集中存储的完整链路。我把它画成了 SVG,方便你理解各组件之间的关系。

日志切割与集中收集架构图 日志切割 → 归档 → 集中收集 架构图 应用服务器 1 logrotate 切割 应用服务器 2 logrotate 切割 应用服务器 3 logrotate 切割 Filebeat 读取日志文件 Filebeat 读取日志文件 Filebeat 读取日志文件 Kafka 集群 日志缓冲 / 削峰填谷 Logstash 过滤 / 解析 / 转换 Elasticsearch(存储 + 索引) 归档策略 按天/按大小切割 保留 30 天 / 自动删除

4.7 避坑指南:我踩过的几个坑

最后分享几个实战中容易踩的坑,希望能帮你省点时间:

  • 日志文件句柄未释放——切割后应用还在往旧文件写。解决方案:确保 postrotate 里发送了 HUP 信号,或者应用本身支持 copytruncate 模式。
  • 压缩导致 CPU 飙高——如果日志量巨大,compress 会在切割瞬间消耗大量 CPU。我建议用 delaycompress 错开高峰期,或者用 nocompress 配合外部定时压缩任务。
  • Kafka 主题分区不足——Filebeat 往 Kafka 写日志时,如果分区数太少,会导致部分分区负载过高。我一般把分区数设为消费者组数量的 3-5 倍。
  • 多行日志合并失败——Java 异常栈、SQL 慢查询日志都是多行的。Filebeat 的 multiline 正则一定要写对,否则日志在 Kibana 里就是一团乱麻。

核心总结:

  • 日志切割用 logrotate,按时间或按大小,二选一别混用。
  • 保留策略根据日志级别定,DEBUG 3 天、ERROR 90 天,别一刀切。
  • 集中收集推荐 Filebeat → Kafka → Logstash → Elasticsearch,链路清晰且可靠。
  • 别忘了多行合并和文件句柄释放,这两个坑我至少各踩过一次。