一个安静的凌晨,告警把我从睡梦里炸醒:核心服务全线挂了。不是某个接口慢,而是所有功能一起瘫痪——下单失败、查询报错、连健康检查都过不了。我迷迷糊糊登上服务器想看日志,结果第一条命令就给了我当头一棒:bash: cannot create temp file: No space left on device。磁盘满了。df -h 一看,根分区赫然写着 100%,一个字节的空闲都没有。
问题是,这台机器的磁盘有几百 G,平时用量才百分之三十几,怎么一夜之间就被塞满了?我用 du 一层层往下扒,真凶很快就现了形:应用的日志目录,一个单独的日志文件,膨胀到了一百多 G。原来前一天上线的一个版本,不知怎么触发了一段疯狂打日志的逻辑(可能是某个异常被反复捕获又反复打印),日志就像决堤的洪水,一整晚不停地往那个文件里灌。而那个文件,从来没有被配置过"日志轮转(log rotation)"——它只会一个劲地变大,没有任何上限、没有任何切分,直到把整块磁盘活活撑爆。
磁盘一满,灾难就连锁了:应用想写新日志,写不进去,抛异常;数据库想写数据文件、写 binlog,写不进去,事务失败;系统连临时文件都创建不了,各种基础操作开始报错——整台服务器,被一个失控的日志文件,以"磁盘写满"的方式,推向了全面瘫痪。这就是运维里一个朴素、却极其致命、又极其常见的事故:日志没有轮转,把磁盘写满,拖垮整个系统。这篇文章,就从这次"日志撑爆磁盘"的事故出发,把磁盘空间管理、日志轮转、以及相关的运维防护,一次讲透。
先摆几个关于磁盘和日志的想当然
动手复盘前,先把我自己曾经深信、后来被这一夜教育的几个念头摆出来。
| 想当然的念头 | 残酷的真相 |
|---|---|
| "磁盘几百 G, 用不满的, 不用管" | 一段失控的日志, 一夜就能把它灌满 |
| "日志就一直写一个文件, 挺好" | 不轮转的日志文件会无限膨胀, 直到撑爆磁盘 |
| "磁盘满了顶多写不了日志, 没大碍" | 数据库、临时文件、系统全写不了, 会全面瘫痪 |
| "删掉大日志文件就能立刻腾出空间" | 若有进程还开着它, 删了空间也不释放 |
| "磁盘用量不用监控, 满了再说" | 满了就是事故现场, 必须提前告警、提前防 |
这些念头的共同病根,是把"磁盘空间"当成了一种近乎无限、无需操心的资源,却忘了它和 CPU、内存、连接数一样,是一种有限的、会被耗尽的资源;而一旦耗尽,它的破坏力是"全局性"的——因为几乎所有的系统活动,都直接或间接地依赖"能往磁盘写东西"。要看清这次事故,得先理解磁盘写满为什么会引发如此全面的崩溃。
第一件事:磁盘写满,为什么会让整个系统瘫痪
很多人对"磁盘满"的危害估计不足,以为顶多是"存不下新文件"。但实际上,无数看似和"存文件"无关的系统活动,底层都依赖"能写磁盘"。应用要写日志、数据库要写数据和事务日志(binlog/WAL)、系统要创建临时文件(/tmp)、各种进程要写 socket 文件、锁文件、PID 文件……一旦磁盘满了,这些写操作全部失败,而很多程序在"写失败"时的处理是抛异常、甚至直接崩溃。于是一个"磁盘满",就像抽掉了地基,让建在其上的一切应用层层倒塌。
在我这次事故里,连锁反应是这样的:失控的日志文件先把磁盘填满 → 应用写日志失败开始抛异常 → 数据库无法写入数据文件和 binlog,事务全部失败 → 系统无法创建临时文件,连一些基础命令都执行不了 → 健康检查也因为各种写操作失败而过不了 → 整个服务被判定为不健康、彻底瘫痪。下面这张图,把这个连锁崩溃画出来:
看懂这张图,事故的严重性就清楚了:磁盘满之所以致命,是因为它打击的不是某一个功能,而是所有功能赖以运行的共同基础——往磁盘写数据的能力。这也解释了为什么现象是"全线崩溃"而非"某个接口出错"。磁盘空间,是那种平时毫无存在感、一旦耗尽就会瞬间瘫痪全局的'隐形地基'。接下来,我们就看怎么守住这块地基。
第二件事:配置日志轮转——给日志文件套上"上限"
根治这个问题的核心,是日志轮转(log rotation):让日志文件不再无限增长,而是按大小或时间切分成多个文件,保留有限的份数,超出的自动删除。这样日志占用的磁盘空间,就被牢牢限制在一个可控的范围内,再怎么疯狂打,也撑不爆磁盘。Linux 上最经典的工具是 logrotate。
# /etc/logrotate.d/myapp —— 给应用日志配置轮转规则
/var/log/myapp/*.log {
daily # 每天轮转一次
rotate 7 # 保留最近 7 份, 更老的自动删除
size 500M # 或者单文件超过 500M 就立刻轮转(防一夜暴涨)
compress # 旧日志压缩存储, 省空间
delaycompress # 延迟一轮再压缩(避免压到正在写的)
missingok # 日志文件不存在也不报错
notifempty # 空文件不轮转
copytruncate # 关键!复制后清空原文件, 而非重命名
# copytruncate 让正在写这个文件的进程, 不用重启就能继续写新内容
}
# 手动测试配置是否生效(-d 是 debug 演练, 不真正执行)
logrotate -d /etc/logrotate.d/myapp
logrotate -f /etc/logrotate.d/myapp # -f 强制执行一次, 验证
这里有个关键参数 copytruncate 值得专门说:日志轮转有两种方式,一种是把当前日志重命名(app.log → app.log.1)再新建一个空的——但这要求写日志的进程能感知到、重新打开新文件,否则它会继续往那个被重命名的旧文件句柄里写(这正是后面要讲的"删了文件空间却不释放"的同源问题)。另一种是 copytruncate:先把内容复制一份出去,再把原文件清空(truncate)——原文件的句柄不变,进程毫无察觉地继续往这个"被清空了的"文件里写。对于那些不支持"重新打开日志文件"信号的应用,copytruncate 是最省心的选择。
同时注意我配置里的 size 500M:它让"单个文件超过 500M 就立刻轮转",这是防范我那次"一夜暴涨"的关键——光靠 daily(每天才轮转一次)是不够的,如果日志在一天之内疯狂暴涨,等不到第二天的轮转,磁盘早就满了。按时间 + 按大小双重触发,才能既定期清理、又防住突发的暴增。
第三件事:应用层也配好滚动策略,双重保险
除了系统级的 logrotate,现代的日志框架(Logback、Log4j2 等)本身也内置了滚动(rolling)策略。在应用层把日志的大小、份数、总量管好,是更贴近应用、也更可靠的一道防线——它不依赖外部的 logrotate 是否配对,而是应用自己就管住了自己的日志。
<!-- Logback 配置:按大小+时间滚动, 并限制总占用空间 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 按天 + 按大小滚动 -->
<fileNamePattern>/var/log/myapp/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>500MB</maxFileSize> <!-- 单文件上限 -->
<maxHistory>7</maxHistory> <!-- 保留 7 天 -->
<totalSizeCap>10GB</totalSizeCap> <!-- 关键!所有日志总量上限 10G -->
</rollingPolicy>
<encoder><pattern>%d %-5level %logger - %msg%n</pattern></encoder>
</appender>
这里最该配、也最容易被忽略的是 totalSizeCap(总量上限):它给"这个应用的所有日志加起来能占多少磁盘"画了一条硬线,超过就删最老的。有了它,哪怕日志疯狂暴涨,占用的空间也绝不会超过你设定的上限——这正是我那次事故缺失的那道关键防线。系统级 logrotate + 应用级滚动策略,两道防线都配上,才是真正的双保险;尤其 totalSizeCap 这种'总量硬上限',是防止日志撑爆磁盘最直接有效的一招。别只配单文件大小,一定要配总量上限。
第四件事:一个反直觉的坑——删了大文件,空间却没释放
事故现场救火时,我撞上了一个让人崩溃的坑:我找到那个一百多 G 的日志文件,rm 一删,满心以为空间会立刻回来,结果 df 一看——磁盘还是 100%!文件明明没了,空间却纹丝不动。这背后是 Linux 一个重要的机制:如果一个文件被删除时,还有进程"开着"它(持有它的文件句柄),那么这个文件占用的磁盘空间不会被真正释放,直到那个进程关闭句柄或退出。
# 现象:删了大文件, df 显示空间却没释放
rm /var/log/myapp/huge.log
df -h # 还是 100%! 因为还有进程开着这个被删的文件
# 排查:找出"还开着已删除文件"的进程
lsof | grep deleted # 列出所有被删除但仍被占用的文件
lsof +L1 # 或这样, 看链接数为 0(已删)却还开着的文件
# 输出会告诉你: 是哪个进程(PID)还攥着这个已删文件的句柄
# 真正释放空间的两种办法:
# 办法一(治本):让持有它的进程释放句柄
# - 重启那个应用进程, 或让它重新打开日志文件(reload/发信号)
# 办法二(救急, 不删文件直接清空):用重定向清空文件内容
: > /var/log/myapp/huge.log # 把文件清空但不删除, 进程句柄还在, 继续写
truncate -s 0 /var/log/myapp/huge.log # 同样是清空, 空间立即释放
这个坑的教训是:救火时,对正在被进程写入的大日志文件,别用 rm 删,而要用 : > 文件 或 truncate -s 0 把它"清空"。清空不会改变文件句柄,进程毫无察觉地继续往这个空文件里写,而被清掉的内容占用的空间会立即释放。rm 反而会让你陷入"文件没了、空间还在、还得去找哪个进程开着它"的尴尬。这也正好解释了为什么前面 logrotate 要用 copytruncate——它就是"清空"而非"删除重建",从而避开了这个句柄问题。
第五件事:磁盘用量必须监控 + 提前告警
修好日志轮转是治本,但更重要的是:磁盘用量必须被监控,并在远没满之前就告警。磁盘满是一种"渐进式"的资源耗尽——它不会突然发生,总有一个慢慢爬升的过程。这意味着,只要你盯着磁盘使用率的曲线、设好阈值告警,完全可以在它逼近危险线、引发事故之前的几小时甚至几天,就收到提醒、从容处理,而不是等它满了、半夜被告警炸醒。
# 监控磁盘使用率, 在 80% / 90% 就告警, 别等 100%
df -h | awk '$5+0 > 80 {print "WARN: " $6 " 使用率 " $5}'
# 别忘了还要监控 inode! 磁盘空间没满, 但 inode 用尽同样会"No space left"
df -i # 查看 inode 使用率(海量小文件会先耗尽 inode)
# 一个简单的磁盘告警脚本(可放进 cron 定时跑)
USAGE=$(df / | tail -1 | awk '{print $5+0}')
if [ "$USAGE" -gt 85 ]; then
send_alert "磁盘 / 使用率已达 ${USAGE}%, 请尽快处理"
fi
# 接入 Prometheus 等监控系统, 对 node_filesystem_avail 设告警更专业
这里要特别提一个容易被忽略的点:除了磁盘空间(block),还要监控 inode。inode 是文件系统用来记录文件元信息的资源,数量有限;如果你的系统产生了海量的小文件(比如缓存碎片、临时文件),可能空间还没满、inode 却先用尽了,同样会报 "No space left on device",而 df -h 看空间还很充足,让人一头雾水。所以磁盘监控要"空间 + inode"双管齐下。设好提前告警(比如 80% 就提醒),是把'磁盘满'这种事故,从'半夜爆发的灾难'变成'白天从容处理的小事'的关键。
第六件事:事故应急——磁盘满了怎么快速救火
万一还是满了,得知道怎么快速止血。核心流程是:先快速定位是谁占了空间,再安全地腾出空间,最后恢复服务。
# 1. 快速定位:找出占空间最多的目录/文件
df -h # 先看哪个分区满了
du -h --max-depth=1 / 2>/dev/null | sort -rh | head # 逐层找大目录
du -sh /var/log/* | sort -rh | head # 找最大的日志文件
# 2. 安全腾空间(优先清空而非 rm, 避免句柄占用):
truncate -s 0 /var/log/myapp/huge.log # 清空失控的大日志
# 清理旧的、可删的:压缩包、临时文件、过期日志
find /var/log -name "*.gz" -mtime +7 -delete # 删 7 天前的压缩日志
# 3. 如果是已删文件占用(空间没释放), 找进程重启或清空:
lsof | grep deleted
# 4. 腾出空间后, 重启受影响的服务, 恢复正常
# 5. 事后:配好 logrotate/totalSizeCap + 监控告警, 杜绝复发
应急的要点,是"先止血、再治本":第一时间用 truncate 清空那个失控的大文件、删掉可删的旧文件,把空间腾出来让服务先恢复;然后再冷静地配好轮转和监控,从根上杜绝复发。救火时千万别慌乱地 rm 一通,尤其别删那些可能正被进程写着的活跃文件——清空(truncate)比删除(rm)更安全、更立竿见影。到这儿,这次事故的方方面面就齐了。我把应对思路收成一张决策图:
把这套理解建立起来,"磁盘写满"这类事故就能被预防、被快速处理。最后,拧成几条可直接照做的铁律:
- 所有日志必须配轮转,用 logrotate 或应用框架的滚动策略, 杜绝单文件无限膨胀。
- 一定要设日志总量上限(totalSizeCap),这是防日志撑爆磁盘最直接的硬线。
- 轮转要按"时间 + 大小"双触发,光按天不够, 防不住一天内的疯狂暴涨。
- 删活跃大文件用 truncate 清空, 别用 rm,rm 会遇到"句柄占用、空间不释放"的坑。
- 磁盘用量提前监控告警,80%~85% 就提醒, 别等 100% 才被半夜炸醒。
- 别忘了监控 inode,海量小文件会让 inode 先于空间耗尽, 同样报磁盘满。
- 救火先止血再治本,先 truncate 腾空间恢复服务, 再配好轮转与监控杜绝复发。
一张磁盘救火速查表
把磁盘相关的现象、成因和对策汇成一张表,磁盘告警时对照着处理。
| 现象 | 成因 | 对策 |
|---|---|---|
| 服务全线崩, df 显示 100% | 磁盘空间被写满 | truncate 大文件腾空间, 配轮转 |
| 单个日志文件上百 G | 日志没轮转, 无限膨胀 | logrotate + totalSizeCap |
| rm 删了大文件空间没释放 | 进程仍持有文件句柄 | truncate 清空 / 重启进程 |
| 空间没满却报 No space | inode 被海量小文件耗尽 | df -i 查 inode, 清理小文件 |
| 一天内突然写满 | 异常刷屏 + 只按天轮转 | 加按大小(size)触发轮转 |
| 满了才被半夜告警 | 没有提前阈值告警 | 80%~85% 就告警 |
更进一步:从源头管住"日志"这件事
修好轮转和监控后,我又往源头想了一层:这次失控,根子上是日志本身被打得太疯。所以除了"管住日志占的空间",更要"管住日志产生的量"。我做了几件事。其一,审查日志级别:生产环境别开 DEBUG,把那些循环里、高频路径上的冗余日志降级或删掉——很多"日志暴涨",根源是某个本不该在生产打、或不该打这么频繁的日志。其二,对异常日志做限流:像我这次"异常被反复捕获反复打印"的情况,可以对同一类异常的日志做采样或限流,别让一个异常在一秒内打几万条。
其三,也是更现代的做法,是日志集中化:把日志通过采集器(如 Filebeat、Fluentd)实时收集到专门的日志系统(ELK、Loki 等),本地只做短期缓冲。这样既便于检索分析,也让"单机磁盘"不再是日志的最终归宿——本地存不了多少、也不怕它撑爆。当然,集中化之后,本地的缓冲日志依然要配轮转(采集器万一挂了,日志还是会在本地堆积),双保险不能少。
# 日志治理的几个层次, 从源头到归宿都管起来:
# 1. 源头控量:生产日志级别设 INFO/WARN, 对高频异常日志限流采样
# 2. 本地轮转:logrotate + 应用 totalSizeCap, 兜住本地磁盘
# 3. 集中采集:Filebeat/Fluentd 实时收走, 本地只做短期缓冲
# 4. 监控告警:磁盘空间 + inode, 提前到 80% 告警
# 核心思想:日志要"产出可控、本地有界、及时收走、用量可见"
这套"从产出到归宿"的全链路治理,才是对日志真正负责任的态度。日志是排查问题的命脉,我们当然需要它;但它也是一种会占用磁盘、会失控暴涨的资源,需要被认真地管理。好的日志治理,是在"记录得足够、便于排查"和"产出可控、不撑爆磁盘"之间,找到一个被精心维护的平衡。
写在最后
这次"日志撑爆磁盘"的事故,给我最深的感触,是它把一个朴素到近乎"不值一提"的道理,以一种惨烈的方式刻进了我心里:再不起眼的资源,也是有限的;再低级的疏忽,在合适的条件下,也能酿成全局性的灾难。"给日志配个轮转"这件事,简单到几乎没有任何技术含量,是运维入门的第一课;可正是这样一件"谁都知道该做"的小事,因为在某次部署中被遗漏、又恰好撞上一段失控的日志,就把整个核心服务在一个凌晨彻底掀翻。最大的事故,常常不是源于多么高深的技术难题,而是源于这些被我们认为"太基础、不会出错、不用专门检查"的地方。
这也让我对运维工作多了一份敬畏。我们常常把目光投向那些光鲜的架构、精巧的算法,却容易忽略,系统的稳定,在很大程度上是由这些"不性感"的基础保障默默撑起来的——日志轮转、磁盘监控、资源告警、备份恢复……它们平时毫无存在感,可一旦缺失,整座大厦就可能因为一块小小的、被忽视的地基塌陷而轰然倒塌。这和这个系列里反复出现的主题再次共鸣:真正的健壮,藏在对每一种有限资源(无论是连接、内存、还是磁盘)的敬畏里,藏在对每一处看似平凡的基础保障的认真里。这次教训于我,是一个永久的提醒:别因为一件事"太基础"就轻慢它、跳过它——因为往往就是这些最基础的防线,在某个我们意想不到的深夜,默默地决定着系统的生与死。愿你我都能俯下身来,把每一块"隐形的地基"都夯得结结实实,让那些本可避免的凌晨告警,永远不要响起。
如果你手上也管着服务器,不妨今天就花二十分钟做三件小事:确认每个应用的日志都配了轮转和总量上限、给磁盘空间和 inode 都设上 80% 的提前告警、再演练一遍"磁盘满了怎么 truncate 救火"。这三件最朴素的小事,就是你系统在某个深夜的安稳底气。
—— 别看了 · 2026