凌晨两点磁盘满,应用写不进日志、数据库拒绝写入、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. 自测
- 为什么"加个清理脚本"解决不了线性增长的日志?
- 保留期由"多久要回溯"决定,还是由容量决定?
copytruncate在什么情况下会让日志头部出现\0?df与du差额很大时,第一步查什么?- 外推型水位告警在什么场景下会失效?
- 容器日志已涨到几百 GB,配了
max-size为什么没变小?