我的服务跑了几个月一直好好的,某天突然各种 No space left on device,数据写不进、健康检查失败,连同节点上别的服务一起遭殃,排查发现是日志文件没配轮转涨到了几十 G 把磁盘撑满了的深度复盘

我的服务一直把日志写到一个文件 app.log,平稳跑了好几个月。某天毫无征兆地一堆故障同时爆发:报 No space left on device、数据写不进、健康检查失败被重启,连同节点上别的服务也跟着遭殃。登机器 df -h 一看磁盘 100% 满了,du 一查祸首是 app.log——它悄悄涨到了几十 GB。复盘才意识到:我只关心了把日志记下来,却从没考虑日志写到哪、会涨多大、怎么清理;一个持续运行的服务每时每刻往日志文件追加,是只增不减的,而磁盘空间有限,只增不减的东西放在有限空间里迟早撑满,只是时间问题;更糟的是磁盘是整台节点共享的,我一个服务的日志写满磁盘把同节点所有服务都拖下了水。根因是日志没配轮转/清理/上限。这篇复盘从故障现场讲到问题本质(单调增长+有限空间=必然撑满)、磁盘满的连锁反应,再到应用层 RollingFileAppender(maxFileSize+maxHistory+totalSizeCap)、系统层 logrotate、容器层 max-size、监控磁盘告警、日志盘分离的完整正解,以及其他只增不减占满有限资源的坑,和只增不减之物在有限空间里必然撑爆、增长与回收必须成对设计、可持续的前提是有进有出的认知。

我的服务跑了几个月一直好好的,某天突然各种 No space left on device,数据写不进、健康检查失败,连同节点上别的服务一起遭殃,排查发现是日志文件没配轮转涨到了几十 G 把磁盘撑满了:一次日志只增不减写爆磁盘、误以为日志记着就完事了的深度复盘

那次"服务好端端跑了几个月,突然集体罢工"的故障,根子是一个我从没在意过的东西——日志。我的服务一直把日志写到一个文件里(app.log),功能、性能都没问题,平稳跑了好几个月。某天,毫无征兆地,一堆诡异故障同时爆发:服务报 java.io.IOException: No space left on device(磁盘没空间了)、数据写不进数据库(临时文件也写不了)、健康检查失败被重启,更糟的是——同一台节点上别的服务也跟着遭殃(它们也写不了磁盘)。我登上机器一看 df -h,傻眼了:磁盘 100% 满了;再 du 一查,祸首是我那个 app.log——它已经悄悄涨到了几十 GB,把整块磁盘几乎吃光了。复盘这件事,我才真正意识到一个被我彻底忽略的问题:问题出在我的日志"只增不减",却没有任何"轮转/清理/上限"机制。我只关心了"把日志记下来"(写日志),却从没考虑过"日志写到哪去、会涨多大、怎么清理";一个持续运行的服务,每时每刻都在往日志文件追加内容——它是一个"只会增长、永不缩小"的东西;而磁盘空间是有限的;一个"只增不减"的东西,放在一个"有限"的空间里,结局是注定的:迟早把空间撑满——只是时间问题(我这跑了几个月才爆,反而麻痹了我);更糟的是,磁盘是整台节点共享的,我一个服务的日志写满磁盘,把同节点所有服务都拖下了水。根本原因是:日志文件只增不减、没有配置轮转(按大小/时间切分并只保留有限份)或清理机制,在有限的磁盘空间里持续增长直至写满,导致本服务及同节点其他服务都无法写磁盘;而我只关注了"记录日志",忽视了"持续增长的东西在有限空间里必须有回收上限"。问题的根,是日志只增不减写爆磁盘——没配轮转/清理/大小上限,单调增长的日志在有限磁盘里迟早写满、并连累同节点其他服务;根源是只想着记日志、没想着它会无限增长占满有限资源。这篇就把这次"日志写爆磁盘"的坑,从头到尾复盘一遍。

故障现场:一个日志文件,撑爆整块磁盘

问题在于日志只增不减、没有轮转和上限:

# 现象: 服务突然各种报错
#   java.io.IOException: No space left on device
#   数据库写入失败(临时文件也写不了)、健康检查失败被kill、同节点其他服务也异常

# 登机器排查:
$ df -h
# Filesystem  Size  Used Avail Use%  Mounted on
# /dev/vda1   100G  100G    0G 100%  /          ← 磁盘100%满了!

$ du -sh /app/logs/*
# 48G    /app/logs/app.log         ← 祸首! 一个日志文件涨到了48G

# 为什么会这样:
# 1. 我的服务持续运行, 每时每刻往 app.log 追加日志(INFO/DEBUG全打), 文件只增不减;
# 2. 我没配任何"日志轮转(rotation)": 没按大小/时间切分、没限制保留份数、没自动删旧;
# 3. 跑了几个月, app.log 从几MB涨到几十GB, 直到把磁盘100%写满;
# 4. 磁盘满了 → 我的服务写不了日志/临时文件/数据 → 各种IOException;
# 5. 磁盘是整台节点共享的 → 同节点其他服务也写不了磁盘 → 集体遭殃。

# (容器场景同样的坑: Docker默认 json-file 日志驱动不限大小,
#  容器stdout/stderr日志会一直涨, 撑满宿主机磁盘; K8s节点磁盘满还会驱逐Pod。)

# ★ 本质: 日志是"只增不减"的(持续追加), 磁盘是"有限"的;
#   "只增不减的东西" + "有限的空间" = 迟早撑满(必然, 只是早晚);
#   必须给日志配"轮转/上限/清理": 按大小或时间切分、只保留最近N份、自动删除旧的。
#   我的错误: 只想着"把日志记下来", 没想过"它会无限长大、占满有限的磁盘"。

盯着 df -h 那刺眼的 100% 和那个 48G 的日志文件,我又懊恼又后怕:"我从来只关心'有没有把日志打出来',压根没想过这个文件会一直涨、涨到把磁盘撑爆……一个只会变大、永远不会变小的东西,放在有限的磁盘上,这结局其实从一开始就注定了,我只是被'跑了几个月没事'给麻痹了。"这个坑最隐蔽的地方在于:它的潜伏期极长——几个月、甚至几年都"没事",日志慢慢涨,完全在你的关注之外;它不是突然坏的,而是缓慢逼近临界点、然后在某一刻骤然爆发;而且爆发时牵连甚广(整个节点的服务),表现还很迷惑(各种 IOException,不直接说是日志的事)下面就来拆解,日志该怎么管。

第一件事:搞懂日志轮转与"增长必须配回收"

我顺着这次事故,把日志管理和"单调增长"的治理彻底理清了。

为什么日志会写爆磁盘? 怎么管?

【核心: 日志只增不减、磁盘有限, 不配轮转必然写满并连累整个节点; 要配日志轮转(按大小/时间切分、
   只保留最近N份、自动删旧)或容器日志max-size、输出到集中日志系统、监控磁盘告警——增长必须配回收】

1. 问题的本质: 单调增长 + 有限空间 = 必然撑满
   - 日志是持续追加的, 文件只增不减(单调增长);
   - 磁盘容量有限; 任何"只增不减"的东西放进"有限空间", 迟早占满, 这是必然, 只是早晚;
   - 跑得久了反而危险: "一直没事"让人以为没问题, 实则在慢慢逼近临界。

2. 危害(磁盘满的连锁反应):
   - 本服务: 写日志/临时文件/数据失败 → IOException、功能异常、健康检查失败被重启;
   - 同节点其他服务: 磁盘是共享的, 大家都写不了 → 集体遭殃;
   - K8s: 节点磁盘压力 → 驱逐Pod、节点NotReady。

3. 正解: 给日志配"轮转 + 上限 + 清理"(让它有界)
   ① 日志轮转(rotation): 按大小(如100MB)或时间(每天)切分日志文件;
   ② 限制保留份数/总量: 只保留最近N个(如10个)或最近M天, 旧的自动删除/压缩;
   ③ 工具: 应用层用 logback/log4j2 的 RollingFileAppender(配maxFileSize+maxHistory+totalSizeCap);
      系统层用 logrotate(配size/rotate/compress/maxage);
   ④ 容器: 配Docker json-file 的 max-size/max-file, 或用日志驱动; K8s靠节点日志轮转;
   ⑤ 集中日志: 日志输出到ELK/Loki等集中系统, 本地只留少量, 别在本地无限堆。

4. 还要做的(防患于未然):
   - 监控磁盘使用率, 设告警(如>80%告警), 别等100%才发现;
   - 控制日志级别(生产别全开DEBUG, 日志量直接小一个数量级);
   - 日志和数据/系统盘分开挂载, 别让日志撑满影响系统/数据。

5. 更普适的原则: 凡"持续产生、占用有限资源"的东西, 都要有"回收/上限"机制
   - 日志、临时文件、缓存、消息堆积、连接、内存对象、监控数据...都会"增长";
   - 有限的资源(磁盘、内存、连接数)容不下无限的增长;
   - 所以"产生"的同时必须设计"回收"(轮转、过期、淘汰、清理、上限)——增长配回收, 缺一不可。

一句话: 日志只增不减、磁盘有限, 不配轮转必然写满并连累整个节点; 要配轮转(按大小/时间切分+只留N份+
   删旧)、容器配max-size、输出集中日志系统、监控磁盘告警; 凡持续增长占用有限资源的东西都要配回收机制。

这套认知,是整个坑的根。问题本质:日志单调增长 + 磁盘有限 = 必然撑满,只是早晚;跑得久"一直没事"反而麻痹人危害:磁盘满→本服务 IOException/被重启、同节点服务集体遭殃、K8s 驱逐 Pod正解:配日志轮转(按大小/时间切分、只留最近 N 份、删旧)、容器配 max-size/max-file、输出到集中日志系统、监控磁盘告警、控制日志级别、日志与数据盘分离更普适的原则:凡"持续产生、占用有限资源"的东西(日志/临时文件/缓存/消息堆积/连接)都要有回收上限机制——增长配回收,缺一不可一句话:日志只增不减、磁盘有限,不配轮转必然写满并连累整个节点;要配轮转(按大小/时间切分+只留 N 份+删旧)、容器配 max-size、输出集中日志系统、监控磁盘告警;凡持续增长占用有限资源的东西都要配回收机制。

第二件事:正解——配日志轮转、容器限大小、监控磁盘

知道了"增长必须配回收",正解就清楚了:从应用、容器、监控三层给日志套上界。

<!-- 正解1: 应用层日志轮转(以 logback 为例, 配大小+时间+总量上限) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>/app/logs/app.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <!-- 按天 + 单文件超100MB 就切分 -->
    <fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
    <maxFileSize>100MB</maxFileSize>
    <maxHistory>7</maxHistory>          <!-- 只保留最近7天 -->
    <totalSizeCap>5GB</totalSizeCap>     <!-- 所有日志总量上限5GB, 超了删最旧的 -->
  </rollingPolicy>
  <encoder><pattern>%d %-5level %logger - %msg%n</pattern></encoder>
</appender>
<!-- 关键: maxFileSize(切分) + maxHistory(留几份) + totalSizeCap(总量上限), 三者一起=有界 -->
# 正解2: 系统层用 logrotate(给非应用管理的日志或老服务)
# /etc/logrotate.d/myapp
/app/logs/*.log {
    daily              # 每天轮转
    rotate 7           # 只保留7份
    size 100M          # 或超100M就轮转
    compress           # 旧日志压缩
    missingok
    notifempty
    copytruncate       # 不中断正在写的进程
}

# 正解3: 容器层限制日志大小(Docker 默认 json-file 不限大小, 必须配!)
# docker run --log-opt max-size=100m --log-opt max-file=5 ...
# 或 daemon.json 全局:
#   { "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "5" } }
# K8s: 配置节点的容器运行时日志轮转, 或用 DaemonSet 采集后清本地。

# 正解4: 监控磁盘使用率 + 告警(别等100%)
# Prometheus: node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.2 告警(剩余<20%)
# 提前告警, 在写满之前处理。

# 正解5: 控制日志级别 + 日志盘分离
#   - 生产环境别全开DEBUG(日志量小一个数量级);
#   - 日志单独挂一块盘, 别和系统盘/数据盘共用, 即使写满也不连累核心。

# 核心: 应用层RollingFileAppender(大小+份数+总量上限)、系统层logrotate、容器层max-size、
#   监控磁盘告警、控制日志级别、日志盘分离——让"只增不减的日志"变成"有界、会回收"的。

这套正解的关键,是给"只增不减的日志"套上明确的"上界 + 回收",让它变成有界的应用层轮转:用 RollingFileAppender 配 maxFileSize(切分)+maxHistory(留几份)+totalSizeCap(总量上限),三者一起才真正有界——这正是本次缺的。系统层 logrotate:按天/大小轮转、只留 N 份、压缩旧的。容器层限大小:Docker 默认 json-file 不限大小,必须配 max-size/max-file。监控磁盘告警:剩余<20% 就告警,别等 100% 才发现。日志级别 + 盘分离:生产别全开 DEBUG、日志单独挂盘不连累核心。

第三件事:其他几个"只增不减占满有限资源"的坑

顺着这次日志写爆磁盘,我把"持续增长却没回收"的几类坑也一并理了:

几类"只增不减、占满有限资源"的坑(核心都是"增长没配回收"):

坑1: 临时文件不清理——程序生成的临时文件/上传文件不删, 慢慢占满磁盘;
   正解: 用完即删、定时清理、用系统临时目录(有清理机制)。

坑2: 缓存只放不淘汰——本地缓存(HashMap当缓存)只put不过期, 内存越用越多 → OOM(同560);
   正解: 用有容量上限/过期策略的缓存(Caffeine/Guava, 配maxSize/expireAfter)。

坑3: 消息队列堆积——生产快于消费, 消息越积越多, 撑爆MQ存储;
   正解: 限流、扩消费者、设队列上限/TTL、监控积压。

坑4: 数据库表无限增长——日志表/流水表只插不归档, 越来越大、查询越来越慢;
   正解: 定期归档/分区/冷热分离/删过期数据。

坑5: 监控/指标数据无限保留——时序数据不设保留期, 撑爆存储;
   正解: 设retention(如保留30天), 降采样旧数据。

坑6: 连接/线程/句柄只创建不释放——泄漏导致耗尽(连接池满、too many open files);
   正解: 用完归还/关闭, try-with-resources, 设上限。

共同的根: 任何"持续产生、单调增长"的东西(日志、临时文件、缓存、消息、数据、连接), 放在"有限的
   资源"(磁盘、内存、存储、连接数)里, 不配"回收机制"(轮转、过期、淘汰、归档、释放、上限),
   就一定会在某天把资源占满——"增长"和"回收"必须成对设计, 只产生不回收是埋雷。

这些坑看似分散,根却是同一个:任何"持续产生、单调增长"的东西,放在"有限的资源"里,若不配"回收机制",就一定会在某天把资源占满——"增长"和"回收"必须成对设计认清这个根("凡单调增长占用有限资源,必配回收/上限"),就能预判并预防一大类"跑久了突然爆"的问题。

第四件事:日志治理的层次 / 配置要点——两张对照表

我把日志治理的各个层次、以及关键配置,整理成对照表,贴在了团队的运维规范里:

层次 手段 作用
应用层 RollingFileAppender 按大小/时间切分,限份数和总量
系统层 logrotate 轮转、压缩、删旧,适配任意日志
容器层 max-size / max-file 限制容器 stdout 日志大小
采集层 ELK / Loki 输出到集中系统,本地少留
监控层 磁盘使用率告警 提前发现,别等写满
隔离层 日志盘独立挂载 满了不连累系统/数据盘
配置项 含义 建议
maxFileSize 单文件多大就切分 100MB~500MB
maxHistory 保留多少份/天 按需,7~30
totalSizeCap 所有日志总量上限 必设!超了删最旧
compress 旧日志压缩 开,省空间
日志级别 INFO/DEBUG 生产 INFO,别全开 DEBUG
磁盘告警阈值 剩余多少告警 剩 20% 就告警

这两张表的核心,第一张是日志治理要多层防护(应用/系统/容器/采集/监控/隔离),而不是只靠一处;第二张是配置里 totalSizeCap(总量上限)是最容易漏又最关键的——只配切分和份数,仍可能因单份太大或份数太多而超。记住一条:给日志配置时,一定要有一个"无论如何总量不超过 X"的硬上限。

第五件事:关于日志与磁盘的几组容易想当然的认知

这次事故也让我厘清了几组关于日志的、容易想当然的概念:

直觉以为 实际上
把日志打出来记下就完事了 还要管它写到哪、涨多大、怎么清
跑了几个月没事就没问题 正缓慢逼近临界,跑越久越接近爆
磁盘那么大,日志能占多少 只增不减,几个月就能涨到几十 G
日志是我服务的事,不影响别人 磁盘共享,写满连累整个节点
容器日志自动会清理 Docker 默认不限大小,会撑满宿主机
配了按大小切分就安全了 没限份数/总量,切再多份也能占满
这是磁盘/运维的问题 是"产生方没配回收"的设计问题

这张表里,我栽的是第一行和第二行:以为"日志打出来记下就完事",又被"跑了几个月没事"麻痹,从没想过这个只增不减的文件会把磁盘撑爆厘清这些,核心是一个意识:"产生日志"只是一半,"管理日志的生命周期(轮转、上限、清理)"是另一半、也是更容易被忘的一半;任何持续产生、占用有限资源的东西,都要在"产生它"的同时设计好"怎么回收它"。

第六件事:服务上线 / 配日志时,我现在的自检习惯

现在每当我的服务要上线、或配置日志,我都会先按这张图问自己:

这张图的精髓,是"只增不减的东西必须配回收+总量硬上限+资源告警"先问会不会只增不减、再看配没配回收、有没有总量硬上限、有没有资源监控告警这套习惯,让我从"把日志记下来就完事"变成了"产生它的同时就设计好怎么回收它"——核心始终是:日志只增不减、磁盘有限,不配轮转必然写满并连累整个节点;要配轮转(切分+留 N 份+总量上限)、容器配 max-size、监控磁盘告警;凡持续增长占用有限资源的东西都要配回收机制。

我立下的几条规矩

这场"日志写爆磁盘、连累整个节点"的事故,换来了我做运维时,刻进骨子里的几条铁律:

  1. 日志只增不减,磁盘空间有限;不配轮转,迟早把磁盘写满(只是早晚)。
  2. 磁盘是节点共享的,一个服务写满磁盘会连累同节点所有服务。
  3. 应用层配 RollingFileAppender:maxFileSize + maxHistory + totalSizeCap(总量硬上限别漏)。
  4. 容器默认 json-file 日志不限大小,必须配 max-size/max-file。
  5. 监控磁盘使用率并告警(剩 20% 就报),别等 100% 才发现。
  6. 生产别全开 DEBUG;日志盘与系统/数据盘分离。
  7. 凡持续产生、占用有限资源的东西(日志/临时文件/缓存/消息/数据),都要配回收/过期/上限。

写在最后

回头看,这场由"日志只增不减写爆磁盘"引发的、连累整个节点的故障,真正教给我的,远不止"给日志配轮转和总量上限"这一个技巧。它让我对"任何'只产生、不回收'的东西, 在'有限的容器'里, 都是一颗注定会爆的定时炸弹——它平时悄无声息地积累, 让你误以为'没事', 直到某天撞上容量的天花板, 骤然引爆",有了一次刻骨的体会。我栽跟头,是因为我只设计了"产生"(怎么把日志写出来),却完全没设计"回收"(怎么清理它)——我把注意力全放在了"开始"和"积累"上, 默认那个不断变大的东西"会一直有地方放";可我忽略了一个最朴素的物理事实: 承载它的空间(磁盘)是有限的;"一个只增不减的量" 撞上 "一个有限的容量", 结局从一开始就写好了——不是会不会爆, 而是什么时候爆; 而"跑了几个月没事"非但不是安全的证据, 反而是"越来越接近引爆点"的倒计时这让我领悟到一个关于"增长、有限与可持续"的深刻认知:这个世界里几乎没有"可以无限增长"的东西, 因为承载增长的资源(空间、时间、内存、注意力、金钱、环境)总是有限的; 所以任何"持续产生、单调增长"的过程, 要想可持续, 就必须配一个与之匹配的"回收/消解/出清"机制——有进就要有出, 有产生就要有回收, 有积累就要有释放; 一个"只进不出、只增不减"的系统, 无论它积累得多慢, 本质上都是不可持续的, 终将被它自己的增长所压垮;这不仅是日志/缓存的工程问题, 也是"债务、库存、待办、技术债、垃圾"等一切"会累积之物"的通则: 光顾着往里加、不安排往外清, 迟早出事这给了我一种面对"会累积之物"时的清醒:每当我设计或使用一个会"持续产生某种东西"的系统时,要在设计"怎么产生它"的同时、而非事后,就问"这东西会一直增长吗?承载它的资源有限吗?我打算怎么、何时、按什么上限回收/清理它?"——把"回收机制"当成和"产生机制"同等重要、必须成对出现的设计, 而不是一个"等出问题再说"的事后补丁;"为一切单调增长之物预先配好回收与上限、让有进有出",是构建任何可长期运行的系统的根本前提认清只增不减之物在有限空间里必然撑爆、增长与回收必须成对设计、可持续的前提是有进有出——这,是我用一次日志写爆磁盘的事故,换来的、关于 DevOps、也关于如何对待一切会累积之物的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给服务配日志时,顺手加上那行 totalSizeCap、给磁盘加个使用率告警,那我登上那台磁盘 100% 的机器、对着 48G 日志文件发愣的那一刻,就值了。

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

我的服务通过域名调用下游,下游因为故障切换换了 IP,我的服务却还死死连着那个已经下线的旧 IP 一直报连接失败,重启之后才恢复,原来是 DNS 解析结果被缓存了很久不刷新的深度复盘

2026-6-3 1:01:30

技术教程

我让大模型以流式方式返回一段 JSON,想着边收到边解析更快,结果每次拿到的都是残缺的半截 JSON 解析直接报错,而且流到一半模型出错时前面已经发给用户的内容根本收不回来的深度复盘

2026-6-3 1:13:20

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