2908 字
约 9 分钟
14
S15-A08 · 磁盘与日志:从爆盘到保留策略

凌晨两点磁盘满,应用写不进日志、数据库拒绝写入、nginx 返回 502——复盘结论往往只有一句"加个清理脚本"。但爆盘不是清理问题,是容量契约缺失:没人回答过"这块盘该装多少天的什么数据"。清理脚本只是把违约时间点从今天凌晨推到三天后的凌晨。这篇不教 du 怎么用,只谈决策:谁在吃盘、保留期定多长、压缩值不值、什么时候该彻底放弃本地日志。


1. 谁在吃盘:增长曲线决定处置方式

占用源 增长曲线 可回收 处置优先级
应用日志 与 QPS 线性,随错误率阶跃 可(按策略轮转) 高,最易失控
容器 stdout 与输出量线性,默认无上限 可(驱动级限制) 高
数据库数据/日志 与业务量相关,含 binlog/redo 部分(需归档) 中,不能手删
镜像层 随发布次数阶梯式 可(prune) 中,周期性
临时文件/上传残留 阶跃,取决于业务缺陷 可 中,先修缺陷
core dump 崩溃时阶跃,单文件可达 GB 可 低,先关掉无限制 dump

判据:阶跃型用阈值清理,线性型必须用保留策略。用清理脚本对付线性增长,等于每天清一次、每天涨回去。


2. 核心公式:增长率 × 保留期 = 稳态占用

保留策略不是"留几天"的偏好,是一道乘法题。

# 实测日增量:取 24h 差值,别用瞬时 du
before=$(du -sb /var/log/app | cut -f1); sleep 86400
after=$(du -sb /var/log/app | cut -f1)
echo "日增量 $(( (after-before)/1024/1024 )) MB"

估法:单行字节(200–600 B,取真实日志 wc -c)× 日行数(QPS × 86400 × 每请求行数,峰值 1.5 倍余量)× 保留天数 × 压缩比(文本约 10%–20%)。量级参考:单行 400 B、峰值 500 QPS、每请求 2 行 → 日增约 35 GB,压缩后 4–7 GB;留 30 天即 120–200 GB。超过盘容量 40% 就压保留期或搬走日志。


3. 保留策略的四个旋钮

旋钮 控制方式 优点 失效边界
按天 rotate N / max-age 可预测,易推容量 日志量突增时照样爆
按大小 size 200M / max-size 直接对应容量上限 高峰期一天轮转几十次,碎文件多
按总量 SystemMaxUse= / 目录配额 硬上限不可突破 超限丢最老数据,可能丢事故现场

叠两层:总量硬上限兜底 + 按天轮转定常态。只用按天,暴涨会爆盘;只用按大小,低频期积累碎文件。

# /etc/logrotate.d/app —— 兜底 + 常态
/var/log/app/*.log {
    daily
    rotate 14
    maxsize 500M        # 单文件超 500M 也立即轮转(防暴涨)
    compress
    delaycompress
    missingok
    notifempty
    dateext
    postrotate
        systemctl reload app.service >/dev/null 2>&1 || true
    endscript
}

4. 分级保留:错误长期,调试短期

日志价值随级别单调下降。DEBUG 与 ERROR 共用策略,等于用事故取证的价格买调试空间。

级别 单条价值 建议保留 理由
ERROR/FATAL 高(取证) 30–90 天 复盘、审计、客诉回溯
WARN 中 14–30 天 趋势分析够用
INFO 低 3–7 天 排查链路通常只看最近几天
DEBUG/TRACE 极低 数小时至 1 天,或默认关闭 只在开关打开时有意义

判断点:DEBUG 默认关闭时,它的保留策略就该是"打开时临时生效"。团队常年开 DEBUG 才能排查,那不是保留问题,是日志规范缺失。


5. 压缩:CPU 换空间,加一笔隐性成本

gzip 约 8–12 倍、CPU 低;zstd 约 10–15 倍、CPU 中。规则:压缩是离线动作,不要放在写入路径上,compress 在轮转后压已关闭文件,代价是轮转瞬间的 CPU 抖动;峰值已吃满 CPU 时用 delaycompress 挪到低峰。

隐性成本是回溯:压缩后要 zgrep 或先解压。判据:某个日志 7 天内被 grep 超过 3 次,就该让它以未压缩形态多留几天。


6. 容器日志:默认没有上限

容器日志是爆盘头号来源:默认配置里没有上限。

日志驱动 落盘位置 能否轮转 限制
json-file(默认) /var/lib/docker/containers/<id>/<id>-json.log 可(max-size/max-file) 不配就无限增长
local 同上,二进制格式 可,默认带轮转 可读性差,需工具
journald 宿主 journal 由 journald 策略管 策略易冲突
syslog/fluentd 远端 本地不落盘 依赖网络与接收端
# compose:单服务级限制,优先于 daemon 默认
services:
  app:
    logging:
      driver: json-file
      options: { max-size: "100m", max-file: "5" }   # 单容器上限 500M

两点必须记住:限制只对新写入生效,已存在的巨型 <id>-json.log 不会自动缩小,需重建容器或手工截断;docker logs 读同一个文件,限制它等于限制可回溯窗口——可接受的取舍,回溯由集中采集承担。全局兜底写在 daemon.json 的 log-opts,改完重启 docker。


7. 采集与本地保留的分工

本地保留与集中采集是分工:本地负责最近几小时的即时排障,远端负责几个月的回溯聚合。分清这点,本地保留期就能定得很短。

职责 本地文件 集中平台
刚出事时 tail -f 主 有延迟
跨服务关联 做不到 主
长期审计 成本高 主
采集器故障时兜底 主 无

采集器要显式配置本地缓冲上限,否则"远端不可用"会变成"本地缓冲爆盘"。判据:取盘容量 10%–20%,超出后丢弃并计数。


8. copytruncate 的坑:截断不改变写入偏移

# 模式 A:create —— 推荐,但要求进程能重开文件
/var/log/app/*.log {
    rotate 7
    create 0644 app app
    postrotate
        kill -USR1 $(cat /run/app.pid)
    endscript
}
差异 create copytruncate
inode 换新文件 不变
写入进程 必须支持重开(信号/句柄) 无需配合
数据丢失窗口 无(rename 原子) 复制与截断之间的写入会丢
适用 能改应用或支持信号 闭源/无法改的应用

copytruncate 是"先 cp 再 truncate 到 0",两次操作间的写入被丢弃,文件偏移被重置。O_APPEND 打开无害;带偏移写入(自研 logger、部分 NFS)会产生头部 N 字节变 \0 的空洞日志,wc -c 看不出,只有 grep -cP '\x00' 能发现。


9. 水位告警:阈值与提前量

水位 动作 提前量依据
70% 趋势告警(外推满盘时间) 留 24–48 小时余量
80% 值班告警 留清理决策时间
95% 页面/电话 + 限制非关键写入 数据库与日志最先受影响
# node_exporter + Prometheus:外推型比百分比更有决策价值
- alert: DiskWillFillIn4h
  expr: |
    node_filesystem_avail_bytes{mountpoint="/"}
    / deriv(node_filesystem_avail_bytes{mountpoint="/"}[1h]) < 4 * 3600
  for: 30m

静态阈值低增长期永不响、高增长期响得太晚;外推型("还剩几小时")直接给行动期限。前提是增长率稳定——一次 core dump 会让外推失效,故要保留百分比兜底。


10. 爆盘时的处置顺序

顺序 动作 为什么在这个位置
1 停非关键写入(调试日志、临时文件、批处理) 先止血,否则清理追不上增长
2 删可安全释放的数据(旧轮转日志、prune 镜像) 不涉及业务正确性
3 处理被进程持有的已删除文件 空间早已"删除"但未归还
4 归档并移走数据库日志(binlog 等) 需确认主从与恢复点
5
核心判断是先止血还是先清理:每分钟数 GB(日志风暴、循环报错)时清理追不上,必须先停写入源。先看两次 du 差值再定顺序。

11. 不可回收文件与容量规划

# 已删除但仍占空间的文件及其持有进程
lsof +L1 2>/dev/null | awk '$7 > 1073741824 {print $1, $2, $7, $9}'
ls -l /proc/*/fd/* 2>/dev/null | grep '(deleted)' | head

处置只有两条路:重启持有进程或 truncate -s 0 该 fd(仅对可写文件有效,可能破坏应用状态)。进程不关 fd 就不还空间——"清理脚本跑完磁盘还满"时该去看 fd,不是继续删文件。

容量规划的落地不是公式,而是给每个服务标一个日志预算:

日增量 = QPS_peak × 86400 × 行数_每请求 × 字节_每行
稳态   = 日增量 × 保留天数 × (1 - 压缩率)
盘预算 = 稳态 × 1.5 + 其他占用

这三个数写进部署清单,任一项变化走评审。超出预算的服务自己降级,而不是让整台机器的磁盘替它承担。


12. 排错:爆盘与日志异常

现象 根因 处理
df 满但 du 加起来远小于容量 被删除文件仍被进程持有 lsof +L1 找持有者,重启或 truncate
日志截断后头部出现 \0 copytruncate 与带偏移写入冲突 改 create + 信号重开,或强制 O_APPEND
容器日志几百 GB 无人管 未设 max-size/max-file 配 daemon 默认 + 单服务覆盖,重建容器生效

13. 边界:本地保留可以极简的情形

场景 合理做法
单机小站 按大小轮转 + 90% 水位告警
日志量极小(日增 < 100 MB) 按天留 30 天,不压缩
已有集中平台且本地不落盘 只配容器日志上限,保留交平台
合规要求原文留存 本地短期,长期走对象存储 + 生命周期
数据库节点 数据目录与日志目录分盘,绝不共用根分区

不能极简的组合只有三种:单盘同时承载数据与日志、容器日志用默认驱动、copytruncate 用在带偏移写入的自研 logger 上——这三种下爆盘只是时间问题。


14. 自测

  1. 为什么"加个清理脚本"解决不了线性增长的日志?
  2. 保留期由"多久要回溯"决定,还是由容量决定?
  3. copytruncate 在什么情况下会让日志头部出现 \0?
  4. df 与 du 差额很大时,第一步查什么?
  5. 外推型水位告警在什么场景下会失效?
  6. 容器日志已涨到几百 GB,配了 max-size 为什么没变小?
S15-A08 · 磁盘与日志:从爆盘到保留策略
https://www.clxhxhhr.top/posts/6217/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。