服务突然大面积报错、写文件上传写库全线失败,登上机器 df 一看磁盘竟然满了:日志从上线起从没切割清理、悄悄撑爆整块磁盘的全线崩溃避坑复盘

这是一次让我印象极深的全线崩溃,它崩得又突然又广,广到我一度以为服务器中毒了。某天下午我们的服务毫无征兆地开始大面积报错,而且错误五花八门毫无规律:有的请求报写文件失败,有的报上传失败,甚至连数据库写入都开始报错,日志里 No space left on device 设备上没有剩余空间这行字疯狂刷屏。我懵了:这几个功能八竿子打不着怎么会同时出问题?直到我登上服务器敲下那个运维排查的第一反应命令 df -h,真相瞬间大白也让我哭笑不得:磁盘满了使用率100%。一个磁盘被塞满的服务器任何需要写入磁盘的操作都会失败,这就是为什么那么多八竿子打不着的功能会同时崩溃。而我顺着 du 命令一层层查下去找到了吃光磁盘的元凶——我们应用的日志目录占了几百GB!原来日志从服务上线起就一直在写越积越多,却从来没有做过切割和清理,几个月下来就像不断膨胀的气球悄无声息把整块磁盘撑爆了。这篇文章从这次日志撑爆磁盘导致全线崩溃的事故出发,讲透磁盘与日志避坑:df du ls 排查磁盘的命令组合拳、日志必须切割加清理加总量封顶、磁盘水位必须监控告警别等满了才知道、别让日志成为沉默的磁盘杀手、已删除文件未释放与 inode 耗尽等反直觉陷阱、把资源水位纳入日常监控,以及一个根本认知——最朴素的隐患往往造成最惨烈的事故,要对朴素的基础怀有最大的敬畏。

这是一次让我印象极深的"全线崩溃"——它崩得又突然、又广,广到我一度以为是服务器中毒了。某天下午,我们的服务毫无征兆地开始大面积报错,而且错误五花八门、毫无规律:有的请求报"写文件失败",有的报"上传失败",甚至连数据库写入都开始报错,日志里 No space left on device(设备上没有剩余空间)这行字疯狂刷屏。我懵了:这几个功能八竿子打不着,怎么会同时出问题?是哪个公共组件挂了吗?

直到我登上服务器,敲下那个运维排查的"第一反应"命令 df -h,真相瞬间大白,也让我哭笑不得:磁盘满了,使用率 100%。一个磁盘被塞满的服务器,任何需要"写入磁盘"的操作都会失败——写日志失败、存上传的文件失败、数据库要写数据失败……这就是为什么那么多八竿子打不着的功能会"同时崩溃":它们崩溃的根源是同一个,都是"磁盘没空间了,写不进去了"。而我顺着 du 命令一层层查下去,找到了那个吃光磁盘的元凶——我们应用的日志目录,占了几百 GB!原来,我们的日志从服务上线起就一直在写、越积越多,却从来没有做过"切割"和"清理";几个月下来,这些日志文件就像不断膨胀的气球,悄无声息地把整块磁盘给撑爆了。这篇文章,就从这次"日志撑爆磁盘导致全线崩溃"的事故讲起,聊聊"磁盘空间"这个最不起眼、却足以让整个服务瘫痪的运维隐患。

故障现场:被日志"撑爆"的磁盘

先还原一下排查的过程,这套"排查磁盘问题"的命令组合拳,值得每个工程师记住:

# 第1步: 服务大面积报错、尤其见到 "No space left on device", 第一反应:
df -h
# 输出里某个分区 Use% 是 100% → 磁盘满了, 真相已现一半
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/vda1       200G  200G    0G 100% /        ← 满了!

# 第2步: 找出是谁吃光了磁盘 —— 从根目录逐层往下查最大的目录
du -h --max-depth=1 / | sort -rh | head    # 看根下哪个目录最大
du -h --max-depth=1 /www/logs | sort -rh | head  # 顺藤摸瓜往下钻
# 最终定位: /www/logs/app/  占了 300G ! ← 元凶: 应用日志

# 第3步: 看看是哪些日志文件这么大
ls -lhS /www/logs/app/ | head    # 按大小排序, 看最大的日志文件
# app.log  280G  ← 一个从未切割过的日志文件, 撑了大半年

这套 dfduls 的排查路径,清晰地揭示了真相:df -h 告诉我们"磁盘满了",du 一层层往下钻找到"是日志目录占的",ls -lhS 最后定位到"是某个从未切割的巨型日志文件"。我们犯的错误很典型:把日志一股脑地往一个文件里写,既没有按天/按大小"切割"成多个文件,也没有"清理"掉过期的旧日志——于是这个文件就像一个永远只进不出的容器,随着时间推移无限膨胀,直到把磁盘的最后一点空间也吃光。

而磁盘写满的后果是灾难性的、且全面的:因为现代服务几乎处处都依赖"写磁盘"——记日志要写、保存上传文件要写、数据库持久化数据要写、甚至很多临时操作都要写临时文件——所以磁盘一满,这些操作就会全部、同时地失败,表现出来就是"各种八竿子打不着的功能一起崩溃"。这也是磁盘满这类故障的迷惑性所在:它的表象是"到处都在崩、毫无规律",让你一头雾水地去查每一个崩溃的功能;而它的根因,其实是那个最朴素、却最容易被忽视的"磁盘没空间了"。记住这个排查直觉:当服务出现"大面积、无规律、涉及多个不相关功能的崩溃",尤其日志里有 No space left 时,第一件事就是 df -h 看磁盘——这一下,往往就拨云见日了。

第一件事:日志必须"切割 + 清理",别让它无限增长

找到了"日志无限膨胀"这个病根,最直接的解药就是给日志加上切割(rotation)和清理(retention):别让所有日志都堆在一个永不停止增长的文件里,而是按时间(每天一个)或大小(每超过 100MB 切一个)把它切成一个个文件,并且只保留最近 N 天/N 个,超过的自动删除。

# 方案1: 用 logrotate(Linux 自带的日志切割工具)配置
# /etc/logrotate.d/myapp
/www/logs/app/*.log {
    daily               # 每天切割一次
    rotate 14           # 只保留最近 14 天的, 更早的自动删除
    compress            # 旧日志压缩, 进一步省空间
    missingok
    notifempty
    copytruncate        # 切割时清空原文件(应用无需重启)
}

# 方案2: 应用日志框架自带滚动(推荐, 应用层直接控制)
# Logback(Java)示例: 按天 + 按大小滚动, 自动保留策略
#   
#     app.%d{yyyy-MM-dd}.%i.log
#     100MB     
#     14          
#     10GB    
#   

这两种方案都能根治"日志无限膨胀":方案一(logrotate)是 Linux 系统层面的通用工具,不管你的应用是什么语言写的,都能用它来切割、压缩、清理日志文件,适合给那些"自己不带日志滚动功能"的程序兜底。方案二(日志框架自带滚动)则是在应用层面直接配置——现在主流的日志框架(Java 的 Logback/Log4j2、Python 的 logging 等)都内置了"按时间/大小滚动 + 自动清理 + 总量封顶"的能力,这是更推荐的做法,因为它和应用结合得更紧、控制更精细。无论用哪种,核心都是给日志立下三条规矩:一要切割(分成多个文件)、二要限量(只保留最近的)、三最好压缩(省空间)。尤其那个"总量封顶"(totalSizeCap)的配置特别有用——它给日志的总大小设了一个硬上限,从根本上保证日志再怎么写,也吃不光磁盘。把这条配上,日志撑爆磁盘的事就基本绝迹了。

第二件事:磁盘水位必须监控告警——别等满了才知道

切割清理解决了"日志膨胀"这个具体的吃磁盘大户,但更根本的一道防线是:对磁盘使用率做监控告警。因为吃磁盘的不止日志,还可能是临时文件、上传文件、数据库文件、core dump 等等;你永远没法穷尽所有"吃磁盘的可能",但你可以盯住一个总的指标——磁盘使用率,在它快满之前就告警,给自己留出处理的时间,而不是等它 100% 满了、服务全崩了,才被动地发现。

# 监控磁盘使用率, 超阈值告警(可用 Prometheus/Zabbix, 或最朴素的脚本+cron)
#!/bin/bash
# 取根分区使用率(去掉%号)
USE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USE" -ge 80 ]; then
    # 超过 80% 就告警 —— 留足处理时间, 别等 100%
    send_alert "磁盘使用率已达 ${USE}%, 请尽快处理!"
fi
if [ "$USE" -ge 90 ]; then
    send_alert "磁盘告急 ${USE}%! 即将影响服务!"   # 更高阈值, 更紧急
fi
# 配 cron 每5分钟跑一次; 生产环境更推荐用 Prometheus node_exporter + 告警规则

这道监控防线的核心价值,是把"磁盘满"从一个突然爆发的、被动的灾难,变成一个有预警的、可从容处理的问题。设两道阈值:到 80% 就预警(这时离满还有不少空间,你有大把时间从容地去清理、扩容);到 90% 就告急(更紧迫了)。关键在于"提前"——磁盘从 80% 到 100% 往往还有一段时间,这段时间就是监控告警为你争取来的、化被动为主动的宝贵窗口。我那次事故,损失之所以大,根本原因之一就是"完全没有磁盘监控"——日志一点点把磁盘填满的整个过程,我们毫不知情,直到它满了、服务崩了、用户投诉了,才后知后觉地去 df -h一个简单的磁盘水位告警,就能让你在灾难发生前好几天,就收到那句"该清理磁盘了"的提醒。这类"资源水位监控"(磁盘、内存、CPU、连接数……),是任何生产服务都该有的基础设施。我把磁盘问题"被动救火 vs 主动预防"的两条路径画成图:

这张图把两条路径的天壤之别摆得很清楚:没有监控,你只能在磁盘满、服务崩之后被动救火;有了监控,你能在它还远没满时就主动处理,服务始终平稳。同样一个"磁盘缓慢增长"的过程,有没有那道告警,决定了它最终是"一场惊心动魄的全线崩溃",还是"一次波澜不惊的例行清理"。

第三件事:别让日志成为"沉默的磁盘杀手"

这次事故也让我反思了一个更深的问题:我们到底该往本地磁盘上,写多少日志?很多服务的日志之所以能膨胀到撑爆磁盘,除了没切割,还有一个原因——日志打得太随意、太多了:大量无意义的 DEBUG 日志、把整个大对象/大响应体打进日志、循环里疯狂打日志……这些都让日志以惊人的速度增长。

// 反面: 日志打得又多又大, 加速磁盘消耗
log.info("收到请求, 完整参数: {}", hugeRequestObject);  // 把整个大对象打进去
for (Item item : millionItems) {
    log.info("正在处理 item: {}", item);   // 循环里百万次打日志! 灾难
}
log.debug("...");  // 生产环境开着 DEBUG, 海量调试日志

// 正面: 日志精简、分级、按需
// 1. 生产环境日志级别设为 INFO 或 WARN, 别开 DEBUG
// 2. 别把大对象/大响应体整个打进日志, 只记关键字段
log.info("收到请求 orderId={}, userId={}", req.getOrderId(), req.getUserId());
// 3. 循环里别逐条打, 打个汇总
log.info("批量处理完成, 共 {} 条, 成功 {} 条", total, success);

这里的核心理念是:日志不是越多越好,而是越"精准有用"越好。那些海量的、无意义的日志,不仅快速吃掉磁盘,还会在你真正排查问题时,把有价值的关键信息淹没在噪音的汪洋里——它既是"磁盘杀手",也是"排查障碍"。所以,打日志要有节制、有策略:生产环境用合适的日志级别(通常 INFO/WARN,别开 DEBUG)、别把大对象整个塞进日志、循环里别逐条打要打汇总、记录对排查真正有用的关键信息而非流水账。把日志打"精"了,既省磁盘,又让日志真正发挥它"帮助排查问题"的本职作用。这其实也呼应了一个更大的趋势——现在很多成熟的系统,都把日志通过 ELK、Loki 等方案集中收集到专门的日志系统里,而不是任由它们堆在每台机器的本地磁盘上;集中化之后,既便于检索分析,也从根本上缓解了"本地磁盘被日志撑爆"的问题。

第四件事:磁盘问题的几个"反直觉"陷阱

排查磁盘问题时,还有几个反直觉的坑,我后来都一一踩过或见过,在这里一并提个醒——它们能让你在"磁盘明明满了/没满,行为却很诡异"时不至于抓瞎。

# 陷阱1: df 说满了, du 加起来却没那么多 → 可能是"已删除但仍被占用"的文件
#   现象: 你 rm 删了大日志文件, df 显示空间却没释放!
#   原因: 文件虽被删, 但仍有进程"打开"着它, 空间不会真正释放
lsof | grep deleted   # 找出"已删除但仍被进程占用"的文件
#   解法: 重启或让那个进程释放该文件句柄, 空间才真正回收
#   (这就是为什么删日志要用 logrotate 的 copytruncate, 而非直接 rm)

# 陷阱2: df 说空间还很多, 却报 "No space left" → 可能是 inode 用尽!
df -i           # 看 inode 使用率(每个文件/目录都占一个inode)
#   现象: 海量小文件(如每个请求一个临时文件)把 inode 耗尽,
#         此时即便磁盘空间还有, 也无法再创建任何新文件
#   解法: 找出并清理那些海量的小文件

# 陷阱3: 临时文件目录 /tmp 被写满, 也会让很多程序莫名失败
du -sh /tmp/*   # /tmp 也是磁盘吃紧的常见重灾区

这三个陷阱都挺隐蔽:陷阱一(已删除但被占用)最坑——你眼看着磁盘满了,rm 删掉了大日志文件,可 df 一看空间居然没释放!原因是那个文件虽然从目录里"删"了,但还有进程开着它的文件句柄,Linux 不会真正回收一个还被打开的文件的空间;得用 lsof | grep deleted 找到它、让占用它的进程释放(或重启)才行。这也正是日志切割推荐用 copytruncate(清空原文件)而非直接 rm 的原因。 陷阱二(inode 用尽)更反直觉——df -h 显示空间充足,却报 No space left,这时要 df -i 看 inode:文件系统能容纳的文件"数量"是有限的(每个文件占一个 inode),海量小文件可能把 inode 耗尽,导致"有空间却建不了新文件"。陷阱三:/tmp 等临时目录也是吃磁盘的隐蔽重灾区。把磁盘问题的排查要点整理成一张表:

现象 排查命令 可能原因
大面积写失败 No space left df -h 磁盘空间满
磁盘满, 找谁占的 du -h --max-depth=1 | sort -rh 逐层定位大目录
删了文件空间没释放 lsof | grep deleted 文件被进程占用未释放
有空间却报 No space df -i inode 耗尽(海量小文件)
哪些大文件 ls -lhS / find -size +1G 定位巨型文件

第五件事:把"资源水位"纳入日常监控体系

这次磁盘事故,本质上暴露的是我们监控体系的一个缺口——只盯着业务指标(QPS、响应时间、错误率),却忽视了"基础资源水位"。磁盘只是其中一个;一个健全的监控体系,应该把服务器和服务依赖的各类"有限资源"的水位都盯起来。我把这些该监控的资源水位列成一张表:

资源水位 不监控的后果 建议告警阈值
磁盘使用率 满了→全线写失败(本次) 80% 预警, 90% 告急
磁盘 inode 耗尽→建不了新文件 80%
内存使用率 满了→OOM, 进程被杀 85%
CPU 使用率 满了→请求堆积变慢 持续 80%+
连接数/连接池 耗尽→新请求拿不到连接 使用率 80%
文件句柄数 耗尽→打不开新文件/连接 接近 ulimit 上限

这张表传达的核心,是一个朴素却常被忽视的运维原则:凡是"有限的、会被耗尽的"资源,都应该被监控,并在它"快耗尽"之前告警。磁盘、内存、CPU、连接、句柄……这些都是有上限的资源,而它们一旦被耗尽,后果往往都是灾难性的、全面的(就像磁盘满导致的全线崩溃)。而它们被耗尽的过程,大多是"缓慢、悄无声息"的(磁盘一点点写满、连接一点点泄漏、内存一点点增长),正因为悄无声息,才尤其需要监控这双眼睛去盯着——在它们还远未耗尽时,就把那条"水位上涨"的曲线呈现出来、把那声"快满了"的告警发出来。我那次的教训,说到底就是:我们的眼睛只盯着业务,却对脚下这些基础资源的水位视而不见,直到其中一个(磁盘)悄悄涨到了顶、淹没了一切。补上这些资源水位监控,是从这次事故里得到的、最该落地的一条改进。

一张"磁盘相关问题怎么排查与预防"的决策图

把这次踩坑沉淀成一张图。无论是遇到磁盘问题排查,还是设计新服务时预防,都可以照着它走:

这张图的两个要点:排查时,第一反应永远是 df -h(空间满)和 df -i(inode 满),再用 du 逐层定位;预防上,根治之道是给磁盘水位配监控告警,把它纳入日常的资源水位监控体系。把"大面积无规律报错 → 先看磁盘"刻进直觉,这类故障你就能几分钟定位;把"资源水位监控"建起来,这类故障你甚至能在它发生前就避免。

我立下的几条磁盘与日志规矩

这次"日志撑爆磁盘"的事故后,团队的规范里加了这么几条:

  1. 日志必须切割清理:所有日志配置按时间/大小滚动 + 保留最近 N 份 + 总量封顶(totalSizeCap),绝不允许单文件无限增长。
  2. 磁盘水位必监控告警:所有服务器配磁盘使用率告警(80% 预警、90% 告急),inode 使用率也要监控。
  3. 日志要精简分级:生产环境用 INFO/WARN 级别,别开 DEBUG;别把大对象整个打进日志;循环里打汇总而非逐条。
  4. 删日志用 copytruncate:别直接 rm 正在被写的日志(空间不释放),用 logrotate 的 copytruncate 或框架滚动。
  5. 大面积报错先看磁盘:服务出现无规律的大面积写失败,第一反应 df -h 和 df -i,别一头扎进各个功能里查。
  6. 日志最好集中收集:通过 ELK/Loki 等把日志集中收集,减轻本地磁盘压力,也便于检索分析。
  7. 资源水位全面监控:磁盘、内存、CPU、连接数、文件句柄等有限资源,都纳入水位监控,快耗尽前告警。

这几条里,第二条和第七条是真正治本的——从"出事后会排查"升级到"出事前能预警"。我尤其想强调这次事故带给我的一个心态转变:它让我意识到,运维监控不能只盯着那些"显眼的、业务相关的"指标(QPS、错误率),更要盯住那些"沉默的、基础的"资源水位(磁盘、inode、句柄)。因为恰恰是这些不起眼的基础资源,一旦悄无声息地耗尽,带来的往往是最全面、最致命的崩溃;而它们耗尽的过程又是那么缓慢、那么没有存在感,以至于不主动用监控去盯,你根本不会注意到它正一步步逼近悬崖。盯住那些沉默的基础资源,就是在守护服务最容易被忽视、却最致命的那条底线。

写在最后:最朴素的隐患,往往造成最惨烈的事故

这次"磁盘被日志撑爆"的事故,给我最深的感慨,是它的"朴素"与"惨烈"之间那巨大的反差。"磁盘空间会被用完"——这是一个朴素到不能再朴素、几乎不配被称为"技术问题"的常识,小学生都懂"杯子装满了就会溢出来"。可正是这么个朴素的常识,因为我们的忽视,造成了一场全线崩溃、影响所有用户的惨烈事故。回头看,我们在那些"高大上"的地方——架构怎么设计、算法怎么优化、并发怎么处理——投入了大量的心思和敬畏;却恰恰在"磁盘会不会满""日志要不要清"这种朴素到我们都懒得多看一眼的地方,栽了最大的跟头。

想通这一点,我对"工程"二字,生出了一份更踏实、也更谦卑的理解。系统的可靠性,从来不只取决于那些光鲜的、高深的设计;它同样、甚至更多地,取决于那些朴素的、琐碎的、不起眼的基础——磁盘够不够、日志清没清、备份做没做、监控全不全。这些事一点都不"性感",不会让你在技术分享时显得很厉害,可它们恰恰是系统稳定运行的地基。一个真正成熟的工程师和团队,不会因为一件事"朴素""不起眼"就轻视它;恰恰相反,他们对这些朴素的基础,怀有最大的敬畏——因为他们深知,再宏伟的大厦,也可能毁于一道被忽视的、朴素的裂缝。我那次,就是被一道叫"磁盘"的、最朴素的裂缝,绊了个结结实实的跟头。

所以,如果你也在做系统、做运维,我想把这次踩坑最想说的话送给你:请别轻视那些朴素到"不值一提"的基础——磁盘空间、日志清理、备份恢复、资源监控。越是朴素的东西,越容易因为"理所当然""不起眼"而被忽视,而它们一旦出问题,往往就是最全面、最惨烈的事故。把这些朴素的基本功扎扎实实做好——给日志配上切割清理,给磁盘配上水位告警,给每一个有限的资源都装上一双盯着它的眼睛——这远不如谈论高深架构那样令人兴奋,却是一个系统能不能稳稳活下去的真正底气。那个被日志撑爆的磁盘,最终用一场全线崩溃,教会了我对"朴素的基础"最深的敬畏:真正可靠的工程,既要有仰望星空的架构,也要有脚踏实地的、对每一寸磁盘空间的认真。愿你我都不再在最朴素的地方,栽最惨烈的跟头。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

重试把下游打死了:重试风暴避坑复盘

2026-6-1 13:58:19

技术教程

让大模型返回JSON给程序处理,开发全过上线却偶发解析失败,捞出原始返回一看JSON五花八门包代码块加客气话:大模型结构化输出避坑复盘

2026-6-1 14:11:57

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