告警邮件在凌晨两点把你从被窝里拽起来,看一眼内容——根分区使用率 95%,马上要满。这个场景我见过太多次了,有时候是测试环境,有时候是正在跑业务的生产机器。Linux 系统跑着跑着磁盘满了,本质上不是系统的问题,而是我们对空间去向缺乏警觉:日志在长、临时文件在堆、软件包缓存越来越臃肿,直到某一天所有写入都报 no space left。
这篇文章就把这件事掰开揉碎讲清楚:根分区是怎么被一步步填满的、怎么在十分钟内定位到大文件、哪些文件可以放心清理、哪些动错了会出大事,以及最后怎么设好机制让这种问题不再半夜找上门。不管你是刚接触运维的新手,还是已经被磁盘报警折腾过的老手,这篇按着顺序看完,下次再遇到能少走很多弯路。
1. 为什么告警总是发生在凌晨:先搞清楚空间去哪儿了
磁盘空间从来不是一夜之间消失的,只是等到你发现的时候往往已经到临界点了。根分区被打满,背后基本逃不出几类元凶。把这几类记在脑子里,排查的时候就不至于像无头苍蝇。前几年我自己也犯过这种错误——一上来就满系统翻大文件,翻了半天发现罪魁祸首就是自己忘了轮转的日志。这个教训让我明白了,先分析类型再去定位文件,效率完全不一样。
1.1 系统日志与审计记录:最容易被低估的占位大头
很多机器默认装了 rsyslog 或 systemd-journald,日志会持续写入 /var/log 目录。问题在于大部分场景下日志是只增不减的:应用每秒钟打几条日志看起来不多,但一天几万次请求积累下来就是几百 MB 甚至几个 GB。系统日志、认证日志、内核日志、应用自定义日志,全都堆在 /var/log 下面,加上有些应用自己维护的 log 目录,空间占比相当可观。
journald 的日志存得比较隐蔽,它在 /var/log/journal/ 目录下,按机器名分目录存着二进制 journal 文件。如果你从来没有配置过它的占用上限,默认情况下它最多可以占用文件系统大小的 10%。对一个大分区来说 10% 可能没什么感觉,但对一个 20GB 的根分区来说,2GB 就这样被无声无息地吃掉了。很多人排查的时候没把 journald 算进去,总在 /var/log 下面找.log后缀的文件,自然找不到真正的大头。
1.2 软件包缓存与临时文件:看似不起眼的长期堆叠
包管理器是另一个经常被忽略的占用源。Debian 系的机器上,apt 会把下载的 .deb 包缓存到 /var/cache/apt/archives,长期不清理能累积出几个 GB。RedHat 系的 dnf 也有类似问题,缓存目录在 /var/cache/dnf。这些文件从功能上说完全没用——包已经装完了,缓存在那里唯一的用途就是下次重装省一次下载,绝大多数情况下你根本不会去用它们。
类似地还有 /tmp 和 /var/tmp。系统正常运行时,许多工具会在 /tmp 下存放临时文件,正常情况下生命周期很短,但总有些进程退出得不干净,临时文件就留在原地了。需要说明的是 /tmp 和 /var/tmp 在语义上有区别:/tmp 里的数据重启后通常会被清理,而 /var/tmp 被要求持久保存,很多程序把会话数据放这里,一放就是几个月没人管。加上 systemd-tmpfiles 机制虽然有清理规则,但默认策略往往偏保守,过期文件积累的速度大于清理速度,等到想清理的时候目录已经膨胀到不忍直视了。
1.3 容器环境里的隐藏空间黑洞
如果你在跑 Docker 或者容器化的应用,磁盘占用出现突增时,优先怀疑镜像和容器层。/var/lib/docker下面有几个东西非常吃空间:一是重复的镜像层,同一个镜像拉了不同 tag,底层共享但 tag 数据会累积;二是构建缓存,多次构建产生大量不再使用的中间层;三是悬空镜像和停止的容器,它们会把磁盘占着但没有任何业务价值。
容器日志也一样要小心。Docker 默认的日志驱动是 json-file,且默认不设置大小上限。一个上生产环境的容器,如果应用频繁输出日志,容器目录下对应 json 文件可以快速增长到几个 GB。这类日志还不受传统 logrotate 管理,需要靠 Docker 的 log-opts 限制或额外工具来处理。我处理过不止一次类似情况,明明 df 显示空间被吃掉了,进 /var/log 看却没什么大文件,最后才发现是 Docker 容器日志在膨胀。
| 占用类型 | 常见路径 | 清理前确认点 |
|---|---|---|
| 系统日志 | /var/log/*.log | 确认不需要的旧日志、轮转配置是否生效 |
| journald 日志 | /var/log/journal/ | 当前状态、系统时间范围 |
| 包缓存 | /var/cache/apt/archives、/var/cache/dnf | 磁盘充足且不准备离线重装 |
| 临时文件 | /tmp、/var/tmp | 防止正在被进程写入的文件被误删 |
| Docker 镜像/日志 | /var/lib/docker、/var/lib/docker/containers | 确认没有运行中容器依赖的相关镜像 |
| 应用日志 | 业务代码自建 log 目录 | 日志是否有审计或合规留存要求 |
这些类型梳理完,你就可以在收到告警时先做一轮心理排查:最近有没有部署新版应用?有没有调过日志级别?是不是跑了很多构建任务?把可能的元凶范围缩小到两三个目录,再往下去跑命令定位,比盲目全盘扫描要省太多时间。
2. 排查链路:df、du、lsof怎么搭着用才能不白跑一趟
定位方向清楚了,接下来就是用工具把它揪出来。很多人拿到 df 的输出之后,习惯先看一眼哪个分区满了,然后马上rm -rf一些看着没用的文件,运气好把空间释放了,运气不好删到重要数据还得从备份恢复。正确的排查方式是用几条命令串成一条链路,每一步都比上一步更接近问题的根源。这里我把这套链路完整捋一遍,按顺序执行基本能覆盖九成以上的故障场景。
2.1 先看 df,再看 df -i:空间和 inode 是两码事
第一步永远是df -h。这一步看两层信息,一是哪个挂载点占用率高,二是文件系统的总容量到底多大,好对故障规模有个概念。但只做这一步是不够的——很多新手忽略了一个重要事实:文件系统满了有时候不是因为数据量大,而是 inode 耗尽了。df -i看的就是 inode 的使用情况,如果 IUse% 接近 100%,即使df -h显示还有空间,系统同样会报 no space left,因为这表示这个分区能创建的新文件数量已经到上限了。
怎么理解 inode?可以把文件系统想象成一个仓库,inode 就是仓库里的货架编号,数据是货架上的货物。货物再多,只要货架编号没满就能继续放;但每个文件至少要占一个编号,当你创建了大量零碎小文件——比如某个程序在一个目录下生成了几百万个小文件——编号就会先耗尽。这时候你查df -h空间可能还剩很多,但touch一个新文件都会失败。如果确认是 inode 满了,重点排查方向就是那些可能堆了大量小文件的目录,本质上和查大文件是两套思路。
# 查看挂载点空间使用情况 df -h # 查看 inode 使用情况 df -i # 只查看根分区,过滤掉临时挂载 df -h / | tail -1这一步做完你就知道自己面对的是空间型故障还是 inode 型故障,后文的清理策略也完全不同。这里顺带提一个日常有助于判断的细节:当df -h和df -i同时出现高占用时,通常是某个目录下既有大文件又有海量小文件,属于灾难级的双满了,这种时候排查范围反而更明确,指向多半是日志目录或者缓存目录。
2.2 du 从浅入深:一层一层剥出真正的大目录
知道是空间问题之后,du就是你的主战武器。但 du 有一个明显的问题:对整个根分区做全量扫描非常慢,尤其是几 TB 的大分区,扫描时间长得让人怀疑命令是不是卡死了。所以正确的用法是逐层往下剥,每次只看当前目录下一级子目录的大小,找到异常大的那个,再钻进去继续看。
# 查看根目录下每个一级子目录的大小,按人类可读格式输出 du -h --max-depth=1 / | sort -hr # 或者用 -x 限制不跨文件系统,只查根分区本身,避免扫描到其他挂载点浪费时间 du -x -h --max-depth=1 / | sort -hr注意-x这个参数,非常有价值。它让 du 只统计当前文件系统内的数据,不会去穿透 /proc、/sys,也不会把 /data 这种独立挂载点算进来。不加这个参数,你会花大把时间去扫描那些本来就不属于根分区的目录,纯粹是白费力气。扫描结果出来之后,哪几个目录占大头一目了然,去排序结果的前几位里找答案即可。
还有一种更直观但需要额外安装的工具叫 ncdu,它把 du 的结果做成交互式界面,用方向键在目录树里上下移动,按d删除文件,按b返回上一级。我个人习惯在服务器上用 du,因为它在最小化安装的系统里通常自带,但如果你有权限装工具,ncdu 在交互体验上会更顺手一些。不过要提醒一点,ncdu 的删除功能太顺手了,有时候光标停在某个目录上随手一按 d 就把整个目录删了,这是我用它时唯一心悸的地方——宁可在进入目录后确认文件列表再动手,也别依赖方向键去按删除。
2.3 find 精准定位大文件,lsof 找被删除但仍占空间的“幽灵文件”
目录层级定位完成后,一般就能锁定到某个具体目录了。这时候最想干的事情是:把目录下超过某个大小阈值的大文件找出来。用 find 一步就能做到:
# 查找指定目录下大于 500MB 的普通文件 find /var -xdev -type f -size +500M -exec ls -lh {} \; # 另一种写法,用 -printf 直接输出大小和路径 find /var -xdev -type f -size +500M -printf '%s %p\n' | sort -nr | head -20-size +500M 指的是文件大小超过 500MB,{}会被替换成实际文件路径。这里用 -xdev 和前面 du 的 -x 是同一个理由——避免跑到别的文件系统上去。如果 find 扫完没发现大文件,而df -h又说分区确实满了,那基本可以断定是 inode 满了,回到df -i去处理。
另一个常见且诡异的情况是:文件被删了,空间却没释放。这通常是因为某个进程还持有该文件的文件描述符。Linux 下用lsof +L1可以找到这类文件——+L1 表示只显示 link count 为 0 的文件,也就是目录项已经不存在但进程仍然打开的文件。这种文件的磁盘空间不会被释放,直到持有它的进程退出或重启。生产中经典案例是 logrotate 处理某个日志后,服务进程还在写旧的备份文件句柄,于是磁盘空间白白被占着。此时要么重启进程,要么向进程发送合适的信号让它重新打开日志文件,单纯删除文件只会白费功夫,因为空间并没有真正回流到文件系统。
# 查看被删除但仍被进程占用的文件 lsof +L1 | head -50 # 更精确地查看某个目录下被占用的已删除文件 lsof +L1 / 2>/dev/null | grep '/var/log' | head -302.4 动态增长型问题的定位思路
排查告警时还有一类场景需要特殊处理:文件不是已经占满,而是正在以肉眼可见的速度增长。比如某个应用突然进入死循环疯狂打日志,或者临时文件生成速度异常。此时你用 du 扫了一遍,可能扫完还是没发现问题,因为在你扫描的这段时间里,文件还在持续增长,而且增长速度极快。
我推荐的做法是分两次快照对比。第一次先记录根分区当前使用量,等两三分钟,再记录一次,两次差值就能估算出增长速度。同时可以配合lsof +L1和find -size去看哪些文件的修改时间最新——不断增长的文件 mtime 一定是最近的。遇到这种情况,别急着删文件,先去找到是哪个进程在写,用lsof | grep deleted或fuser定位到 PID,确认这个进程到底该不该继续写。如果确认是 bug 导致的日志风暴,正确的顺序是:先停止或重启该服务,再清掉已经膨胀的日志,最后处理日志配置。顺序反了的话,你刚清出 2GB 空间,可能一个小时后又被填满了。
3. 动手清理:哪些文件能碰、哪些必须绕道走
定位到具体文件之后,最考验经验的就是“清理”这一步了。清理工作从来不是把文件删掉那么简单,要考虑文件是否有业务价值、删除后是否有进程受影响、清理对象之间是否存在依赖关系。我把几类高频清理对象分开讲,每一类都会说清楚可以怎么清、注意事项是什么,以及什么情况下绝对不要乱动。
3.1 日志清理:用系统机制,别用蛮力
清理日志最忌讳的就是跑到 /var/log 下面rm -rf *.log。这种方式看着立竿见影,实际上一堆进程还握着日志文件的句柄在写,你可能删完文件后磁盘空间一点没释放(进程继续往已删除的 inode 写入),还可能把正在归档的审计日志删掉,影响合规留存。正确做法是用系统自带的日志清理机制。
journald 的日志用 journalctl 清理是标准操作。命令很简单:
# 清理 journal 日志,使总占用低于 100MB journalctl --vacuum-size=100M # 清理 journal 日志,只保留最近 3 天 journalctl --vacuum-time=3d这两个参数的含义不复杂:vacuum-size 把 journal 总占用压缩到指定值以下,vacuum-time 把早于指定时间的日志条目删掉。注意这些命令都需要 root 权限。执行完后可以用journalctl --disk-usage验证一下效果。实际经验是,Servers 上日志长期不清理的,这个命令一跑就能回收两三个 GB 的空间。
传统日志轮转文件则使用 logrotate。检查 /etc/logrotate.d/ 下的配置,确认各应用日志的轮转规则是否启用了 size 限制和保留数量。一个常见配置长这样:
# //etc/logrotate.d/ 示例日志配置 /var/log/myapp/*.log { daily rotate 7 maxsize 100M compress delaycompress missingok notifempty copytruncate }这里有几个参数值得解释一下:daily表示每天轮转一次,maxsize 100M表示单文件超过 100MB 也触发轮转,rotate 7表示保留过去 7 份归档,compress会把归档文件压缩,delaycompress则推迟一天压缩以便服务进程还能向最近的归档写入内容,copytruncate用于那些无法自己重新打开日志文件的程序——它先把当前日志复制一份再清空原文件,避免文件被删除后进程句柄失效的问题。搞清楚这些参数,你排错的时候才知道问题出在哪个环节。
3.2 包管理器缓存与不用的软件包:安全回收空间的重头戏
包管理器自带清理动作,用起来几乎没有风险。Debian/Ubuntu 系用:
# 清理 apt 缓存中的旧安装包 apt clean # 只清理已不再安装的软件包缓存 apt autocleanRedHat/CentOS 系用dnf clean all清理缓存。另一个很容易被忽略的是旧内核——内核更新后旧的 kernel 包还在系统里,一堆 initramfs 和 modules 加起来也有几百 MB。Debian/Ubuntu 上用apt autoremove --purge可以把不再需要的依赖和旧内核一并清理,这个命令不会误伤正在使用的内核,因为系统会自动保留当前运行的版本和最近一个可用版本。
经验之谈:包缓存清理是所有清理动作中最稳妥、最没有副作用的一种,适合定期执行。但如果机器处于离线环境,或者你打算近期重装某个包,那么保留缓存有助节省下载时间,这种情况下可以不用 clean,改用 autoclean 只清掉确实无用的一部分缓存。
3.3 容器与镜像清理:区分“可控清理”和“激进清理”
容器环境里,docker system df先看一下哪里占了空间,这个命令会列出镜像、容器、卷、构建缓存各自的占用情况。确认之后,分策略清理:
# 清理停止的容器、悬空镜像、无用网络和构建缓存 docker system prune # 更激进一些:连未使用的镜像一起清理,注意这会删除所有没有被容器引用的镜像 docker system prune -a需要注意-a的差异。docker system prune 只清理被标记为悬空的镜像和停止的容器,相对安全;docker system prune -a 会把所有未被任何运行中容器引用的镜像全部挖出来删除,风险明显上升。如果你只有一个跑着的容器,-a 会把镜像仓库里所有旧版本镜像都删掉,下次要回滚或重新部署就得重新拉取。另外,加了--volumes会连匿名卷一起清理,这个慎用——有状态应用的数据如果放在匿名卷上,删掉就等于丢了数据。
容器日志的合理配置是在 docker 启动参数里加上 log 限制:
# //etc/docker/daemon.json 示例 { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }每次容器写的日志文件超过 50MB 就轮转,最多保留 3 个归档文件。这个配置改完后需要重启 docker 守护进程才生效。已运行容器可以用truncate -s 0 文件路径安全清空日志,这种清空方式不会删除文件本身,进程的写入句柄也不会失效,是处理和容器大日志时最稳的办法。比如容器 id 是 abc123,对应日志路径通常是 /var/lib/docker/containers/abc123/abc123-json.log,直接 truncate 它比删除重建安全得多。
3.4 临时文件与缓存目录:看清 /tmp 的机制再动手
/tmp 的清理,很多时候不需要手动干预,因为系统有 systemd-tmpfiles 机制在定期清理。这里需要注意的地方是:不要把所有临时文件都当成可清理对象。比如某些数据库或缓存服务把临时表空间放在 /tmp 下,盲删会直接导致服务起不来。稳妥做法是先用lsof | grep /tmp检查正在使用 /tmp 下文件的进程,确认没有进程正在使用,再针对性清理特定的过期文件。
另一类容易让人误删的目录是 /var/crash 和 /var/tmp。/var/crash 存放的是崩溃转储文件,如果机器之前发生过内核 panic 或某个应用崩溃,落盘的 core dump 可能非常巨大,但这些文件对排查崩溃原因有用,删之前确认崩溃问题已经调查完毕。/var/tmp 同理,有应用把会话级数据或中间文件放这里,实际清理时需要先看清楚每个大文件由哪个应用创建,别一梭子全删了。
我自己的清理原则可以概括成一句话:先确认责任方,再动手删除。删除任何大文件之前,至少用 lsof 确认没有进程正在使用它;删除任何业务相关的日志或数据文件之前,确认留存期限和备份策略。不要为了贪一时的空间,把事情推到更麻烦的境地。
4. 别等到下次报警:日志轮转与清理机制怎么布置
把眼前的空间问题解决掉,只算完成了一半工作。另一半工作,甚至说更重要的工作,是让类似的告警不再频繁找上门。这涉及到三件事:日志轮转配置要合理、系统本身的清理机制要启用、空间增长要有监控手段。这三件事是同步做的,而不是等出了问题再临时布置。以下几个环节是我实际操作中验证过有效、且不挑机器规模的做法。
4.1 logrotate 和 journald 的占用上限:从源头控制增量
最根本的控制方式就是限制日志总量的上限。journald 这一层,在 /etc/systemd/journald.conf 里设置:
# //etc/systemd/journald.conf 关键参数 SystemMaxUse=500M SystemKeepFree=2G MaxFileSec=1daySystemMaxUse限制了 journal 目录总占用不得超过 500MB,SystemKeepFree保证文件系统至少预留 2GB 空闲空间给其他用途,MaxFileSec按天切分 journal 文件,避免单个文件无限增大。这些配置改完别忘了重启 journald 服务:
systemctl restart systemd-journald传统日志则靠 logrotate 的全局配置。在 /etc/logrotate.conf 里设置默认轮转频率和保留数量,同时确保 /etc/logrotate.d/ 下每个应用都有自己的配置。很多运维新手只关注应用的日志轮转,完全忽略掉系统的 btmp、wtmp、lastlog 这些账号相关日志。特别是 wtmp 和 btmp,在 Fail2ban 之类的工具没有完整配置时,异常登录尝试会在 btmp 里堆积大量数据,某些极端场景半年就能攒出好几个 GB。logrotate 默认会有这些文件的配置,但很多精简安装的机器默认未开启轮转计划,你需要确认一下有没有实际生效。
4.2 包缓存自动清理与临时文件有效期:小处着眼,避免滚雪球
包管里器缓存可以配合定时任务定期清理。Debian/Ubuntu 系常见的做法是把 apt clean 挂到 cron 或者 systemd timer 上,每周跑一次。同样地,docker 环境里也可以定期执行 docker system prune -f,但注意不要加 -a,理由前面说过了。这里提醒一句:如果你的机器上跑着 CI 之类的构建服务,每天有大量镜像和构建缓存产生,建议把构建目录单独挂到独立分区上,避免构建缓存把根分区打满——这种分区上的隔离是解决这类问题的长效机制,比反复清理高效得多。
systemd-tmpfiles 的清理策略也可以在 /etc/tmpfiles.d/ 自定义。比如想让 /tmp 下超过 1 天没访问的文件自动清掉,就放一个配置文件:
# //etc/tmpfiles.d/tmp-clean.conf v /tmp 1777 root root 1d注意这里写的是以天为单位,目录的修改时间超过 1 天才会被清理。如果你有应用往 /tmp 写很重要的中间数据,建议先确认应用的写入频率和生命周期,再把阈值调大或者排除特定目录,别把默认的清理周期压得太激进。
4.3 监控与预警:敏感点在于趋势而不是阈值
把清理机制全部配好之后,剩下的一层防线是监控。监控这层的关键不是等磁盘到 90% 再发告警,而是监控增长趋势,提前预判。大部分团队都会配磁盘使用率告警,但很多没配 inode 和增长速率——真实故障里有相当比例是 inode 先满或者日志一夜之间暴涨引起来的。所以我的建议是至少做三种指标监控:
| 监控指标 | 推荐阈值 | 说明 |
|---|---|---|
| 分区使用率 | 80% 提示、90% 告警、95% 紧急 | 分阶段阈值,给处理留时间窗口 |
| inode 使用率 | 80% 提示、90% 告警 | 防止小文件风暴 |
| 单日空间增量 | 超过 GB 级触发告警 | 捕获日志突增、数据异常写入 |
空间增长速率这个指标尤其重要,因为磁盘使用率是缓慢爬升的,靠固定阈值告警往往发现时已经很晚了;而增长速率一旦出现突刺,往往意味着应用出问题或者日志配置被改过,这时候处理起来比等到 95% 再动手要从容得多。我自己给生产环境的经验是,如果某台机器单日增长超过全分区 5%,就值得拉出来专门看一看了,不要等它慢悠悠地爬到阈值。
4.4 预留分区与扩容策略:最后一招也要提前想好
无论清理和监控做得多么到位,业务增长总有超出预期的时候。在为出现极端情况做准备时,最好的方案是在系统安装阶段就把根分区和其他数据目录分开挂载,给根分区预留足够的空间。如果机器是用 LVM 管理的,还有扩容的路可走:
# 查看卷组是否还有空闲空间 vgs # 将卷组剩余空间扩展到根分区所在的逻辑卷,然后执行扩展 lvextend -l +100%FREE /dev/mapper/vg-root # 扩展文件系统,xfs 用 xfs_growfs,ext4 用 resize2fs resize2fs /dev/mapper/vg-root注意 XFS 文件系统只能扩不能缩,且扩展文件系统的命令和 ext4 完全不同,用错会直接报错。所以我强烈建议在安装系统阶段就留出 LVM,给后续的调整留足余地。如果系统已经用静态分区且无法在线调整,那能做的就是在大目录上做软链接,把 /var/log 这类高频增长的目录迁到独立的数据分区去。这种东西做起来不难,但需要在系统还算健康的时候提前规划,等磁盘全满了再去迁移,操作起来会痛苦很多。
机器上的根分区使用率长期维持在 85% 以下,对我来说是一条心理安全线。高于这个数字,我就会找时间去看看到底是哪个目录在增长,而不是等它主动报警。毕竟磁盘告警这玩意,它不关心你凌晨有没有在睡觉,但它完全可以不出现,只要你把日志轮转配好、缓存定期清、监控跟上趋势,绝大多数空间问题都能被扼杀在早期阶段。Linux 系统管理就是这么回事,功夫下在平时,等出了故障才手忙脚乱,往往是没把机制建起来的代价。