VPS磁盘满了找不到大文件:排查顺序
来源: 飞翔小编 2026/11/5 8:49:46 浏览量:0
一、先看结论:磁盘满了先跑这三行,不要先删文件

多数人遇到磁盘满的第一反应是 rm 掉一个看起来最大的文件,这往往是错的。因为 df 显示的是文件系统层面的占用,du 显示的是你权限范围内能遍历到的文件占用,这两个数字不一致时,真正的元凶往往不在你以为是的地方。正确的第一步是拿到三个数字:文件系统已用百分比、inode 使用百分比、以及根目录下最大的一层目录占用。
df -hT / # 根分区:容量、已用、可用、挂载类型(ext4 / xfs)
df -i / # inode 使用率,这是另一个独立的耗尽维度
du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -15
第三行里的 -x 关键:它让 du 不跨越文件系统边界,不去统计 /proc、/sys、/dev 这些伪文件系统。少了这个参数,du / 会被内核虚拟文件系统撑到几百兆甚至几个 G 的假占用,你会去查一个根本不占真实磁盘的东西。2>/dev/null 是为了屏蔽 /proc 里那些读取时会报「Permission denied」或「No such file or directory」的瞬时文件。
三条命令跑完,你会得到一张判断表。先看 df -i 的 inode 百分比:如果 inode 已经接近 100% 而空间百分比还有余量,那是 inode 耗尽,不是空间耗尽,解法完全不同,先往下翻到第五节。如果 df -h 的空间百分比真的到了 90% 以上,再看 du 的结果和 df 的已用是不是对得上:对不上,说明有第六节讲的那几类「看不见的空间」,对得上,就直接进第四节找大文件。
排序命令 sort -h 是必须的。默认 sort 按字典序排,du 输出里的 900M 会排在 1.1G 前面,看起来像最大的其实不是。-h 让它按人类可读的单位比较。管道末尾的 tail -15 只保留最大的 15 行,因为全量输出动辄几百行,真正有用的永远在前几行。
还要加一个参数 --max-depth=1,意思是只统计根目录下一层子目录各自的总量,不深入每一层。这是「逐层下钻」的标准做法:第一层找到 /var 最大,第二层就 du -xh --max-depth=1 /var | sort -h | tail -10,找 /var/log。这样每层只看十几行,比一次性 du -xh / 快得多,也不容易被海量小文件拖死。
很多教程会先让你跑 lsof | grep deleted,但那是第五步的工具,不是第一步。原因是 lsof 要遍历所有打开的文件描述符,在文件数量极多的机器上会跑好几分钟,而且需要 root 权限。在你还没确认「空间到底被谁吃了」之前花这个时间不划算。
最后一句提醒:在你确认空间去向之前,不要用 rm 删任何东西。误删系统文件(尤其是 /usr/lib、/lib 下的共享库)会让一台还能 SSH 的机器变成需要救援模式的一台机器,代价远高于多花十分钟排查。
二、df 和 du 不一致:六种成因与对应处理

这是排查磁盘满时最反常识的一幕。df -h 说已用 40G,du -xh --max-depth=1 / 加起来只有 30G,中间 10G 去哪了?答案通常是下面六种之一,而这六种的排查顺序也应该固定。
第一种是被删除但仍被进程持有的文件。这是最高频的原因,机制是 Linux 的 unlink 语义:删除一个正在被写入的文件,磁盘块不会立即释放,因为文件描述符还开着,内核要等持有它的进程关闭文件或重启之后才回收空间。所以 du 看不到它(目录项已经没了),df 能看到它(块还在被占用)。
第二种是文件系统层面的预留空间。ext4 默认给 root 保留 5%(大磁盘上是绝对值),这部分 df 算在已用里,但普通用户 du 不到、也删不掉。用 tune2fs -l /dev/vda1 | grep -i reserved 可以看到真实值。
第三种是 journal 日志。ext4 的日志默认 64MB 到 256MB 不等,xfs 的元数据日志按文件系统大小按比例走。xfs 上一个 1TB 的盘,元数据开销可能到 tens of GB 级别,这是 xfs 与 ext4 在同样容量下 df 差异明显的主因之一。
第四种是已删除但仍被映射到内存的文件。最典型的就是删除一个大日志文件后,进程把文件 mmap 到内存里,内存没释放,空间就一直卡着。这需要 lsof +L1 才能看到。
第五种是硬链接与符号链接的统计差异。du 对硬链接只算一次,对符号链接不跟随。如果你的 /var/log 下面有指向别处的硬链接,du 少算。
第六种是 overlay 与 bind mount。在容器里,overlayfs 的下层镜像层与上层可写层的占用统计口径不同,du 在容器内看到的和宿主机 df 看到的会有差。
处理顺序是这样的:先用第四节的命令定位目录级占用,再用第二节的 lsof +L1 和 lsof | grep deleted 确认是否有幽灵文件,然后核对 reserved blocks 和日志类型,最后才怀疑文件系统 bug。绝大多数情况在第一、二步就结束了。
判定「du 与 df 差值在正常范围」的经验阈值:差值小于总容量的 2% 属于正常,2% 到 5% 提示有预留或日志开销,超过 5% 基本可以确定存在已删除未释放的文件或挂载点被 du 跳过。
有一个容易漏掉的点:du -x 里的 -x 只在同一个文件系统内统计。如果你的机器上有多个挂载点(数据盘挂到 /data、备份盘挂到 /backup),du / 在没有 -x 时会把它们也算进去,导致 du 大于 df,而这同样是正常现象,不是 bug。
三、文件系统类型的影响:ext4 与 xfs 的元数据开销

第二节讲的差值里,有一部分来自文件系统类型本身,这一节单独说明,因为它会直接影响「同样标称 40G,为什么可用空间不一样」这种常见疑问。
先确认自己用的是什么文件系统:
df -hT /
# 输出示例:/dev/vda1 ext4 40G 32G 6.0G 86% /
ext4 是最常见的默认选择,它的设计目标是在中等容量下兼顾稳定和性能。它的元数据开销相对可预测:日志(journal)默认 64MB(小于 512MB 的文件系统)或 256MB(大于 512MB 的文件系统),加上 inode 表、位图等结构,通常在 1G 到 3G 的量级,具体取决于块大小和 inode 比例。
xfs 是另一套思路,设计目标是支撑大容量和高并发。它没有 ext4 那种固定大小的日志,而是按文件系统总大小按比例分配元数据空间——大致是文件系统的 12% 左右量级(不同版本与参数有差异)。这意味着一个 1TB 的 xfs 文件系统,元数据开销可能在几十 G 级别,这就是为什么同样标称 1TB 的盘,xfs 上 df 显示的可用空间会明显小于 ext4。
这个差异有实际后果。对数据库和频繁写入的场景,xfs 的元数据分配是非延迟的,通常表现更好;对「我要的就是盘上标称的空间」的场景,ext4 的实际可用更接近标称值。
btrfs 是第三个选项,它用写时复制和校验和做数据完整性,但会带来快照、subvolume 的额外管理复杂度,并且占用的空间常常超出用户的直觉。对不熟悉 btrfs 的用户,扩展空间时是最容易出事的选项。
选择上的实用建议:如果你要跑数据库或日志写入密集的服务,且在意写性能,xfs 是合理选择,但要在容量规划时按标称的 85% 再打一次折;如果你只是跑静态站、反向代理、几个小服务,ext4 的空间利用率更实在。
无论哪种文件系统,装机后第一件事都应该是确认实际可用:
df -hT /
df -i /
这两个数字一起看,才能知道这台机器的「真实起点」。
四、被删除但仍占空间的文件:lsof 与重启的正确用法

上一节讲的第一种成因值得单独展开,因为它是「磁盘明明还有空间,df 却说满了」这类故障的主要来源。
先看清楚现象。假设 /var/log/app.log 有 40G,你执行 rm 删掉了它,ls 里确实没有了,du 也不见了。但 df -h 显示可用空间一点没变。这说明 40G 的块还在被某个进程占着。
原理是 Linux 里文件与数据的解耦:目录项(名字到 inode 的映射)、inode(文件元数据)、数据块(实际内容)是三个独立的对象。rm 只删除目录项,inode 和数据块的释放要等最后一个引用它的文件描述符关闭。写日志的进程持有那个 fd,write 还在往里写(写到已删除的 inode,磁盘空间继续增长),所以空间不仅没释放,还可能继续被吃掉。
用这条命令找出来:
# 列出所有已删除但仍被打开的文件,FD 列是 "n" 表示无名字(即已删除)
sudo lsof +L1
# 输出重点看两列:FD 列的数字是文件描述符,SIZE 列是仍占用的字节数
典型输出长这样:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
nginx 1841 root 3w REG 253,0 41271843840 0 2314 /var/log/app.log (deleted)
这一行说明 nginx 的工作进程 1841 还开着那个已删除的日志文件,占着 41GB。
处理有三条路,按推荐顺序:
第一条,重启持有它的进程,让它关闭文件描述符。文件描述符随进程退出而关闭,内核随即释放数据块:
sudo systemctl restart nginx
df -h /
注意这条路的副作用:如果这个进程本身不重要,重启没问题;如果它是数据库或者正在处理业务的进程,先想清楚。更温和的做法是让应用自己重新打开日志文件,这取决于应用是否支持日志轮转时执行 reopen 信号。
第二条,直接 truncate 而不是 rm。对仍在被写的文件用 : > /var/log/app.log 或 truncate -s 0,文件大小立刻归零,空间立刻回来,而且进程继续写不会报错。这是比 rm 更适合日志文件的清理方式,也是很多运维没意识到的差别。
第三条,重启整机。最粗暴但最彻底,只有在前两条都不奏效时用。
最关键的是预防。日志文件应该配轮转而不是手动删:
# /etc/logrotate.d/app
/var/log/app.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
这里 copytruncate 是关键:它先把内容复制到轮转文件再清空原文件,进程持有的 fd 始终指向同一个 inode,不需要重启应用。如果用默认的 create 模式(重命名后新建),应用还握着旧 fd,空间不会释放,就会重现前面那个幽灵文件问题。所以日志轮转场景下 copytruncate 不是可选项。
顺带说一个数据:rotate 14 表示保留 14 份轮转文件。如果单个日志文件峰值 2G、每天 2 份,daily + rotate 14 的最坏占用是 28G。这个数要跟你的磁盘容量对一下,不能凭感觉写。
五、逐层定位大文件:find、sort 与大目录识别

确认空间确实被某个目录吃掉之后,就该找具体文件了。逐层下钻的思路是:先定位到一级目录,再逐层深入。
一条命令列出根目录下最大的 15 个子目录:
du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -15
-x 不跨文件系统,--max-depth=1 只看一层,2>/dev/null 屏蔽权限错误,sort -h 按大小排序,tail -15 取最大的 15 行。
如果结果显示 /var 最大,进去再看一层:
du -xh --max-depth=1 /var 2>/dev/null | sort -h | tail -10
一路下钻到具体目录后,找出里面最大的单个文件:
# 找出 /var 下所有大于 100M 的文件,按大小排序
find /var -type f -size +100M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head -20
-size +100M 表示大于 100MB 的文件,-exec ls -lh 拿到可读的大小和路径,awk 抽取第 5 列(大小)和第 9 列(路径),sort -rh 按大小倒序。
找最近生成的大文件,这个在排查「一夜之间磁盘满」时最有用:
# 最近 24 小时内修改过的大文件
find /var /home -type f -mtime -1 -size +50M -exec ls -lh {} \; 2>/dev/null | head -20
-mtime -1 表示修改时间在 1 天以内。这个命令能直接抓出是哪个进程在疯狂写日志。
还有一种情况是「文件数多但每个都不大」——inode 被小文件吃掉了。这时候不能用 -size 筛,要数数量:
# 找出文件数最多的目录
for d in /var/* /home/* /root/*; do echo "$(find "$d" -type f 2>/dev/null | wc -l) $d"; done | sort -rn | head -10
这一行输出的是「文件数 目录」的组合,从大到小排。正常情况下,一个目录下的文件数应该在几百到几千的量级;如果是几十万个,说明这里有问题——要么是缓存碎片,要么是邮件队列堆积,要么是某个程序在疯狂创建临时文件。
还可以用 find 直接找出 inode 占用异常的目录:
find / -xdev -type f -printf '%i\n' 2>/dev/null | sort | uniq -c | sort -rn | head
这行命令统计 inode 编号的重复次数。正常情况下每个 inode 只对应一个硬链接,计数大于 1 说明有硬链接在多处引用。
最后提一个更快的现代替代:ncdu(NCurses Disk Usage)。交互式界面,方向键逐层下钻,比 du 命令直观得多,也快得多。
sudo apt install ncdu # Debian/Ubuntu
sudo ncdu / # 扫描根目录
在大机器上,ncdu / 扫描时间可以接受,而 du -sh / 可能要跑十几分钟。日常排查用 du 够了,但如果你经常处理「磁盘突然满」的运维问题,装一个 ncdu 是很划算的投入。
还有两个实用技巧。其一是给 du 加 --exclude,跳过已知无关的目录,能把扫描时间砍掉一大截:
# 跳过虚拟文件系统和挂载点,只看本机的真实目录
du -xh --max-depth=1 --exclude=/proc --exclude=/sys --exclude=/dev / 2>/dev/null | sort -h | tail -15
其二是用 find 配合 -printf 直接输出人类可读的大小,省掉中间那层 ls 解析:
find /var -xdev -type f -size +100M -printf '%s %p\n' 2>/dev/null | sort -rn | head -20 | awk '{printf "%.2f GB %s\n", $1/1024/1024/1024, $2}'
-printf '%s %p\n' 输出字节数和路径,sort -rn 按字节数倒序,最后用 awk 把字节换算成 GB 并对齐输出。相比 find -exec ls -lh 再用 awk 抽列,这种写法少一层文本解析,在几十万文件的目录上差别明显。
六、inode 耗尽:No space left on device 的另一种真相

No space left on device 这个报错有两种完全不同的来源,很多人只盯着第一种。
第一种是数据块耗尽。df -h 的使用率接近 100%,df -i 的使用率还很低。这是空间问题,第四节讲的方法适用。
第二种是 inode 耗尽。df -h 的使用率可能只有 30%,但 df -i 显示 Use% 已经是 100%。这时候你 df -h 看着满有空间,怎么删都删不掉,因为一个字节都写不进去了——连创建新文件都需要先申请一个 inode。
判别方法就一条命令:
df -i
输出里的 IUse% 接近 100 而 Size/Used/Avail 看着正常,就是 inode 问题。典型症状是:可以修改已有文件的内容(不占新 inode),但不能创建新文件;日志报 No space left on device;数据库突然无法建临时表。
inode 耗尽的常见来源有三个。第一个是邮件队列堆积,海量小文件躺在 /var/spool/postfix 或 /var/spool/exim 里。第二个是 session 文件堆积,比如用文件存储 session 的 PHP 应用,某个爬虫把会话目录撑爆。第三个是缓存碎片,某个应用写了几十万个小缓存文件。
处理方式和空间问题不同:
# 清理前先看清楚是谁
sudo du -sh /var/spool/postfix /var/spool/exim 2>/dev/null
sudo du -sh /var/lib/php/sessions 2>/dev/null
确认是队列堆积后,正确做法是用工具清而不是手动 rm 整个目录。Postfix 的队列要用 postsuper -d ALL 清理,直接删文件会留下数据库索引不一致的问题。PHP session 目录可以直接删文件,因为 session 本来就是一次性的。
还有一个配置上的预防手段:给某些目录单独挂载一个文件系统,并预留 inode 空间。不过更实际的做法是定期检查:
# 找出 inode 使用率超过 80% 的挂载点
df -i | awk 'NR>1 && $5+0 > 80 {print $1, $5}'
关于 inode 数量:ext4 文件系统默认的 inode 数量在格式化时按块数比例确定,一般是块数的四分之一。1TB 的盘通常有上亿个 inode,理论上非常够用。inode 真正耗尽的情况,通常意味着磁盘上已经堆积了异常数量的小文件,而不是容量本身的限制。
七、日志轮转与 journald:容易被忽略的隐形占用

前面几节处理的是「找」和「清」,这一节讲「防」——把日志空间控制在一个可预期的范围内。
journald 是 systemd 系统上的日志子系统,它默认把日志存在 /var/log/journal 下。按机器默认配置,journald 最多占用磁盘的 10%(且不超过 4GB)。在 40GB 磁盘的机器上,journald 最多能吃掉 4GB;在 500GB 磁盘上,也是 4GB(因为有绝对上限)。
查看实际占用:
journalctl --disk-usage
限制 journald 的空间,改 /etc/systemd/journald.conf:
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=100M
MaxRetentionSec=7day
SystemMaxUse 是总量上限,MaxRetentionSec 是最长保留时间,两者都会独立生效,先到先算。改完要重启 journald:
sudo systemctl restart systemd-journald
如果机器上根本没有 journald(用的是 syslog-ng 或 rsyslog),那就要去查 syslog 的输出目录和轮转配置。
传统日志的轮转在 /etc/logrotate.conf 里,全局配置通常包含这些项:
weekly
rotate 4
compress
delaycompress
missingok
notifempty
create 0640 root adm
rotate 4 意味着保留 4 份历史。算一下最坏占用:如果 /var/log 下有 10 个活跃日志,每个峰值 500M,weekly 加 rotate 4 加 compress 之后,压缩后大约是原始体积的十分之一,最坏占用在 5G 量级。
定期检查轮转是否真的在跑:
sudo logrotate -d /etc/logrotate.conf # 调试模式,只看不改
sudo logrotate -f /etc/logrotate.conf # 强制执行一次
-d 输出里会列出每个配置文件预计怎么处理,包括「正在跳过,因为配置缺失」这类提示,是排查「为什么日志没轮转」的第一工具。
还要注意一个细节:logrotate 的定时器同样是 cron 或 systemd timer 驱动的,而且如果日志路径写错,它会静默跳过。logrotate -d 能看到它到底处理了哪些文件。
对「日志把磁盘吃满」这个具体场景,最有效的三步是:第一,journalctl --vacuum-size=200M 立刻回收;第二,给 journald 设 SystemMaxUse 上限;第三,确认应用日志配了 copytruncate 轮转,避免幽灵文件重现。
八、容器与数据库场景的特殊占用

这一节讲两类容器与数据库场景,它们的磁盘占用结构和普通文件系统不一样,用普通方法排查会看错。
Docker 容器的 overlayfs 分两层:下层是只读镜像层,上层是可写层。容器里 df -h 看到的是宿主机整个文件系统的空间,而不是这个容器能用的空间。所以容器内 df 显示还有 50G,宿主机的分区却已经满了,这是 Docker 的正常表现,不是 bug。
排查容器写入应该从宿主机做:
# 看每个容器的可写层大小
docker ps -s --format "table {{.Names}}\t{{.Size}}"
# 进容器内找大文件
docker exec 容器名 du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -10
如果写入了容器内的 /var/lib/docker/volumes,那部分不会随容器删除而消失,必须单独清理 volume:
docker system df # 看镜像、容器、volume 各占多少
docker system prune # 清理未使用的镜像和容器
docker volume prune # 清理未挂载的 volume
docker system prune 是安全的常用命令,但 docker system prune -a --volumes 会删掉所有未使用的镜像和 volume,跑之前务必确认没有重要的数据在 volume 里。
数据库场景有三个特殊的占用点。第一个是 binlog。MySQL 的 binlog 默认保留 30 天,日志量大的时候能吃掉几十 G。查看和清理:
# 看 binlog 占了多少
du -sh /var/lib/mysql/*bin* 2>/dev/null
# 清理 7 天前的 binlog
mysql -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);"
第二个是 InnoDB 的 redo log 和 undo log。它们一般不会失控(上限由 innodb_log_file_size 控制),但如果临时表空间 ibtmp1 涨上去了,说明有查询在做大排序:
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -i "temporary table"
第三个是 PostgreSQL 的 WAL 归档。pg_wal 目录在主备不同步时会持续堆积:
du -sh /var/lib/postgresql/*/main/pg_wal
这三个位置都不会被 du --max-depth=1 /var 的结果直接点名(它们在子目录里),需要你主动去看。这就是「逐层下钻」的价值——第一层只告诉你 /var 大,第二层告诉你 /var/lib 大,第三层才告诉你 /var/lib/mysql 大。
九、清理方案的成本账:清还是扩

空间用完了有两条路:清理或者扩容。这两条路的成本可以算出来,不需要凭感觉。
先算清理能回收多少。把第三节到第七节找到的所有可回收项加起来。典型场景的账是这样的:一个 20G 磁盘的机器,journald 占 4G、日志轮转历史占 3G、binlog 占 2G、幽灵文件占 5G,合计 14G。清理后可用空间从 2G 回到 16G,可用率从 90% 降到 20%。这个回收比例(14G / 20G = 70%)说明问题出在「管理缺失」而不是「容量不足」,这种情况下优先改配置而不是加钱。
但如果清理后可用空间只剩 40%(比如 40G 盘清完还有 16G),而你的业务每月还在新增 3G 日志,那么下个月这个时候你会再次满。判断公式很简单:可用空间 ÷ 月新增量 = 能撑几个月。16G ÷ 3G = 5.3 个月,超过三个月就该考虑扩容或者加日志轮转策略。
现在算扩容的钱。用渠道库里能查到的真实价格:
sixtynet LAX-Storage-500G 400.00 美元 / 年 → 折合每月 33.33 美元
4 核 CPU、4 GB 内存、500 GB 磁盘、10000 Mbps 带宽、10000 GB 月流量
sixtynet LAX-Storage-1TB 700.00 美元 / 年 → 折合每月 58.33 美元
8 核 CPU、8 GB 内存、1000 GB 磁盘、10000 Mbps 带宽、15000 GB 月流量
单位成本可以这么算:
| 机型 | 折合月价 | 每 GB 内存 | 每 GB 磁盘 | 每 TB 流量 |
|---|---|---|---|---|
| LAX-Storage-500G | 33.33 美元 | 8.33 美元 | 0.067 美元 | 3.413 美元 |
| LAX-Storage-1TB | 58.33 美元 | 7.29 美元 | 0.058 美元 | 3.982 美元 |
从 500G 升到 1TB,月价从 33.33 美元涨到 58.33 美元,多花 25 美元/月,多得 500 GB 磁盘,折合每 GB 增量成本 0.05 美元。比直接买一台全新机器的每 GB 磁盘单价(0.067 美元)还低,因为大盘的规模效应在这里体现出来了:每 GB 内存成本从 8.33 美元降到 7.29 美元,每 GB 磁盘从 0.067 降到 0.058。
结论很清楚:如果你的痛点是「磁盘不够」,在同一个商家内部往上一档扩容通常比换机器划算。如果你的痛点是「内存不够」,那要看你的负载是不是磁盘型——数据库和文件缓存是磁盘型,内存型负载(大量并发连接、内存缓存)在这个商家体系里要跳到 LAX-LARGE(16 GB 内存、320 GB 磁盘,1600 美元/年,折合每月 133.33 美元),月价是 500G 盘的四倍,磁盘容量反而降了一半。所以「买大内存机型」不是「顺便解决磁盘问题」的方案。
对比另一条路线,中文区大内存机型(云途 HK.DS.S)是 800 元/月,128 GB 内存、6000 GB 磁盘、40000 GB 流量,折合每 GB 磁盘 0.133 元、每 GB 内存 6.25 元、每 TB 流量 20.48 元。这是完全不同的产品形态:内存和磁盘都是大规格,但流量单价高得多,且按月付。它适合的场合是「内存和磁盘都是瓶颈、流量不大」的场景,比如自建大数据平台、跑模型推理中间层、存储型服务。而 LAX-Storage 这一系列适合「磁盘大、流量大、内存需求适中」的存储与分发场景。
所以清理还是扩容,取决于缺口在哪一维。缺空间就升磁盘档,缺内存就升内存档,缺流量就升流量档,三者不是一回事。
十、什么情况下不该靠清理解决:四类不适用场景

这一节讲的是:什么情况下「清理磁盘」这个动作本身就不该做,或者换了机器也解决不了问题。判断依据只能是公开事实——机房位置、线路类型、计费方式、IP 资源、存储介质类型,不涉及对任何商家好坏的评价。
第一种场景:磁盘增长来自业务写入而非垃圾文件。如果你的业务每分钟都在产生必须保留的订单数据、日志索引、用户上传,清理只是把问题推迟几天。这种情况下正确的动作是「让数据去该去的地方」,而不是「腾空间」。具体做法:把冷数据归档到对象存储或异地,数据库做分区和定期归档,或者直接换更大的盘。
替代方案:如果你需要的是「归档空间而不是计算资源」,看大磁盘低单价档位。LAX-Storage-1TB 每 GB 磁盘 0.058 美元,是本轮所有在售机型里单位磁盘成本最低的一档,1000 GB 磁盘、15000 GB 月流量,折合每月 58.33 美元。
第二种场景:你的 VPS 是共享存储型应用,磁盘是网络块存储而非本地盘。这时 du 遍历会非常慢,而且 IO 密集的扫描会影响业务。此时应该用块存储厂商提供的容量视图或监控 API 来看,而不是在生产机上一层层 du。替代方案:把排查动作放到业务低峰期,或者在同机房开一台排查专用实例跑扫描。
第三种场景:IP 资源限制导致不能简单扩容或迁移。有些应用对绑定 IP 有依赖(白名单、证书、第三方回调地址),换机器意味着换 IP,需要改一堆配置。这种情况下磁盘问题应该通过「就地清理 + 调整保留策略」解决,而不是「买新的」。
替代方案:优先选支持保留原 IP 的扩容路径(同商家同机房升配),而不是跨商家迁移。
第四种场景:你在为「磁盘满」付费,但你真正的问题不是磁盘。举一个具体的判断方法:算一下磁盘成本占你总成本的比例。如果一台机器的总成本里磁盘只占 5%,而你为了省这块空间花了两小时排查和优化,那这两小时的成本远高于直接升级。
替代方案:按比例优先优化占比最大的那一维。折合每月 33.33 美元的 LAX-Storage-500G,如果你的瓶颈是 CPU(4 核跑满了),那么升配到 8 核的 LAX-Storage-1TB 更对症;如果瓶颈是内存(4 GB 不够用),也该升到 8 GB 的那一档。磁盘是最不值钱的维度之一。
第五种场景值得一提:流量型瓶颈。Sixtynet 的 Los Angeles China Unicom-Premium 是 2500 美元/年,折合每月 208.33 美元,带宽标注 100 Mbps。这个机型的定位是线路质量而非容量,如果你的应用需要的是大带宽而不是大磁盘,拿它来解决磁盘问题是完全错配。
十一、预防:把磁盘占用控制在可预期范围

排查是救火,预防是治本。这一节给出一套可以直接照抄的配置。
第一件事是加磁盘空间告警,在使用率达到 70% 时就通知,而不是等到 95%:
#!/bin/bash
# /opt/disk-check.sh 由 cron 每小时调用
USE=$(df -h / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
INODE=$(df -i / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
if [ "$USE" -ge 70 ]; then
/usr/local/bin/alert.sh "根分区使用率 ${USE}%,请检查"
fi
if [ "$INODE" -ge 80 ]; then
/usr/local/bin/alert.sh "根分区 inode 使用率 ${INODE}%,请清理小文件"
fi
两个阈值要分开设。空间 70%、inode 80%,因为 inode 耗尽通常来得更突然,小文件堆积的速度可能超乎预期。
第二件事是给应用日志配好轮转,并确认用的是 copytruncate。检查现有配置:
grep -r "copytruncate\|rotate" /etc/logrotate.d/ /etc/logrotate.conf 2>/dev/null
配置轮转时有两个参数要一起算。其一是 rotate 的份数乘以单个日志的峰值大小,等于轮转目录的最坏占用。其二是压缩后的体积通常是原始的十分之一左右,compress 打开后可以把这个数字打下来,但注意 delaycompress 会让最近一份保持未压缩状态。
举例:单个日志峰值 1G,daily + rotate 7 + compress + delaycompress,最坏占用约 7G。如果你的根分区只有 20G,这就是 35% 的空间被日志历史占着。遇到这种情况,要么减少 rotate 份数,要么给 /var/log 单独挂一个分区。
第三件事是限制 journald。/etc/systemd/journald.conf 里设 SystemMaxUse=500M,然后 systemctl restart systemd-journald。
第四件事是给数据库日志设保留策略。MySQL 设置 expire_logs_days 或用 event scheduler 定期 PURGE BINARY LOGS;PostgreSQL 用 archive_mode 配合定时归档到对象存储。
第五件事是定期做一次容量盘点,把三个数记下来:根分区使用率、inode 使用率、单目录最大占用。这个动作本身不复杂,但它能在问题发生前一周就发现趋势——比如「日志目录每周涨 1.2G,三个月后会满」这样的判断,可以让你从容处理,而不是在半夜被叫醒。
判断这套预防机制是否到位,看三个数字:连续观察四周,如果使用率曲线是平的而不是持续爬升,说明轮转和保留策略生效了;如果 inode 使用率稳定在 30% 以下,说明没有异常小文件堆积;如果告警从未在 70% 处触发过,说明容量规划有余量。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 主要访客在国内、在意打开速度:先确认回程线路,再谈配置。看 iwebfusion 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 racknerd 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十二、常见问题

df 和 du 显示的空间对不上,差了十几个G,是磁盘坏了吗? 不是硬盘坏了。按第二节的六种成因逐个排查:先跑 sudo lsof +L1 看有没有已删除仍被占用的文件,再看 tune2fs -l 的 reserved blocks,再确认是不是多挂载点被 du 统计了。绝大多数差值在前两步就能定位。
inode 是什么,为什么空间还有却报 No space left on device? inode 是文件系统里存放文件元数据的槽位,每个文件占一个。空间够但 inode 用完时,你无法创建任何新文件,因为创建文件必须先申请 inode。跑 df -i 确认,然后按第五节清理小文件。
为什么我删了大文件,df 显示的空间没变? 因为那个文件正被进程打开。Linux 的删除只是移除目录项,数据块要等最后一个文件描述符关闭才释放。用 sudo lsof +L1 找到持有者,重启进程,或者对日志文件用 truncate -s 0 而不是 rm。
日志轮转配了,为什么空间还是涨? 大概率是 logrotate 用了默认的 create 模式:文件被重命名后应用还握着旧的文件描述符,空间不会释放。改成 copytruncate 模式。另外确认 logrotate 真的在被 timer 触发,用 sudo logrotate -d /etc/logrotate.conf 看它处理了哪些文件。
journald 占的空间怎么算出来的? 按 SystemMaxUse 的默认值算,默认是磁盘的 10% 但不超过 4GB。实际占用用 journalctl --disk-usage 查,立即回收用 journalctl --vacuum-size=200M。
容器里 df 显示还有空间,为什么宿主机说满了? Docker 的 overlayfs 让容器看到的是宿主机整个文件系统的空间统计。写入了容器可写层或 volume 的内容实际占在宿主机上,要从宿主机用 docker ps -s 和 docker system df 来看。
MySQL 的 binlog 能删吗? 可以删,但要用 PURGE BINARY LOGS 命令,不能直接 rm 文件。用 rm 会导致 binlog 索引和实际文件不一致,主从复制可能出问题。清理前确认备份已完成且没有主库还在依赖这些 binlog 做位点恢复。
磁盘快满了但我不知道该删什么,有没有安全的清理命令? 没有一键安全的清理命令。journald 可以用 journalctl --vacuum-size=200M 回收;包管理缓存可以用 apt clean 或 yum clean all;临时文件可以在确认没有进程占用后清理 /tmp 里的老文件。除此之外都应该先定位再删。
可以直接在生产机上跑 du -x / 吗? 目录层级极多时会很慢,且产生大量 IO,影响业务响应。生产环境建议用 ncdu 交互式下钻,或者在低峰期跑,或者用带 -x 且限定路径的方式分段排查。
VPS 磁盘空间不够,应该买更大的盘还是换商家? 先看缺口在哪一维。缺空间就在同一个商家内部往上一档升配,500G 升 1TB 的增量成本是每 GB 0.05 美元,比全新机器的每 GB 0.067 美元还低;换商家会带来 IP 变更、线路重测、迁移窗口一整套额外成本,除非现有商家确实没有更大规格。
为什么我的 VPS 有 40G 硬盘但可用空间只有 34G? 差额是文件系统元数据、预留空间和 journald 日志。ext4 默认给 root 预留 5%,journald 按配置占用一部分。用 df -h 看真实可用,tune2fs -l 看预留,journalctl --disk-usage 看日志。这部分不是浪费,不用刻意消除。
