磁盘空间告警后的7个深坑:du和df对不上、inode耗尽、已删文件不释放怎么办
坑一:du和df对不上——已删除文件未释放空间
这是最经典的问题,也是新手最容易懵的场景。
df显示磁盘快满了,你用du -sh /*逐层排查,加起来远远不到df显示的数字。差的那部分空间去哪了?
根因:Linux删除文件时,如果有进程还在持有这个文件的文件描述符(fd),内核只会删除目录项(unlink),但不会释放实际的磁盘块。du按文件名统计,已删除的文件它看不到;df读的是文件系统的superblock,统计的是实际占用的block数。
排查命令:
# 找出所有被删除但仍被进程持有的文件
lsof +L1
# 只看大于100M的
lsof +L1 | awk '$7 > 104857600 {print $1, $2, $7, $9}'
输出大概长这样:
COMMAND PID SIZE NAME
java 12345 52428800000 /var/log/app/service.log (deleted)
nginx 6789 2147483648 /var/log/nginx/access.log (deleted)
解决方案:
方案一:最干净的办法——重启持有文件的进程:
# 确认进程PID后重启
systemctl restart your-service
方案二:不能重启?可以清空文件内容而不是删除文件。对已删除但未释放的文件,通过/proc文件系统截断它:
# 找到进程PID和fd编号
ls -l /proc/12345/fd/ | grep deleted
# 截断文件释放空间(不会影响进程运行,进程后续写入从0开始)
: > /proc/12345/fd/77
方案三:生产环境写日志别用rm删,用truncate或重定向清空:
# 正确姿势——清空文件内容但保留文件
truncate -s 0 /var/log/app/service.log
# 或者
cat /dev/null > /var/log/app/service.log
# 或者更简洁
: > /var/log/app/service.log
踩坑记录:之前一个同事写了个crontab定时清日志的脚本,用的rm -f然后touch新建。结果日志框架(log4j)还持有旧文件的fd,疯狂往已删除的文件里写,磁盘空间根本不释放。改成copytruncate方式的logrotate之后问题消失。
坑二:inode耗尽——磁盘还有空间但无法创建文件
df -h显示还有大把空间,但写文件报No space left on device。
第一次遇到这个问题的时候,我反复确认了三遍磁盘空间,以为系统出了bug。
排查命令:
# 检查inode使用率
df -i
# 输出示例
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 3276800 3276800 0 100% /
IUse% 100%,inode用完了。
根因:每个文件(包括目录、软链接)占用一个inode,inode总数在格式化文件系统时就固定了(ext4默认每16KB分配一个inode)。如果系统里有海量小文件——比如PHP的session文件、邮件队列、缓存临时文件——inode可能比磁盘空间先耗尽。
定位哪个目录吃掉了inode:
# 统计各目录的文件数量(注意这个命令在文件多的时候会比较慢)
find / -xdev -type f | cut -d "/" -f 2-3 | sort | uniq -c | sort -rn | head -20
如果你大概知道是哪个分区,可以缩小范围:
# 统计/var下每个子目录的文件数
for dir in /var/*/; do echo "$(find "$dir" -xdev -type f | wc -l) $dir"; done | sort -rn | head -10
常见元凶:
/var/spool/postfix/maildrop/—— 系统cron任务产生大量未投递邮件/tmp/下的session文件或缓存文件/var/lib/php/sessions/—— PHP session文件堆积- 某些应用按时间戳创建大量小文件的目录
解决方案:
# 批量删除(文件太多rm会报参数过长,用find)
find /var/spool/postfix/maildrop/ -type f -delete
# 如果find也卡住,用ls配合xargs分批删
ls -f /var/spool/postfix/maildrop/ | xargs -n 1000 rm -f
预防措施:
# 格式化时指定更多inode(适合存大量小文件的分区)
mkfs.ext4 -i 4096 /dev/sdb1 # 每4KB分配一个inode,是默认的4倍
# 监控inode使用率,加到你的监控系统里
df -i | awk 'NR>1 && $5+0 > 80 {print "inode告警: "$6" 使用率 "$5}'
坑三:日志撑爆磁盘——logrotate没生效
磁盘空间告警十有八九跟日志有关。但不少人配了logrotate,以为万事大吉了,直到磁盘还是满了。
常见翻车场景:
1、logrotate配置写了但从没执行过:
# 检查logrotate最近有没有跑过
cat /var/lib/logrotate/status # CentOS/RHEL
cat /var/lib/logrotate/logrotate.status # 有些发行版路径不同
# 手动执行看看有没有报错
logrotate -d /etc/logrotate.d/your-app # -d是debug模式,只显示会做什么,不实际执行
logrotate -vf /etc/logrotate.d/your-app # -v详细输出 -f强制执行
2、应用日志不走logrotate:比如Java应用用log4j/logback自己管日志轮转,或者Docker容器的日志。
# Docker日志也会撑爆磁盘,查看容器日志大小
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -10
# 配置Docker日志轮转(/etc/docker/daemon.json)
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
# 改完要 systemctl restart docker
3、logrotate用了create模式但应用还在写旧文件:
# 对不支持HUP信号重新打开日志的应用,用copytruncate
/var/log/app/*.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate # 先复制再清空原文件,不需要应用配合
}
一个排查日志大文件的快捷组合:
# 找出大于500M的文件
find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh | head -20
坑四:/boot分区满——内核版本堆积
这个坑相对冷门,但遇到了很烦。/boot分区通常就给500M-1G,几个内核版本就塞满了。系统更新或者装新内核直接失败。
排查:
df -h /boot
# 查看已安装的内核
rpm -q kernel # CentOS/RHEL
dpkg --list 'linux-image-*' # Ubuntu/Debian
清理旧内核:
# CentOS 7/RHEL 7 —— 保留最近2个内核
package-cleanup --oldkernels --count=2
# CentOS 8/RHEL 8+
dnf remove --oldinstallonly --setopt installonly_limit=2 kernel
# Ubuntu/Debian
apt autoremove --purge
注意:千万别手动rm删/boot下的文件然后不更新grub,会导致系统起不来。
# 如果手贱删了,补救:
# CentOS/RHEL
grub2-mkconfig -o /boot/grub2/grub.cfg
# Ubuntu
update-grub
坑五:ext4预留空间——5%的隐形消耗
df显示磁盘用了95%就开始告警,你看了下容量,觉得还有十几G空间啊,怎么就告警了?
根因:ext4文件系统默认会预留5%的空间给root用户,防止磁盘100%满了之后普通用户还能写入导致系统完全无法操作。在大容量磁盘上,这5%相当可观——1TB磁盘就是50G。
查看和调整预留空间:
# 查看预留块数
tune2fs -l /dev/sda1 | grep -i "reserved"
# Reserved block count: xxxxx
# Reserved blocks uid: 0 (user root)
# 对数据盘(非系统盘),可以把预留比例调低甚至清零
tune2fs -m 1 /dev/sdb1 # 预留1%
tune2fs -m 0 /dev/sdb1 # 不预留(仅数据盘,系统盘建议至少保留1%)
# 改完立即生效,不需要重启或umount
XFS没有这个问题,XFS默认不预留空间。所以如果你看到ext4的df显示和你预期差了几十G,先查这个。
坑六:LVM扩容踩坑——扩了逻辑卷忘了扩文件系统
云服务器加了磁盘,或者虚拟机扩了盘,fdisk -l看到容量已经变大了,但df -h还是老样子。
这个问题经常出现在LVM环境下。扩容其实分三步:扩物理卷/物理磁盘 → 扩逻辑卷 → 扩文件系统。很多人做完前两步就以为搞定了。
完整的LVM在线扩容流程:
# 1. 如果是新加磁盘,先创建PV
pvcreate /dev/sdb
# 把新PV加入VG
vgextend centos /dev/sdb
# 2. 如果是已有磁盘扩容(比如云盘),先让系统识别新容量
# 方法一:不重启识别新磁盘大小
echo 1 > /sys/block/sda/device/rescan
# 如果是分区(比如/dev/sda3扩容),还要用growpart扩分区
growpart /dev/sda 3
# 再扩PV
pvresize /dev/sda3
# 3. 扩逻辑卷
lvextend -l +100%FREE /dev/centos/root # 用掉所有剩余空间
# 或者指定大小
lvextend -L +50G /dev/centos/root
# 4. 扩文件系统(关键!很多人忘了这步)
# ext4用resize2fs
resize2fs /dev/centos/root
# xfs用xfs_growfs
xfs_growfs /dev/centos/root
踩坑点:
- ext4和xfs扩容命令不一样,搞混了会报错。
resize2fs只支持ext系列,xfs_growfs只支持XFS - XFS只能扩不能缩,别想着缩小XFS分区,它不支持
lvextend -r参数可以自动扩文件系统,省一步:
# -r 自动识别文件系统类型并resize,推荐用这个
lvextend -l +100%FREE -r /dev/centos/root
怎么确认当前文件系统类型:
# 方法一
df -Th
# 方法二
lsblk -f
# 方法三
blkid /dev/centos/root
坑七:tmpfs和overlay占用——df看到的不全是真实磁盘
有时候df -h列出一堆挂载点,有些其实根本不占磁盘空间(tmpfs在内存里),有些则是重叠挂载可能被重复统计。
Docker环境下尤其常见:
# 查看Docker总体磁盘占用
docker system df
# 详细信息
docker system df -v
# 清理所有未使用的镜像、容器、网络、构建缓存
docker system prune -a --volumes
# ⚠️ 谨慎使用,会删除所有停止的容器和未使用的镜像
K8s环境里的磁盘坑:
# kubelet的imagefs和nodefs可能在同一个分区
# 查看kubelet的磁盘压力阈值
kubectl describe node <node-name> | grep -A5 "Conditions" | grep DiskPressure
# kubelet默认阈值:
# imagefs.available < 15% 开始驱逐Pod
# nodefs.available < 10% 开始驱逐Pod
# 清理kubelet不再使用的镜像
crictl rmi --prune
一个容易忽略的点——有些bind mount会让同一份数据在du统计时被计算两次:
# 用-x参数限制du不跨文件系统
du -shx /*
总结:磁盘告警排查速查表
| 现象 | 排查命令 | 根因 | 解决方案 |
|---|---|---|---|
| du和df对不上 | lsof +L1 | 已删文件被进程持有 | 重启进程或截断fd |
| 有空间但创建文件失败 | df -i | inode耗尽 | 清理小文件,重格式化时调整inode |
| 日志疯长 | find / -size +500M | logrotate未生效 | 检查logrotate配置和状态 |
| /boot满 | df -h /boot | 旧内核堆积 | package-cleanup --oldkernels |
| 磁盘95%但实际没那么满 | tune2fs -l | ext4预留5% | tune2fs -m 1 调低预留 |
| 扩了盘但df不变 | lvs && pvs | 只扩了LV没扩文件系统 | lvextend -r 或手动resize |
| Docker/K8s磁盘暴涨 | docker system df | 镜像/容器/日志堆积 | docker system prune |
最后几句
磁盘告警这事,说大不大说小不小。但如果排查思路不对,很容易在一个方向上死磕半小时,最后发现根因在另一个地方。
建议把df -h和df -i都加到你的日常巡检脚本里,inode问题被漏掉的概率很高。还有logrotate的状态也定期检查一下——别等到磁盘炸了才发现它半年没跑过。
我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。 关注公众号:耕云躬行录 个人博客:躬行笔记