最近连续两周,我都在帮不同环境下的 CentOS 7 机器处理同一个问题:系统盘满了。有的机器 / 分区只有 50G,日志和容器一撑就直接 100%;有的机器是跑了一年多的数据库服务器,/var 下面悄悄涨了 30G 的备份;最夸张的一台是 /var/lib/docker/overlay2 一个目录占掉 80G,历史镜像和容器日志全堆在上面。CentOS 7 虽然已经进入生命周期的尾部,但生产环境里存量依然很大,尤其是当年用 CentOS 7 x86_64 minimal 镜像精简安装的机器,分区方案普遍偏保守,/ 分区太小、/home 和 /var 没有独立拆分,一旦出问题,连执行 shutdown 都可能卡死。这篇把我在实际运维中用到的、踩过坑反复验证过的系统盘清理方法整理成一份汇总,从怎么定位空间占用,到怎么动手清理,再到怎么防止复发,一次讲透。不管你是刚接手一台告急服务器的新手,还是想优化现有清理策略的老手,这篇文章都值得你收藏后边看边操作。
1. 系统盘告急:先判断空间到底被谁吃了
1.1 为什么系统盘总是最先告急
CentOS 7 的默认分区方案里,一般会把根分区留给 / 和 /var,而 /home 单独分出去,甚至很多用 minimal 镜像装出来的机器只有一个 / 分区。这种布局对于跑业务的人来说本身没什么问题,但系统盘的容量会随着运行时间的拉长不断被四类东西消耗掉:日志文件、软件包管理产生的缓存、临时文件、以及应用写入的数据。我这边最常见的场景是 journald 日志一路涨到几个 G,然后 /var/log/messages 又因为 logrotate 配置不对持续膨胀,再加上 yum 操作留了满目录的缓存包,最后的结果就是 df -h 一看,/ 分区 100%,ssh 登录慢得像蜗牛,连敲命令都快没响应。
与其等它满了再去抓瞎,不如先搞清楚几个基础命令,后面所有清理动作都建立在它们的基础上。优先解释这些命令背后的原理,是因为很多新手一上来就 rm -rf 一些看起来像缓存的东西,运气好能临时释放空间,运气不好直接把系统搞挂,所以排查顺序和判断优先级比清理动作本身更重要。我在实际处理告警时,默认遵守一个原则:先定位,再评估,最后才动手。这个顺序看起来慢,但比直接删文件不知道要稳多少倍。
1.2 快速定位大目录的排查命令
定位空间占用,第一步我习惯从根目录开始往下筛。常用组合是:
bash复制df -h /
du -x --max-depth=1 -h / 2>/dev/null | sort -rh | head -20
df 看的是整个分区使用率,du 则是累加计算目录实际占用的块数。加上 -x 参数强制 du 不要跨文件系统,这样可以避免把 /proc、/sys 这些虚拟文件系统也扫进去,扫描速度更快,结果也更准确。2>/dev/null 是忽略权限不足的目录报错,生产环境里总有一些目录当前用户没有权限读,不屏蔽的话输出会特别脏,影响你判断。
如果 du 显示某个目录特别大,就再往下一层重复同样动作,比如 /var 大就执行:
bash复制du -x --max-depth=1 -h /var 2>/dev/null | sort -rh | head -20
这样一层层递进,很快就能锁定是 /var/log 还是 /var/lib 在膨胀。要注意 du 在首次扫描全盘时速度比较慢,尤其是磁盘读写压力大的时候,一般建议在业务低峰执行,避免阻塞 IO。等文件系统缓存热起来之后再扫就快很多,不过不用担心扫两三次会浪费多少时间,系统盘告急的时候,多花两分钟定位比盲目删除省下来的时间多得多。
还有一个小技巧,扫描时可以把结果输出到临时文件里保存,比如:
bash复制du -x --max-depth=2 -h / 2>/dev/null | sort -rh > /tmp/disk_usage_$(date +%F).txt
这样即使你中途断开 ssh,下次登录还能接着看分析结果。
1.3 被删除但仍占用磁盘的文件辨别
这是系统盘清理里最反直觉的一个坑:你明明删掉了大文件,df -h 一看空间还是满的。原因是有进程仍在打开那个文件,文件虽然在目录树里看不到了,但文件句柄还处于打开状态,占用的 block 没有被释放。排查方法是用 lsof:
bash复制lsof +L1 | grep deleted
+L1 表示显示 link count 小于 1 的文件,也就是已经被删除但仍被某些进程占用的文件。看到输出后,确认是哪个进程、哪个路径,再根据实际情况决定是重启这个进程,还是直接 kill 让它重新拉起。最常见的元凶是日志服务、数据库进程和长时间运行的 Java 应用,我遇到过一台机器上 Java 应用把 log 文件删了之后没有 reopen,结果日志继续往已删除句柄里写,空间一直不释放,重启应用后瞬间恢复正常。
所以清理动作做完之后,别急着下结论,先执行 df -h 确认空间确实释放了,这个习惯能帮你少走很多弯路。如果明确看到某个已删除的大文件被 PID 占用,且这个进程不能随便重启,可以尝试通过清空 /proc 下的文件描述符来应急,比如把日志文件重定向为 0 字节,这个操作我会在后面的故障排查章节详细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 日志、缓存与软件包:日常清理的主战场
2.1 清理 journald 日志
systemd-journald 是 CentOS 7 上默认的系统日志收集器,它会长期累积 /var/log/journal 目录下的日志文件。很多人不知道它默认策略是只在磁盘空间达到一定阈值时才做清理,所以在小分区机器上它经常把整个盘吃光。有一个真实的场景,某台机器上 /var/log/journal 堆了 7G,而整块系统盘才 40G,df -h 显示 / 分区 100%,ssh 连接都建立不起来,最后还是从虚拟化控制台进去才恢复了系统。
先查看当前占用:
bash复制journalctl --disk-usage
如果占用已经到了几个 G,可以按时间或体积来清理。按时间保留 7 天数据:
bash复制journalctl --vacuum-time=7d
按体积限制到 100M:
bash复制journalctl --vacuum-size=100M
这两个命令都是不需要重启服务的,执行完即时释放。更根治的办法是修改 /etc/systemd/journald.conf,把 SystemMaxUse 设置成一个合理值,比如 500M,然后重启 systemd-journald:
bash复制sed -i 's/#SystemMaxUse=/SystemMaxUse=500M/' /etc/systemd/journald.conf
systemctl restart systemd-journald
设置好之后,日志总量会被牢牢控制在 500M 以内,属于一劳永逸的操作。不过要注意,生产环境里如果需要保留比较长的审计日志,别把 SystemMaxUse 调得太小,否则出问题想回溯日志发现全没了,那种感觉比磁盘满还要痛苦。个人建议是至少保留 200M 到 500M 的空间给 journald,这样既能覆盖短期排障需求,又不会失控。
2.2 压缩与轮转系统日志
/var/log 下面还有 messages、secure、cron、maillog 这些传统日志,因为 CentOS 7 会默认启用 logrotate,所以大多数情况下它们会被按周轮转并 gzip 压缩,保留 4 份。但实际运维中我经常发现配置失效的情况,比如 rsyslog 新增的自定义日志文件没有写在 /etc/logrotate.d/ 里,又比如 cron 服务长期异常导致 logrotate 没有按时执行。
查 logrotate 有没有真的跑起来,可以直接看:
bash复制cat /var/lib/logrotate/logrotate.status
看到某些文件 last rotated 日期非常久远,就说明轮转出了问题。手动执行一次:
bash复制logrotate -f /etc/logrotate.conf
-f 强制轮转,执行完再看空间。如果某个日志文件增长极快,比如一个服务每天写几个 G 的 debug 日志,我会直接在 /etc/logrotate.d/ 下给这个服务单独建一个配置,把 daily 改成 hourly,并加上 size 阈值,比如:
code复制/var/log/example/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
size 100M
}
copytruncate 这个参数在应对那些不断打开文件句柄的进程时特别有用。普通 rename 加 create 的方式会让进程继续往旧文件写,而 copytruncate 会先复制再清空原文件,虽然会丢一点点写入内容,但在磁盘快满的紧急场景下它是最安全的止损手段。我自己在 Nginx、Tomcat、Java 应用的日志轮转里都用了 copytruncate,实测下来没有出现过文件句柄错乱的问题。
2.3 清理 yum 缓存和冗余内核
yum 在安装包时默认会把下载的 rpm 缓存到 /var/cache/yum,时间一长,这里累积几个 G 是很正常的。直接清:
bash复制yum clean all
对于用最小化安装的机器,如果常用工具是通过 yum 装的,这个目录涨得会更快,所以建议定期清理。还有一个主力占用源是内核,每次 yum update 都会保留旧内核,/boot 分区空间不足时旧内核就是首要清理对象。我在一台跑了两年的机器上见过八个内核版本并存,/boot 直接被塞满。
查看已安装内核:
bash复制rpm -qa | grep kernel | sort -V
系统为了开机回滚,会默认保留最近几个版本的内核,但这在小分区服务器上非常奢侈。删除旧内核可以用:
bash复制yum remove kernel-3.10.0-xxx.el7.x86_64
注意千万不要动当前正在运行的内核版本,用 uname -r 先确认一下。如果系统里有太多旧内核需要一次清掉,可以用 package-cleanup 工具:
bash复制yum install yum-utils -y
package-cleanup --oldkernels --count=2
--count=2 表示只保留最近两个版本,既能保证回滚能力,又能释放 /boot 和 /usr 的空间。这个工具会自动识别当前运行内核并跳过,所以相对安全,不过还是建议在删除前敲一下 uname -r,确认自己的操作对象。
2.4 /tmp 与 /var/tmp 临时文件处理
/tmp 和 /var/tmp 是另一个容易被忽视的堆积点。CentOS 7 的 systemd-tmpfiles 会定期清理 /tmp 下超过 10 天的文件,但 /var/tmp 默认保留 30 天,而且如果你跑的应用程序在这个目录里留下了大量中间文件,占用同样很可观。尤其在跑 Java 程序或者安装包解压路径指向 /tmp 的时候,几十万个碎片文件轻轻松松就能吃掉好几 G 空间。
清理临时目录之前,最好先确认没有正在使用的文件。例如数据库临时表、Java 应用解压目录等,直接 rm 风险不小。我的习惯是先看目录下最大文件的修改时间,确认没有近期活跃的文件再动手:
bash复制find /tmp /var/tmp -type f -mtime +10 -exec rm -f {} \;
这条命令是安全优先,只删修改时间超过 10 天的文件。如果需要更激进,可以改成 -mtime +1 并在业务低峰执行。特别提醒,不要让这条命令在运行时打断正在写 /tmp 的下载任务或会话文件,否则会出现一些莫名其妙的应用报错。我碰到过一个场景,某程序的会话文件放在 /tmp 下,清理脚本一跑,所有在线会话全部失效,业务方反馈问题后我才注意到是清理任务惹的祸。所以这类命令最好加上 -mtime 限制,并且把执行时间放在凌晨。
3. 容器与中间件环境下的特殊清理
3.1 Docker 目录膨胀的处理
现在很多 CentOS 7 机器上都跑着 Docker,而 Docker 默认的数据目录 /var/lib/docker 是在系统盘上的。我见过一台机器,/var/lib/docker/overlay2 占掉 80G,原因是历史镜像长期不清理,以及某个容器日志没有限制大小。用 docker system df 看一下各维度占用:
bash复制docker system df
它会列出镜像、容器、数据卷、构建缓存各自占用的空间。接下来按需清理:
bash复制docker container prune # 清理已停止的容器
docker image prune -a # 清理无标签镜像
docker system prune -a # 停止的容器+无用网络+无标签镜像+构建缓存
不过注意,docker system prune -a 会删除所有未被运行中的容器引用的镜像,虽然能释放大量空间,但如果你待会儿要重新部署某个服务,可能需要重新拉镜像,所以别一上来就 -a。我一般先跑 docker image ls 看一眼还有哪些镜像,确认没有需要保留的再执行。
容器日志的清理更容易被忽略。在没有配置 json-file 大小限制的情况下,每个容器的标准输出日志都会追加到 /var/lib/docker/containers/容器ID/*-json.log 里。清理起来也简单:
bash复制truncate -s 0 /var/lib/docker/containers/*/*-json.log
truncate 是清空而不是删除,这样可以避免 Docker 进程继续往已删除的 inode 写入导致空间不释放。更合理的做法是在 /etc/docker/daemon.json 里加上日志轮转配置:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
然后重启 Docker。这样每个容器日志最多膨胀到 150M,不会再有日志撑爆 /var 的烦恼。如果你用的是 Docker Compose 管理,也可以在 compose 文件里给每个服务单独加 logging 配置,效果一样。
3.2 数据库和中间件日志的治理
CentOS 7 上最常见的数据库是 MySQL 和 MariaDB,它们的 binlog 和错误日志都很容易吃空间。尤其是开启 binlog 的服务器,如果 expire_logs_days 没设置,binlog 会一直累积,几年不清理能占几百 G。
查看 binlog 占用和清理方式:
bash复制show binary logs;
PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;
更省心的做法是在 my.cnf 里加入:
code复制expire_logs_days = 7
max_binlog_size = 256M
这样 MySQL 会自动清理 7 天前的 binlog,配合 InnoDB 的 redo log 自动切换,就不会出现单个 binlog 无限增长的情况。MySQL 8.0 新语法改成了 binlog_expire_logs_seconds,但 CentOS 7 上跑 MariaDB 或 MySQL 5.7 还是 expire_logs_days 为主。
中间件这边,Nginx、Tomcat 的 access log 和 catalina.out 也是大户。Nginx 默认会把访问日志写到 /var/log/nginx/access.log,没有 logrotate 配置的话单文件能长到几十 G。我会在 /etc/logrotate.d/nginx 里加入 size 100M 的轮转策略,并设置 delaycompress + copytruncate,避免 Nginx 句柄切换问题。Tomcat 的 catalina.out 问题类似,很多应用不配置 maxage 和 maxsize,logrotate 里又没写,最终结果就是整个 / 被撑满。这里加一条配置就能解决问题:
code复制/var/log/tomcat/catalina.out {
daily
copytruncate
rotate 10
compress
missingok
notifempty
}
改完配置后用 logrotate -f /etc/logrotate.conf 触发一次,确认没有任何报错,再观察两天,基本就能把日志增长问题压住。除了数据库和 Web 服务,消息队列、缓存服务同样需要关注日志轮转,思路是一致的,就不逐一展开了。
3.3 备份文件与 core dump 的处置
数据库备份、站点打包、镜像导出,这类文件最容易躺在 /root、/home、/opt 和 /var/backups 里。因为备份文件体积大,动辄几十 G,且生成它们的进程只运行一次就退出,不占用文件句柄,所以定位和删除都相对简单。
我处理这种问题时会先看 /root 和 /home 下有没有 .sql、.tar.gz、*.zip:
bash复制find /root /home -maxdepth 2 -type f \( -name "*.sql" -o -name "*.tar.gz" -o -name "*.zip" -o -name "*.log" \) -size +100M -exec ls -lh {} \;
确认是历史备份后,再和业务方确认是否可以删除。如果没有确认条件,我的建议是先移动到另一块独立磁盘或者对象存储,而不是直接 rm,毕竟备份文件在某些场景下是唯一的自救手段。core dump 文件也经常占据大块空间,它们通常出现在进程工作目录、/var/crash 或者以 core.* 命名的根目录文件。可以临时关闭核心转储来避免继续生成:
bash复制ulimit -c 0
同时清理已有的 core 文件。内核参数里如果开启了 kernel.core_pattern 默认写入当前目录,生产环境可以直接改成禁止写入,或者写到一个有空间上限的独立分区,这样以后再出问题不会把系统盘搞满。
4. 边界操作:不能随便删的内容与安全迁移
4.1 没有独立分区时的取舍策略
很多用 CentOS 7 minimal 安装的机器,安装时图省事,整个磁盘只分了一个 / 分区,这是后面所有磁盘问题的根源。这时候清理动作再激进,也只是缓解,时间一长还是会满。如果分区布局已定,能做的优化首先是降低文件系统保留块比例。ext4 默认给 root 保留 5% 的块,用于 fsck 和紧急情况下 root 用户操作。如果一个 2T 的盘,5% 就是 100G,对系统盘来说这个保留空间太奢侈。可以用 tune2fs 调整:
bash复制tune2fs -m 1 /dev/xxx
把保留块降到 1% 甚至 0%,生产环境建议至少保留 1%。这个操作不会释放逻辑上的空间,但它能让你实际可用的空间变大,在磁盘容量只剩几个 G 的时候,这多出来的几个百分点就是救命的。还有一点是挂载参数 noatime。在 /etc/fstab 里给根分区加上 noatime 后,每次读取文件时系统不再更新访问时间戳,既能减少 IO,也能减少元数据写入负担。这类优化不会直接清理出空间,但能让磁盘生命周期变长,属于预防式清理的范畴。
4.2 把大目录迁移到独立磁盘或数据盘
如果机器上有第二块数据盘,最合理的长期方案是把 /var 或 /home 迁移过去。下面以迁移 /var/lib/mysql 为例,前提是已经新挂载了一块数据盘到 /data:
bash复制systemctl stop mysqld
rsync -avz /var/lib/mysql/ /data/mysql/
mv /var/lib/mysql /var/lib/mysql.bak
mkdir /var/lib/mysql
mount --bind /data/mysql /var/lib/mysql
mount --bind 是把数据盘上的目录绑定到原来路径,对应用来说路径没变,但空间已经落到新盘上。为了让重启后依然生效,需要在 /etc/fstab 里加一行:
code复制/data/mysql /var/lib/mysql none bind 0 0
确认启动正常后,再删除 /var/lib/mysql.bak。这里有个容易被忽略的坑:rsync 完一定要记得先停服务再复制,不然复制过程中数据库还在写文件,拷贝出来的数据不一致,启动时各种报错。另一个坑是复制时保留权限,rsync 的 -a 参数已经带了 -o -g -p,但如果你源目录有特殊 ACL 或 selinux 上下文,复制后还得 restorecon 一下,否则 MySQL 起不来。如果可迁移目录是 /home,操作逻辑一样。迁移之前还可以考虑把 /home 的用户数据压成一个 tar 包再搬,这样数据一致性更容易保证,缺点是占用临时空间更大,适合数据量不大的场景。
4.3 inode 耗尽的排查与清理
空间不足还有一个变种:明明 df -h 显示还有空间,但应用报“No space left on device”。这是 inode 满了,准确说是文件数量到达了文件系统的上限。小文件特别多的目录,比如邮件队列 /var/spool/mqueue、临时文件目录、以及某些应用的缓存目录,最容易触发这个问题。
排查命令:
bash复制df -i /
看 IUse% 是不是到了 100%。定位哪个目录的文件数最多,可以用:
bash复制find / -xdev -type f | cut -d'/' -f2 | sort | uniq -c | sort -rn | head -10
定位到具体目录之后,再深入一层看是哪个子目录堆了大量小文件。我遇到最典型的案例是某个推送服务把每次请求的响应快照都写到一个目录,一个月积了几十万个小文件,直接把 inode 耗光。清理方式同样是删旧文件,但删除大量小文件时建议分批删,比如:
bash复制find /var/lib/example -type f -mtime +30 -delete
一次性删除几十万文件会让 du 和 df 在统计时出现短暂的元数据高压,业务高峰期别干这件事。还有一点,删除大量小文件后,df -i 的释放速度没有 df -h 那么快,因为 inode 的回收需要文件系统同步元数据,耐心等一下再确认即可。
5. 常见问题与排查技巧实录
5.1 清理后空间没释放怎么办
这个问题在 1.3 里已经提过一部分,这里再补充一个更完整的排查顺序。清理动作做完后 df -h 还显示满的,先依次确认三件事:第一,有没有进程持有已删除文件的句柄,用 lsof +L1 | grep deleted 查;第二,删除的是不是挂载点本身的文件,如果你删的是 /var/log 而 /var/log 恰好挂在独立分区上,你在外面删目录当然无效;第三,有没有文件系统自身延迟回收,ext4 在异常断电或强制挂载后有时需要 fsck 才能回收孤儿块,这个情况比较少见,但遇到的话只能找维护窗口做 fsck。
大部分情况是第一种,处理方式就是找回持有句柄的进程。如果那个进程是核心业务不能重启,可以在 /proc 目录下找到 fd 指向,比如 /proc/PID/fd/2,然后用 cat /dev/null > /proc/PID/fd/2 把文件清空,也能释放空间。执行这个操作前,先确认 /proc/PID/fd/2 确实指向你要清空的大文件,别一不小心把标准输出给清了,方向错了就麻烦了。
5.2 哪些内容绝对不能直接 rm
这里列一张速查表,都是我在生产环境里撞过墙或者听同行踩过坑总结出来的:
| 路径或内容 | 为什么不能直接删 | 正确做法 |
|---|---|---|
| /var/lib/mysql 或表空间文件 | 删除后数据不可恢复 | 先停库,确认备份,再删或迁移 |
| /etc 下的任何文件 | 系统配置丢失会导致起不来 | 改动前备份,用 mv 改名而不是 rm |
| /proc、/sys、/dev | 虚拟文件系统,删了系统立刻出问题 | 永远别碰 |
| 正在运行的业务日志 | 删除后句柄不释放,空间照样占着 | 用 truncate -s 0 清空 |
| 容器内部数据卷 | 外部删文件可能让容器内进程崩溃 | 用 docker volume 管理 |
| 未确认的备份文件 | 可能是唯一备份 | 先转移到其他盘,确认不需要再删 |
| /boot 下正在使用的内核 | 删了启动不了 | 用 uname -r 确认当前内核 |
这张表的核心逻辑一句话:凡是你不知道它是干什么的文件,都先不要删;凡是你知道它是干什么但别人可能也依赖的文件,都先确认再删。运维里“快”不是第一位的,“稳”才是。
5.3 建立周期性清理机制
清理完一次盘,如果不做任何预防,三个月后又得从头来一遍。长期维护的做法是写一个清理脚本放进 crontab。我这边一个相对通用的脚本会做这几件事:清理 yum 缓存和 journald 旧日志、压缩并轮转 /var/log 下的旧日志、删除超过 30 天的 /tmp 文件、清理 Docker 构建缓存和无标签镜像。
脚本大概长这样:
bash复制#!/bin/bash
# system_clean_daily.sh
yum clean all >/dev/null 2>&1
journalctl --vacuum-size=200M >/dev/null 2>&1
find /tmp /var/tmp -type f -mtime +30 -delete 2>/dev/null
logrotate -f /etc/logrotate.conf >/dev/null 2>&1
docker system prune -f >/dev/null 2>&1
然后加入 crontab:
bash复制0 4 * * * /root/scripts/system_clean_daily.sh
凌晨 4 点执行,避开业务高峰。要注意脚本里的 docker prune 不要加 -a,避免把无标签但有引用的镜像也删掉。另外,脚本执行完建议写一条日志到 /var/log/system_clean.log,方便后续排查空间到底什么时候被释放的。你的生产环境可能比这个复杂,但套路就是先明确哪些能自动清理,哪些必须人工确认,然后把能自动的交给 cron。如果机器之前没装邮件通知,也可以在脚本最后加一行 echo 到控制台,配合 crond 的 MAILTO 配置,每天给自己发一封清理结果邮件,这样即使出了异常也能及时发现。
最后聊两句个人体会。我在处理系统盘问题上最大的收获,不是学会用多少个命令,而是养成了“先看情况、再动手”的习惯。很多故障是删出来的,尤其是生产环境,你对一个文件的理解不够深,就不要轻易下手。如果你只是第一次接触 CentOS 7 清理,建议先在测试机器上完整跑一遍上面的命令,把 du、lsof、journalctl 这几个命令的输出看熟,再上生产。等哪一天你遇到一台磁盘满的机器,能够从容地一层层剥开问题,你会感谢当初认真踩过这些坑的自己。
