磁盘空间耗尽,大概是服务器运维中最让人头疼的问题之一。一旦写满,应用日志无法写入、新文件创建不了、数据库刷盘失败,甚至 SSH 都连不上——典型症状就是满屏的 No space left on device。更麻烦的是,空间到底被谁吃了往往并不直观:有时候日志文件明明删了,进程还抓着文件描述符不放,空间死活回不来。清理也绝不是见文件就删那么简单,得搞清楚哪些能动、哪些正被占用、删掉会有什么后果。
本文以 RHEL 9、AlmaLinux 9 和 Ubuntu 22.04 LTS 为测试蓝本,从文件系统和 LVM 两个维度,系统梳理 Linux 磁盘管理机制、空间占用分析方法、常见占用来源、清理策略以及预防措施。日志、数据库、临时文件、缓存……这些常见坑,我们一个一个填。
1. 磁盘空间基础概念
1.1 磁盘空间相关概念
Linux 里和磁盘空间相关的几个关键概念:
文件系统空间(Filesystem level):文件系统层面的可用空间,df 命令看到的就是它,也是应用直接能用的部分。
逻辑卷空间(LVM level):若用 LVM 管理磁盘,逻辑卷的空间可能比物理卷小,或者逻辑卷已扩容但文件系统还没跟上。
物理磁盘空间(Physical disk level):硬盘分区的实际大小,和分区表、RAID 配置挂钩。
inode 数量:文件系统不光限制空间,还限制 inode 数量。inode 存的是文件元数据,一旦 inode 耗尽,即便还有空间也创建不了新文件。
block 大小:文件系统的最小分配单元,通常 4 KB。写个 1 字节的小文件也要吃掉 4 KB,这也是小文件一多空间“虚高”的根本原因。
1.2 查看磁盘空间状态
# 查看所有文件系统的空间使用情况
df -h
# 查看 inode 使用情况
df -i
# 以 inode 和空间占用排序
df -i
df -h
# 查看特定目录的磁盘使用(逐层深入)
du -sh /var/*
du -h --max-depth=1 /var
# 找出大文件
find / -type f -size +100M -exec ls -lh {} \;
1.3 理解 df -h 输出
$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 3.9G 0 3.9G 0% /dev/shm
/dev/sda1 100G 45G 55G 45% /
/dev/mapper/vg-root 200G 180G 20G 90% /data
- Size:文件系统总大小
- Used:已使用空间
- Avail:可用空间
- Use%:使用率
- Mounted on:挂载点
当 Use% 冲到 90% 以上就得警惕了,超过 95% 必须立刻出手。Avail 显示的是 root 和普通用户总共能用的空间。
2. 快速定位占用来源
2.1 分层排查法
别上来就乱翻,按从大到小的层次来:
# 第一层:确定哪个挂载点满了
df -h
# 第二层:确定哪个目录占用最多
du -sh /*
du -h --max-depth=1 / | sort -hr | head -10
# 第三层:进入占用最多的目录继续排查
du -h --max-depth=1 /var
du -h --max-depth=1 /var/log
2.2 快速定位大文件
磁盘使用率已经飙到 95% 以上,急需找到能马上清理的大文件:
# 查找大于 500MB 的文件
find / -type f -size +500M -exec ls -lh {} \; 2>/dev/null
# 查找特定目录下大于 100MB 的文件
find /var -type f -size +100M -exec ls -lh {} \; 2>/dev/null
# 按大小排序查看目录内容
du -ah /var | sort -rh | head -20
# 只看文件不看目录
du -ah /var | grep -v '/$' | sort -rh | head -20
2.3 查找被删除但仍被进程占用的文件
这是最阴险的情况:文件删了,空间却不见释放。
# 方法一:查看已删除但未释放的空间
lsof +L1
# 方法二:查看所有打开文件的进程
lsof | grep deleted
# 方法三:查看特定进程的打开文件
ps aux | grep process_name
ls -la /proc/PID/fd | grep deleted
为什么会这样:进程打开文件时,内核会分配一个文件描述符。哪怕在文件系统层面把文件删了,只要进程还攥着这个 FD,空间就不会释放。常见场景:logrotate 轮转后,Nginx 或应用仍然向旧的 FD 里写数据。
解决方法:重启持有 FD 的进程,或者发信号让它重新打开日志。比如 Nginx:
# 对于 Nginx,重新打开日志
nginx -s reopen
# 或者向 master 进程发送信号
kill -USR1 $(cat /var/run/nginx.pid)
# 查看哪些文件描述符被标记为 deleted 但仍被占用
lsof +L1 | grep -v "memfd"
3. 日志文件排查与清理
3.1 日志文件空间占用分析
日志绝对是磁盘杀手榜的常客。先看看它吃了多少:
# 查看 /var/log 目录总大小
du -sh /var/log
# 查看 /var/log 下各子目录的大小
du -h /var/log | sort -rh | head -20
# 查看具体的日志文件大小
ls -lhS /var/log/*.log
# 统计各日志文件的行数
wc -l /var/log/*.log
3.2 常见日志文件说明
Linux 系统和常用服务的日志“势力范围”一览:
| 路径 |
内容 |
风险 |
| /var/log/messages |
系统核心日志 |
正常不会太大 |
| /var/log/secure |
安全和认证日志 |
SSH 暴力破解可能让它疯长 |
| /var/log/maillog |
邮件服务日志 |
看邮件量 |
| /var/log/yum.log |
yum 操作日志 |
通常很小 |
| /var/log/cron |
定时任务日志 |
通常很小 |
| /var/log/dmesg |
启动时的内核日志 |
由 ring buffer 决定大小 |
| /var/log/nginx/access.log |
Nginx 访问日志 |
可以非常大 |
| /var/log/nginx/error.log |
Nginx 错误日志 |
除非持续报错,一般不大 |
| /var/log/mysql/error.log |
MySQL 错误日志 |
通常不大 |
| /var/log/mysqld.log |
MySQL 通用日志 |
开启后可能很大 |
| /var/log/postgresql/postgresql-*.log |
PostgreSQL 日志 |
视日志级别而定 |
| /var/log/httpd/ 或 /var/log/apache2/ |
Apache 日志 |
可以非常大 |
| /var/log/kern.log |
内核日志 |
通常不大 |
3.3 安全清理日志文件
千万别直接用 rm 干掉正在写入的日志,否则又会掉进那个“删了不释放”的坑。正确的姿势是:
对于支持信号刷新日志的进程:
# Nginx:通知重新打开日志文件
nginx -s reopen
# 或者发送信号
kill -USR1 $(cat /var/run/nginx.pid)
# 然后删除旧日志文件
rm /var/log/nginx/access.log.1
对于不支持的进程,用 truncate 清空内容,保留文件描述符:
# 清空文件内容但保留文件描述符
truncate -s 0 /var/log/nginx/access.log
# 或者使用 : > 语法
: > /var/log/nginx/access.log
对于已经切割出来的旧日志,可以直接删:
# 删除旧的日志文件
rm /var/log/nginx/access.log.2026-04-*.gz
# 删除 7 天前的日志
find /var/log -name "*.gz" -mtime +7 -delete
# 删除 30 天前的日志
find /var/log -name "*.log.*" -mtime +30 -delete
3.4 日志切割配置检查
检查 logrotate 配置,确保日志能被自动、正确地切割:
# 查看 logrotate 配置
cat /etc/logrotate.conf
cat /etc/logrotate.d/nginx
# 示例配置
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 nginx nginx
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
关键参数说明:
- daily/weekly/monthly:切割频率
- rotate N:保留 N 份历史日志
- compress:压缩旧日志
- delaycompress:延迟压缩,最近一份不压
- notifempty:空文件不切割
- postrotate/endscript:切割后执行的命令,通常通知进程重新打开日志
手动执行 logrotate 调试:
# 强制执行一次 logrotate(加 -f 强制)
logrotate -f /etc/logrotate.conf
# 查看 logrotate 状态
cat /var/lib/logrotate/status
# 测试配置是否正确
logrotate -d /etc/logrotate.d/nginx
4. 数据库文件空间管理
4.1 MySQL/MariaDB 空间分析
# 登录 MySQL 查看各数据库大小
mysql -e "SELECT table_schema AS 'Database', ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' FROM information_schema.tables GROUP BY table_schema;"
# 查看各表的详细大小
mysql -e "SELECT table_schema, table_name, ROUND(data_length / 1024 / 1024, 2) AS data_mb, ROUND(index_length / 1024 / 1024, 2) AS index_mb FROM information_schema.tables ORDER BY data_length DESC LIMIT 20;"
# 查看 binlog 占用空间
mysql -e "SHOW BINARY LOGS;"
# 查看 innodb 文件大小
ls -lh /var/lib/mysql/*.ibd
4.2 MySQL binlog 清理
MySQL binlog 记录所有数据变更,是复制和恢复的基石,但胃口也不小:
# 查看当前的 binlog 配置
mysql -e "SHOW VARIABLES LIKE 'expire_logs_days';"
mysql -e "SHOW VARIABLES LIKE 'max_binlog_size';"
# 手动删除 binlog(指定到某个 binlog 文件)
mysql -e "PURGE BINARY LOGS TO 'mysql-bin.000123';"
# 删除某个时间之前的 binlog
mysql -e "PURGE BINARY LOGS BEFORE '2026-04-01 00:00:00';"
重要:删 binlog 前必须确认从库已经全部接收,否则复制会中断。主从架构下务必在主库执行。
设置自动清理:
# 在 my.cnf 中设置
expire_logs_days = 7
max_binlog_size = 100M
4.3 PostgreSQL 空间分析
# 查看各数据库大小
psql -c "SELECT pg_database.datname, pg_size_pretty(pg_database_size(pg_database.datname)) FROM pg_database ORDER BY pg_database_size(pg_database.datname) DESC;"
# 查看各表大小
psql -c "SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC LIMIT 20;"
# 查看 WAL 文件占用
ls -lh $PGDATA/pg_wal/
du -sh $PGDATA/pg_wal/
4.4 PostgreSQL WAL 清理
PostgreSQL 的 WAL(Write-Ahead Log)同样是大户头:
# 检查 wal 保留配置
psql -c "SHOW wal_keep_size;"
psql -c "SHOW max_wal_size;"
# 手动 checkpoint 并清理 WAL
psql -c "CHECKPOINT;"
# 设置 WAL 保留大小(在 postgresql.conf 中)
wal_keep_size = 1GB
max_wal_size = 2GB
# 查看归档状态
psql -c "SELECT * FROM pg_stat_archiver;"
4.5 MongoDB 空间分析
# 查看数据库列表及大小
mongosh --quiet --eval "db.adminCommand('listDatabases')" | grep -E "name|sizeOnDisk"
# 查看集合大小
mongosh --quiet --eval "db.getCollectionNames().forEach(function(c){ var s=db[c].stats(); print(c + ': ' + (s.size/1024/1024).toFixed(2) + ' MB'); });"
# 查看 oplog 大小
mongosh --quiet --eval "db.oplog.rs.stats().maxSize / 1024 / 1024"
5. 临时文件和缓存清理
5.1 /tmp 目录清理
系统重启时 /tmp 通常会被清空,但长年不动的服务器上,它也可能堆满杂物:
# 查看 /tmp 大小
du -sh /tmp
# 查看 /tmp 中最占用空间的文件和目录
find /tmp -type f -exec ls -lh {} \; | sort -k5 -rh | head -20
# 查找过于老旧的文件(30 天未访问)
find /tmp -atime +30 -type f -ls
# 查找临时文件(按大小排序)
find /tmp -type f -printf "%s %p\n" | sort -rn | head -20
5.2 系统缓存清理
Linux 会用空闲内存做文件缓存,一般无需手动干预。但排查时可以 sync 确保数据落盘后再 drop cache:
# 查看内存和缓存使用
free -h
# 同步文件系统确保数据落盘
sync
# 谨慎操作:手动释放 pagecache
echo 3 > /proc/sys/vm/drop_caches
警告:生产环境执行 drop_caches 可能造成性能抖动,通常只在排查问题时用用。
5.3 Yarn/NPM/Maven 等包管理器缓存
开发环境和构建服务器上,这些缓存经常暗藏杀机:
# Yarn 缓存
yarn cache dir
yarn cache clean
# NPM 缓存
npm cache ls
npm cache clean --force
# Maven 缓存
du -sh ~/.m2/repository
rm -rf ~/.m2/repository/*
# PIP 缓存
du -sh ~/.cache/pip
rm -rf ~/.cache/pip
5.4 Docker 资源清理
Docker 存储占用往往是头号磁盘杀手之一:
# 查看 Docker 磁盘使用
docker system df
# 详细分析各类资源
docker system df -v
# 清理未使用的资源
docker system prune -a # 删除所有未使用的镜像、容器、网络
docker container prune # 删除已停止的容器
docker image prune -a # 删除未使用的镜像
docker volume prune # 删除未使用的卷
docker network prune # 删除未使用的网络
# 清理特定时间之前的资源
docker system prune --filter "until=168h" # 删除 7 天前的资源
5.5 旧内核和已卸载包清理
系统更新后,旧内核赖着不走,几百兆的空间就这么被占了:
# 查看已安装的内核
dpkg --list | grep linux-image
rpm -qa | grep kernel
# Ubuntu/Debian 清理旧内核
apt-get autoremove -y
# 或者使用 purge-old-kernels(如果安装了 byobu)
purge-old-kernels --keep 2
# RHEL/CentOS 清理旧内核
dnf remove $(dnf repoquery --installonly --latest-limit=-2 -q)
# 或者
package-cleanup --oldkernels --count=2
6. inode 耗尽问题
6.1 inode 概念
inode 存储的是文件元数据,每个文件对应一个 inode。一旦 inode 用光,就算磁盘还有空间也白搭。
# 查看 inode 使用情况
df -i
# 查看特定文件系统的 inode 数量
df -i /
# 查看 inode 使用最多的目录
find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -10
6.2 大量小文件导致 inode 耗尽
常见的“罪魁祸首”:邮件队列(/var/spool/mail/)、海量缓存文件、Nginx 访问日志被切成无数小文件。
# 统计目录下文件数量
find /var/spool -type f | wc -l
# 查看某个目录的文件数量分布
for dir in /var/*; do echo -n "$dir: "; ls -1A "$dir" 2>/dev/null | wc -l; done
# 查找 inode 占用最高的目录
find / -xdev -type d -exec sh -c 'echo "$(ls -1A "$1" 2>/dev/null | wc -l) $1"' _ {} \; | sort -rn | head -10
6.3 解决 inode 耗尽
邮件队列导致的话:
# 查看邮件队列大小
ls -lh /var/spool/mail/
# 清理旧邮件(需确认)
find /var/spool/mail -type f -name "*" -mtime +30 -delete
创建文件系统时可以指定 inode 密度:
# 创建时指定
mkfs.ext4 -i 8192 /dev/sdb1 # 每 8KB 一个 inode
7. LVM 环境下的问题
7.1 LVM 概念回顾
LVM(Logical Volume Manager)把物理磁盘抽象为卷组(VG)和逻辑卷(LV),让空间管理灵活得多:
- PV(Physical Volume):物理卷,对应分区或整盘
- VG(Volume Group):卷组,多个 PV 组成
- LV(Logical Volume):逻辑卷,从 VG 中划分
7.2 LVM 空间问题排查
# 查看卷组空间使用
vgdisplay
# 查看逻辑卷信息
lvdisplay
# 查看每个 LV 的空间使用
lvs -o lv_name,lv_size,data_percent,metadata_percent
# 查看 PV 使用情况
pvs
pvdisplay
7.3 扩展逻辑卷
文件系统空间吃紧但 VG 里还有余粮时:
# 扩展逻辑卷
lvextend -L +10G /dev/mapper/vg-root
# 扩展文件系统(ext4)
resize2fs /dev/mapper/vg-root
# 扩展文件系统(xfs)
xfs_growfs /data
7.4 扩展卷组
当 VG 自身也“弹尽粮绝”时,需要给它加盘:
# 创建新分区或磁盘
# 创建物理卷
pvcreate /dev/sdb1
# 将新 PV 加入 VG
vgextend vg_name /dev/sdb1
# 扩展 LV
lvextend -L +50G /dev/mapper/vg-root
# 扩展文件系统
resize2fs /dev/mapper/vg-root
7.5 缩小逻辑卷(谨慎操作)
缩小 LV 要先卸载,风险极高,生产环境绝对不推荐。如果非要操作:
# 卸载文件系统
umount /data
# 检查文件系统
e2fsck -f /dev/mapper/vg-data
# 缩小文件系统
resize2fs /dev/mapper/vg-data 50G
# 缩小 LV
lvreduce -L 50G /dev/mapper/vg-data
# 重新挂载
mount /dev/mapper/vg-data /data
8. 分区表和文件系统修复
8.1 检查文件系统错误
清理磁盘前,最好先查查文件系统有没有“内伤”:
# 检查文件系统(必须先卸载)
umount /data
e2fsck -f /dev/mapper/vg-data
# 检查并修复(自动修复模式)
e2fsck -p /dev/mapper/vg-data
注意:千万别在已挂载的文件系统上跑 e2fsck(除非只读模式)。生产环境先用 fsck -n 预览一下。
8.2 修复损坏的 inode
如果 inode 表损坏导致空间统计不准:
# 显示 inode 使用情况
dumpe2fs /dev/sda1 | grep -E "Inode size|Inode count|Inode blocks per group|Inodes per group"
# 检查损坏的 inode
badblocks /dev/sda1
8.3 修复 LVM 配置损坏
如果 LVM 配置坏了,数据仍然可以抢救:
# 扫描物理卷
pvscan
# 激活所有卷组
vgchange -ay
# 查看 LVM 状态
lvs -a
9. 紧急处理流程
当磁盘已满导致服务不可用时,按下面这个顺序快速“灭火”。
第一步:登录服务器
如果 SSH 已经卡死,试试:
# 使用密钥登录
ssh -i ~/.ssh/id_rsa user@server
# 如果 SSH 会话还活着但命令执行不了,先试个简单的
df -h /
# 要是根目录满了,先清理
ls -la /tmp
第二步:定位最紧急的占用
# 查看磁盘使用情况
df -h
# 查找大文件
find / -xdev -type f -size +100M -exec ls -lh {} \; 2>/dev/null
# 查看已删除但未释放的文件
lsof +L1
第三步:立即清理
# 清理临时文件
rm -rf /tmp/*
rm -rf /var/tmp/*
# 清理旧日志(如果进程支持)
: > /var/log/messages
: > /var/log/secure
# 清理包管理器缓存
yum clean all
apt-get clean all
# 清理 Docker(谨慎)
docker system prune -f
第四步:通知相关服务
清理完,别忘了让日志服务恢复正常:
# 重启日志服务
systemctl restart rsyslog
systemctl restart systemd-journald
# 重启应用(如果需要)
systemctl restart nginx
systemctl restart myapp
10. 预防措施
10.1 磁盘空间监控告警
靠人盯不如靠监控。一套到位的告警,能让你在磁盘被塞满前就接到通知。
Prometheus 示例规则:
groups:
- name: disk
rules:
- alert: DiskSpaceUsageHigh
expr: (node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) / node_filesystem_size_bytes{mountpoint="/"} * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Disk space usage is above 85%"
- alert: DiskSpaceUsageCritical
expr: (node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) / node_filesystem_size_bytes{mountpoint="/"} * 100 > 95
for: 1m
labels:
severity: critical
annotations:
summary: "Disk space usage is above 95%, immediate action required"
Zabbix 监控模板也同样推荐开启 LLD(Low Level Discovery)自动发现挂载点并监控。
10.2 日志空间限制
用配置给日志目录加个“天花板”:
# 使用 logrotate 自动管理
# 确保配置文件存在且正确
# 对于 Nginx,可以限制 access_log 缓冲
access_log /var/log/nginx/access.log combined buffer=32k flush=5s;
10.3 独立分区策略
关键目录安排独立分区,防止一个目录吃光整个根分区:
/var/log 独立
/tmp 独立(用 tmpfs 性能更好)
/home 独立
/data 或 /opt 独立
# /etc/fstab 示例
tmpfs /tmp tmpfs defaults,noexec,nosuid,size=2G 0 0
tmpfs /var/tmp tmpfs defaults,noexec,nosuid,size=1G 0 0
10.4 数据库数据目录独立
数据库数据目录也应该落到独立分区或 LV 上:
# 创建独立 LV 给 MySQL
lvcreate -L 100G -n mysql vg
mkfs.xfs /dev/mapper/vg-mysql
mount /dev/mapper/vg-mysql /var/lib/mysql
10.5 定期检查脚本
用 cron 定时扫一眼,发现苗头立刻发邮件报警:
# /etc/cron.daily/disk-check
#!/bin/bash
THRESHOLD=85
df -h | grep -vE '^Filesystem|tmpfs|cdrom' | awk -v threshold=$THRESHOLD '{if ($5+0 >= threshold) print $1 " " $5 " is above threshold"}' | mail -s "[Alert] Disk space warning" admin@example.com
11. 常见场景处理
场景一:/var/log 目录占满
# 查看 /var/log 各文件大小
du -sh /var/log/*
ls -lhS /var/log/*.log
# 如果是 messages 文件过大
: > /var/log/messages
# 然后重启 rsyslog
systemctl restart rsyslog
# 如果是 secure 文件过大(SSH 暴力破解)
: > /var/log/secure
# 清理已切割的旧日志
find /var/log -name "*.gz" -mtime +7 -delete
find /var/log -name "*.log.*" -mtime +7 -delete
场景二:/ 分区满但 /home 有空间
# 查看根分区占用分布
du -h --max-depth=1 / | sort -hr
# 常见可疑目录
du -sh /var/*
du -sh /tmp
du -sh /.snapshots # Snapper 快照
# 如果 /var/lib/docker 占用过大,清理 Docker
docker system prune -af
场景三:LVM VG 没有剩余空间
# 查看 VG 状态
vgdisplay vg_name | grep -E "Free PE|Total PE"
# 如果有未使用的 PV,可以移除释放空间
vgreduce vg_name /dev/sdc1
# 或者扩展 VG(添加新磁盘)
pvcreate /dev/sdd
vgextend vg_name /dev/sdd
# 或者缩小某个不常用的 LV
# 先备份/卸载
umount /mnt/backup
e2fsck -f /dev/mapper/vg-backup
resize2fs /dev/mapper/vg-backup 50G
lvreduce -L 50G /dev/mapper/vg-backup
场景四:Nginx 写入失败但日志文件被删除
# 检查是否有进程持有已删除文件的 FD
lsof +L1
# 如果是 Nginx
nginx -s reopen
# 查看 Nginx 错误日志
tail -f /var/log/nginx/error.log
# 如果 Nginx 仍无法写入,可能是权限问题
ls -la /var/log/nginx/
chown nginx:nginx /var/log/nginx/
12. 总结
磁盘空间排查的根儿就在于分层定位:df 找哪个挂载点,du 追哪个目录,find 揪出具体文件。最常见的坑——文件删了但进程还占着 FD,用它 lsof +L1 一抓一个准。
清理时务必分清轻重:已经滚存的旧日志、临时文件可以直接删;正在写入的日志用 truncate 清空而不是 rm。LVM 环境下,在线扩展更是常规操作,生产环境强烈建议使用 LVM 管理磁盘。
说到底,预防比事后救火关键得多。扎实的监控告警机制,加上定期的自动清理任务,才能让你在磁盘满之前就从容出手。
更多运维实战话题,欢迎访问 云栈社区 一起探讨。