日志撑爆磁盘服务全线崩:磁盘写满避坑复盘

一个安静的凌晨告警把我从睡梦炸醒:核心服务全线挂了,不是某个接口慢而是所有功能一起瘫痪,下单失败、查询报错、连健康检查都过不了。迷迷糊糊登上服务器想看日志,第一条命令就当头一棒:cannot create temp file: No space left on device,磁盘满了,df -h 一看根分区赫然 100% 一个字节空闲都没有。可这台机器磁盘几百 G 平时用量才三十几,怎么一夜就被塞满?du 一层层往下扒真凶现形:应用日志目录一个单独的日志文件膨胀到一百多 G——前一天上线的版本触发了一段疯狂打日志的逻辑(某个异常被反复捕获又反复打印),日志像决堤洪水一整晚往那文件灌,而那文件从来没配过日志轮转,只会一个劲变大没有上限没有切分直到把整块磁盘撑爆。磁盘一满灾难连锁:应用写日志失败抛异常、数据库写数据和 binlog 失败事务失败、系统连临时文件都建不了,整台服务器被一个失控日志文件以磁盘写满的方式推向全面瘫痪。这篇文章从这次日志撑爆磁盘的事故出发,讲透磁盘空间管理:磁盘满为何让整个系统瘫痪、用 logrotate 与应用滚动策略配轮转、totalSizeCap 总量上限、删大文件空间却不释放的句柄坑要用 truncate、磁盘空间与 inode 提前告警、应急救火流程,以及从源头控量与日志集中化。

一个安静的凌晨,告警把我从睡梦里炸醒:核心服务全线挂了。不是某个接口慢,而是所有功能一起瘫痪——下单失败、查询报错、连健康检查都过不了。我迷迷糊糊登上服务器想看日志,结果第一条命令就给了我当头一棒: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.logapp.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),还要监控 inodeinode 是文件系统用来记录文件元信息的资源,数量有限;如果你的系统产生了海量的小文件(比如缓存碎片、临时文件),可能空间还没满、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)更安全、更立竿见影。到这儿,这次事故的方方面面就齐了。我把应对思路收成一张决策图:

把这套理解建立起来,"磁盘写满"这类事故就能被预防、被快速处理。最后,拧成几条可直接照做的铁律:

  1. 所有日志必须配轮转,用 logrotate 或应用框架的滚动策略, 杜绝单文件无限膨胀。
  2. 一定要设日志总量上限(totalSizeCap),这是防日志撑爆磁盘最直接的硬线。
  3. 轮转要按"时间 + 大小"双触发,光按天不够, 防不住一天内的疯狂暴涨。
  4. 删活跃大文件用 truncate 清空, 别用 rm,rm 会遇到"句柄占用、空间不释放"的坑。
  5. 磁盘用量提前监控告警,80%~85% 就提醒, 别等 100% 才被半夜炸醒。
  6. 别忘了监控 inode,海量小文件会让 inode 先于空间耗尽, 同样报磁盘满。
  7. 救火先止血再治本,先 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

偶发 connection reset:连接池复用死连接避坑

2026-5-30 12:51:00

技术教程

放量就 429 账单还暴涨:大模型 API 生产化避坑

2026-5-30 13:05:08

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索