一个没有配置日志轮转的服务,把一个几十 GB 的日志文件一路写到磁盘爆满,然后整台机器上的服务集体瘫痪:一次磁盘写满的深度复盘
那次半夜的告警来得猝不及防:一台机器上的好几个服务同时开始报错、崩溃,有的报"无法写入文件",有的数据库报"无法写入",还有的直接 502。最诡异的是,这几个服务彼此之间没什么关系,怎么会一起出事?我 SSH 上去,敲了一行 df -h,瞬间看清了元凶——磁盘使用率 100%,No space left on device(设备上没有空间了)。我顺着 du 一路往下查,找到了那个罪魁祸首:某个服务的一个日志文件,单个文件就有几十 GB,把整块磁盘几乎吃干净了。我盯着这个巨无霸日志文件,才终于明白了问题的根本,后背发凉:这个服务的日志,从上线那天起就一直在往同一个文件里追加,从来没有"轮转(rotation)"——既不按大小切分、也不按日期切分、更不删除旧日志。于是这个文件就无限地、一刻不停地、越长越大,跑了大半年,终于把磁盘写满了。而磁盘是整台机器共享的资源,一旦被写满,这台机器上所有需要写磁盘的东西都完蛋了:其他服务写不了日志、写不了临时文件,数据库写不了数据,系统也可能出各种问题——于是几个毫不相干的服务,被这一个"失控生长"的日志文件,一起拖下了水。这篇就把这次"日志没轮转、磁盘写满"的坑,从头到尾复盘一遍。
故障现场:一个无限增长、从不轮转的日志文件
问题不在某行代码,而在一个被遗漏的运维配置:
故障的来龙去脉:
1. 某服务的日志配置: 所有日志都往一个固定文件追加(如 /var/log/app/app.log);
2. 但【没有配置任何日志轮转(log rotation)】:
- 不按大小切分(写满100MB就换一个新文件);
- 不按日期切分(每天一个新文件);
- 不删除/归档旧日志(只保留最近N个/N天)。
3. → app.log 这个文件从上线起就一直追加, 一刻不停地变大;
4. 跑了大半年, app.log 长到了几十GB, 把磁盘写满;
5. 磁盘是【整台机器共享】的: 磁盘满了之后——
- 这个服务自己: 写不了日志(可能直接异常/卡住);
- 同机器的其他服务: 写不了日志/临时文件 → 报错/崩溃;
- 数据库: 写不了数据/binlog → 拒绝写入、甚至宕;
- 操作系统: /tmp满、各种写操作失败 → 系统级问题。
6. → 一个失控的日志文件, 通过"写满共享磁盘", 把整机的服务全拖垮了。
排查命令:
- df -h 看磁盘使用率(发现某分区100%);
- du -sh /* 或 du -h --max-depth=1 /var/log 逐层找出"谁占了这么大";
- → 定位到那个几十GB的日志文件。
# 关键: 日志/数据文件如果【只增不清、不轮转】, 迟早把磁盘写满; 而磁盘是整机共享资源,
# 写满会拖垮【整台机器上所有服务】——这是一种"资源耗尽型"的连锁故障。
第一次定位到这个几十 GB 的日志文件时,我又懊恼又后怕:"一个日志文件,居然能把整台机器的服务全搞挂?而且它就这么默默地长了大半年,没人发现?"这个坑最容易被忽视的地方在于它的缓慢与沉默:磁盘不是一下子满的,而是日积月累、缓慢地被填满——它潜伏期极长(可能几个月),平时一点征兆都没有;直到磁盘真的满了的那一刻,才突然、剧烈地爆发,而且爆发时波及整台机器(不只是肇事的那个服务)。另一个坑是它的连带性:肇事的是 A 服务的日志,遭殃的却是同机器的 B、C 服务和数据库——共享资源(磁盘)被一个组件耗尽,惩罚的是所有共享它的组件。下面就来拆解,日志轮转和磁盘管理。
第一件事:搞懂日志轮转,以及磁盘是共享资源
我顺着这次事故,把日志管理和磁盘这个共享资源彻底理清了。
日志轮转(log rotation) 与 磁盘作为共享资源
【核心: 日志只增不轮转迟早写满磁盘; 磁盘是整机共享资源, 一处写满拖垮全机; 必须给日志配轮转+保留策略】
1. 日志轮转(log rotation)是什么:
- 不让日志写进一个无限增长的文件, 而是【定期切分 + 保留有限份数 + 清理旧的】:
- 切分: 按大小(如每100MB)或按时间(如每天)生成新文件;
- 保留: 只保留最近N个文件 / 最近N天(超过的删除或压缩归档);
- → 这样日志总量被【限制在一个上限内】, 不会无限增长撑爆磁盘。
2. 为什么必须轮转:
- 服务会【持续不断】地产生日志, 是一个"只增"的数据流;
- "只增而不清理"的东西, 在有限的磁盘上, 【必然】迟早写满——只是时间问题;
- → 任何"持续产生、只增不减"的数据(日志、临时文件、上传文件), 都必须有"清理/上限"机制。
3. 磁盘是整机共享的有限资源:
- 一台机器(或一个磁盘分区)的空间是【所有服务共享】的;
- 一个服务把它写满 → 【所有】需要写盘的服务/进程都受影响(写日志、临时文件、DB、系统);
- → 这是"共享资源被一方耗尽, 殃及所有方"的典型(类似内存被一个进程吃光、连接被一方占满)。
4. 还要配监控:
- 磁盘使用率要有监控告警(如超过80%就告警), 在写满【之前】就介入;
- "等磁盘满了才发现"是最差的——那时已经造成连锁故障了。
类比: 日志文件像一个不停往里倒水、却没有出水口的水缸; 水(日志)持续地倒进来、从不排出,
水缸(磁盘)迟早溢出; 而这个水缸是和邻居们共用的, 一溢出, 大家的地都被淹了。
轮转就是给水缸装上"水位到了就排掉旧水"的机制。
一句话: 服务持续产生日志、只增不减必然写满磁盘; 磁盘是整机共享资源、写满会拖垮全机所有服务;
必须给日志配轮转(按大小/时间切分+保留有限份数)、并监控磁盘使用率、在写满前告警介入。
这套认知,是整个坑的根。日志轮转是什么:不让日志写进一个无限增长的文件,而是定期切分(按大小/时间)+ 保留有限份数 + 清理旧的,把日志总量限制在一个上限内、不撑爆磁盘。为什么必须轮转:服务持续产生日志、是"只增"的数据流,"只增不清理"在有限磁盘上必然迟早写满;任何"持续产生、只增不减"的数据(日志/临时文件/上传)都必须有清理/上限机制。磁盘是整机共享的有限资源:一个服务写满它,所有需要写盘的服务/进程都受影响(写日志、临时文件、DB、系统)——共享资源被一方耗尽殃及所有方。还要配监控:磁盘使用率超阈值就告警,在写满之前介入(等满了才发现是最差的)。就像日志文件是个不停倒水却没出水口的水缸,迟早溢出,而它和邻居共用,一溢出大家的地都被淹;轮转就是给水缸装上"水位到了就排旧水"的机制。一句话:服务持续产生日志、只增不减必然写满磁盘;磁盘是整机共享资源、写满会拖垮全机所有服务;必须给日志配轮转(按大小/时间切分+保留有限份数)、并监控磁盘使用率、在写满前告警介入。
第二件事:正解——配置日志轮转、保留策略,并监控磁盘
搞懂了原理,正解就清晰了:给日志配置轮转(应用日志框架内置滚动、或系统 logrotate)、限制总量(切分+保留有限份数+压缩归档)、监控磁盘使用率并告警;持续产生的数据都要有上限。
# ====== 正解一: 用日志框架内置的滚动策略(应用层) ======
# 以 logback(Java) 为例: 按大小+时间滚动, 并限制总大小和保留天数
#
#
# app-%d{yyyy-MM-dd}.%i.log.gz # 按天+序号, gz压缩
# 100MB # 单文件超100MB就切
# 15 # 最多保留15天
# 5GB # ★ 所有日志总大小上限5GB, 超了删最老的
#
#
# → 关键是 totalSizeCap/maxHistory: 给日志总量一个【硬上限】, 绝不无限增长。
# (其他框架: log4j2有类似的Rolling策略; Python logging有RotatingFileHandler/TimedRotating...)
# ====== 正解二: 用系统的 logrotate(运维层, 适合任何写文件的程序) ======
# /etc/logrotate.d/myapp
/var/log/app/*.log {
daily # 每天轮转
rotate 14 # 保留14份(14天)
size 100M # 或超过100M就轮转
compress # 旧日志压缩(节省空间)
delaycompress
missingok # 文件不存在不报错
notifempty # 空文件不轮转
copytruncate # 复制后清空原文件(适合无法重新打开文件句柄的程序)
}
# → logrotate由cron定期执行, 对任何往文件写日志的程序都适用, 是运维层的通用方案。
# ====== 正解三: 监控磁盘使用率 + 告警 ======
# - 监控各磁盘分区使用率, 超过阈值(如80%)就告警, 在写满【之前】介入;
# - 关键目录(日志/数据/临时)单独监控其增长趋势。
# ====== 正解四(容器环境): 别往容器内无限写文件 ======
# - 容器的可写层/emptyDir也有上限, 写满同样出问题;
# - 推荐: 日志输出到stdout/stderr, 交给容器日志驱动(json-file要配max-size/max-file!)或日志采集(EFK);
# docker的json-file默认【不限制大小】, 也要配 --log-opt max-size=100m max-file=5。
# ====== 排查口诀 ======
# 服务集体异常/写入失败 → 先 df -h 看磁盘! → du 找出谁占的 → 清理+配轮转。
# 核心: 给日志配轮转(框架内置滚动 或 系统logrotate), 限总量(切分+保留有限份数+压缩); 监控磁盘
# 使用率并告警; 容器里日志走stdout且配log driver上限; 任何"只增"的数据都要有清理/上限机制。
修复的核心,是"给日志(及一切只增数据)一个总量上限,并监控磁盘"。正解一:用日志框架内置滚动——logback 的 SizeAndTimeBasedRollingPolicy 按大小+时间切分,关键是 totalSizeCap/maxHistory 给日志总量一个硬上限、超了删最老的、压缩归档(各框架都有类似策略)。正解二:用系统 logrotate——daily/rotate/size/compress,cron 定期执行,对任何写文件的程序通用。正解三:监控磁盘使用率+告警(超 80% 就告警、写满前介入)。正解四(容器):别往容器内无限写文件——日志走 stdout/stderr 交给日志驱动(docker 的 json-file 默认不限大小、要配 max-size/max-file)或采集。排查口诀:服务集体异常/写入失败先 df -h 看磁盘,du 找出谁占的。归根结底:给日志配轮转(框架内置滚动或 logrotate)、限总量(切分+保留有限份数+压缩);监控磁盘使用率并告警;容器里日志走 stdout 且配 log driver 上限;任何"只增"的数据都要有清理/上限机制。
第三件事:磁盘与资源耗尽相关的其他常见坑
排查后我把磁盘/资源耗尽相关的其他常见坑也系统梳理了一遍。
磁盘 / 资源耗尽的其他常见坑
# 1. 日志不轮转(本文): 无限增长写满磁盘。→ 轮转+保留上限+监控。
# 2. 临时文件不清理: 程序生成的临时文件/上传文件只增不删 → 写满磁盘。→ 定期清理/设过期。
# 3. 容器日志驱动不限大小: docker json-file默认无上限。→ 配max-size/max-file。
# 4. inode耗尽: 大量小文件把inode用光(磁盘空间还有, 但创建不了新文件)。→ 监控inode。
# 5. 删了文件但进程还占着: rm了大日志但进程没关句柄, 空间不释放。→ 重启进程/truncate。
# 6. 数据库/中间件数据增长: binlog、WAL、慢查询日志、dump等只增不清。→ 设保留期。
# 7. 监控缺失: 没监控磁盘, 等满了才知道。→ 磁盘/inode使用率监控告警。
# 8. 资源没隔离: 多服务共享一块盘, 一个失控拖垮全部。→ 关键服务独立磁盘/配额(quota)。
# 共同根源: 任何"持续产生、只增不减"的东西(日志/临时文件/数据), 在"有限的共享资源"(磁盘)上,
# 若没有"清理/上限/隔离/监控"机制, 迟早会耗尽资源、并殃及所有共享者。
# 核心: 给一切"只增"的数据配清理/上限(轮转/过期/保留期); 监控有限资源(磁盘/inode)的使用率;
# 关键服务的资源做隔离/配额; 别让一个组件耗尽共享资源拖垮全机——资源管理是运维的基本功。
排查让我把资源耗尽的其他坑也梳理清了。一、日志不轮转(本文)。二、临时文件不清理。三、容器日志驱动不限大小(配 max-size)。四、inode 耗尽(大量小文件)。五、删了文件但进程还占着句柄(空间不释放)。六、数据库/中间件数据增长(binlog/WAL 设保留期)。七、监控缺失。八、资源没隔离(共享盘一个失控拖垮全部)。它们的共同根源是:任何"持续产生、只增不减"的东西,在"有限的共享资源"上,若没有"清理/上限/隔离/监控"机制,迟早会耗尽资源并殃及所有共享者。核心是:给一切"只增"的数据配清理/上限(轮转/过期/保留期);监控有限资源(磁盘/inode)使用率;关键服务的资源做隔离/配额;别让一个组件耗尽共享资源拖垮全机。下面这张图,是这次磁盘写满坑的成因与解法:
第四件事:磁盘写满排查命令速查表
这次踩坑后,我把"磁盘写满"的排查命令整理成一张表,下次照着查。
| 命令 | 作用 | 说明 |
|---|---|---|
| df -h | 看各分区使用率 | 第一步, 找出哪个分区满了 |
| df -i | 看inode使用率 | 空间还有但创建不了文件时看这个 |
| du -h --max-depth=1 /path | 看各子目录大小 | 逐层往下找"谁占的" |
| du -sh /path/* | sort -rh | head | 找出最大的几个 | 快速定位大文件/大目录 |
| lsof | grep deleted | 找被删但进程还占的文件 | rm了空间不释放时用 |
| ls -lh 大文件 | 确认大文件大小 | 找到肇事文件 |
这张表是我现在排查磁盘问题的"工具箱"。核心是:排查"磁盘写满"有一套固定的、由表及里的套路——df -h 先看哪个分区满了(空间)、df -i 看是不是 inode 满了(小文件太多)、du 逐层往下定位"谁占的"、lsof | grep deleted 排查"删了却没释放"的情况;从"整体现象"一步步收敛到"具体的肇事文件/目录"。它给我的最大启发是:面对一类常见故障,积累一套"标准排查流程(checklist/runbook)"极其有价值——"磁盘满了"这种故障,第一步必看 df、空间足但创建失败必看 inode、找大文件必用 du、空间不释放必查 lsof,这套流程一走,几分钟就能定位;而没有这套流程的人,可能会在慌乱中东查西查、浪费大量时间。这让我体会到运维的一个核心能力:"把常见故障的排查,沉淀成可复用的标准流程"——每种典型故障(磁盘满、CPU 高、内存泄漏、连接耗尽、服务无响应)都该有一套"先看什么、再看什么"的 runbook;故障来临时,有流程可循远胜于临时凭经验乱撞——它让排查变得快速、系统、可传承(新人照着也能查),也减少了慌乱中的遗漏。积累磁盘排查的标准命令、把常见故障沉淀成可复用的 runbook——是这个坑带给我的运维认知。
第五件事:这个坑暴露的"慢性问题"特征
这次让我意识到,磁盘写满是一类典型的"慢性病"。我把"慢性问题"和"急性问题"对比成表。
| 维度 | 慢性问题(本文) | 急性问题 |
|---|---|---|
| 发展 | 缓慢累积(几月) | 突然发生 |
| 潜伏期 | 长, 平时无感 | 短/无 |
| 例子 | 磁盘渐满、内存缓慢泄漏、连接缓慢堆积 | 突发崩溃、突发流量 |
| 爆发方式 | 到临界点突然全面爆发 | 即时 |
| 防范关键 | 趋势监控+提前告警 | 容错+快速响应 |
这张表道出了一类常被忽视的问题。核心是:磁盘渐满是典型的"慢性问题"——它缓慢累积、潜伏期很长(可能几个月)、平时毫无征兆,直到资源耗尽的临界点才突然全面爆发;这类问题(还有内存缓慢泄漏、连接缓慢堆积、数据缓慢膨胀)和"突发型急性问题"的防范方式完全不同——急性问题靠容错和快速响应,而慢性问题靠"趋势监控 + 提前告警"。它给我的深刻启发是:对待"缓慢累积型"的风险,关键不是"等它爆发时怎么救",而是"在它还没爆发时,怎么通过趋势就提前发现"——磁盘使用率从 50% 到 80% 到 95% 是有过程、有趋势的,只要监控这个趋势、在 80% 就告警,就能在写满前从容处理,而不是等 100% 时被连锁故障打个措手不及;"慢性问题最怕的就是'没监控、不知道它在缓慢恶化'"。这给了我一种监控的自觉:监控不能只盯"当前是否正常(瞬时值)",还要盯"趋势(在往哪个方向走、增长速度多快)"——对那些会"缓慢累积、终将耗尽"的资源(磁盘、内存、连接、文件句柄、数据量),要监控其增长趋势、设置提前量的告警阈值,在它逼近极限之前就介入;"用趋势监控提前发现慢性问题、把救火变成防火",是运维成熟度的一个重要标志。识别慢性问题的特征、用趋势监控和提前告警在爆发前介入——是这个磁盘坑带给我的更高层认知。
第六件事:上线一个会写文件的服务时,我现在的检查习惯
现在每当我上线一个会持续写日志/文件的服务,我都会按这张图把"只增数据"这块过一遍:
这张图的精髓,是"会写文件就先确认日志轮转、总量上限、磁盘监控"。会持续写日志/文件就依次确认:配了轮转、有总量上限(totalSizeCap/rotate N)、容器里走 stdout 且配 log driver 上限、磁盘 inode 监控告警、其他只增数据(临时/上传/dump)也配清理。这套习惯,让我从"上线只管服务能跑"变成了"上线先确认它产生的数据会不会撑爆磁盘"——核心始终是:任何只增的数据都要有清理/上限,日志必配轮转+总量上限,并监控磁盘。
我立下的几条规矩
这场"日志没轮转、磁盘写满"的事故,换来了我做服务和运维时,刻进骨子里的几条铁律:
- 日志必须配轮转。按大小/时间切分,绝不让它写进一个无限增长的文件。
- 日志要有总量上限。totalSizeCap/maxHistory/rotate N,超了删最老的、压缩归档。
- 磁盘是整机共享资源,写满拖垮全机。一个组件耗尽它,所有共享者遭殃。
- 容器日志走 stdout,且配 log driver 上限。docker json-file 默认不限大小。
- 监控磁盘和 inode 使用率,提前告警。在写满之前(如 80%)就介入。
- 一切"只增"的数据都要有清理/上限。日志、临时文件、上传、binlog、dump。
- 磁盘满先 df -h,服务集体异常先想共享资源。积累标准排查 runbook。
写在最后
回头看,这场由"一个日志文件无限增长"引发的、整机服务集体瘫痪的事故,真正教给我的,远不止"日志要配轮转"这一个运维技巧。它让我对"任何'持续产生、只增不减'的东西,放在'有限的空间'里,都是一颗定时炸弹",有了一次刻骨的体会。我栽跟头,根源在于我只关注了日志的"产生"、却完全没有为它的"消亡"做任何安排。我写代码时想的是"怎么把日志记下来(产生)",理所当然地以为"记下来就完事了",却从没想过"这些日志会一直累积、谁来清理、它的总量有没有上限"。我把日志当成了一个"只管产生、无需管理"的东西,可它实际是一个"持续流入、却没有流出"的累积过程;一个只进不出的过程,在一个有限的容器(磁盘)里,结局是注定的——迟早溢出。这让我领悟到一个关于"持续性过程"的深刻认知:很多东西不是"一次性的动作",而是"持续进行的过程"(记日志、存数据、建连接、占内存、生成文件)——对这类过程,不能只设计它的"产生/增长"端,必须同时设计它的"清理/消亡/上限"端;"有生就要有灭、有进就要有出、有增长就要有上限"——一个只有"产生"而没有"回收"的过程,在有限的资源里,必然会走向资源耗尽。这给了我一种设计"持续性系统"的完整视角:设计任何会"持续累积"的东西时,都要在源头就同时回答"它怎么增长、增长到哪为止、超了怎么清理"这一整套问题——日志要轮转、缓存要过期、连接要回收、临时数据要清理、内存要释放;"为'增长'配上'边界和回收'",是设计一切运行在有限资源上的持续性系统的基本功——因为资源总是有限的,而"持续增长"在有限资源里,从来都不是可持续的。认清持续产生的数据必须配清理与上限、为增长设计边界和回收——这,是我用一次磁盘写满的事故,换来的、关于 DevOps、也关于如何设计一切持续性系统的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次上线一个会写日志的服务时,顺手就把日志轮转和总量上限配上,那我那个被磁盘告警叫醒的半夜,就没白熬。
—— 别看了 · 2026