修复 df 显示有空闲空间却报设备上没有空间错误

即使 df -h 显示还有数 GB 的空闲空间,某条命令仍可能以“设备上没有空间”(No space left on device)失败:

touch: cannot touch '/var/lib/app/cache/item': No space left on device

这条信息并不总是意味着文件系统耗尽了数据块。Linux 每创建一个新文件都需要一个空闲 inode,而报错的路径可能位于另一个挂载点或容量受限的内存文件系统上。Btrfs 在 df 仍报告有空闲容量时也可能耗尽可用的数据或元数据空间;当某个程序用尽 inotify 监视限制时,内核也会返回相同的错误。

本指南将说明如何按照实用的顺序逐项排查每个原因、定位已耗尽的资源,并安全地释放空间。

快速参考

Check命令
受影响路径上的空间df -h /affected/path
受影响路径上的 inodedf -ih /affected/path
文件系统与源设备findmnt -T /affected/path
占用 inode 较多的目录sudo du --inodes -x -d 2 /path | sort -n | tail -20
仍被打开的已删除文件sudo lsof +L1
Btrfs 分配详情sudo btrfs filesystem usage /affected/path
inotify 监视限制sysctl fs.inotify.max_user_watches
Docker 存储用量docker system df
日志大小sudo journalctl --disk-usage
APT 缓存大小sudo du -sh /var/cache/apt/archives

检查报错的确切路径

首先将报错的路径传给 df,而不只是传给根目录:

df -h /var/lib/app/cache
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p5   40G   17G   22G  44% /var

Mounted on(挂载点)一列显示实际承载该路径的文件系统。此处报错的目录位于独立的 /var 文件系统上,而不是 df -h / 本应报告的根文件系统。一台服务器可能拥有独立的 /、/var、/home、/tmp、容器存储或应用数据文件系统,而它们各自独立地被填满。

使用以下命令显示源设备、文件系统类型和挂载点:

findmnt -T /var/lib/app/cache
TARGET SOURCE         FSTYPE OPTIONS
/var   /dev/nvme0n1p5 ext4   rw,relatime

如果 Use%(使用率)为 100%,则文件系统确实耗尽了数据块。可以使用 du 命令,或参考 Linux 中查找大文件的指南,来定位占用空间最大的目录和文件。

检查 inode 使用情况

当数据块使用率看起来正常时,检查同一路径的 inode 使用情况:

df -ih /var/lib/app/cache
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p5   2.6M  2.6M     0  100% /var

每个文件和目录都会消耗一个 inode。一个包含数百万个微小缓存、会话、队列或临时文件的文件系统,可能在字节层面仍有大量空闲容量时却已耗尽 inode。

如果 IFree(空闲 inode 数)为 0 或 IUse%(inode 使用率)为 100%,则 inode 耗尽就是原因所在。

找出占用 inode 最多的目录

GNU du 命令可以统计 inode 数量而非字节数。若要在不跨入其他已挂载文件系统的情况下检查 /var:

sudo du --inodes -x -d 2 /var 2>/dev/null |

  sort -n |

  tail -20
12540    /var/lib/systemd
48201    /var/lib/docker
73122    /var/cache
2310884  /var/lib/app/cache
2485119  /var/lib
2621402  /var

最大的数字出现在底部。在此示例中,仅 /var/lib/app/cache 一个目录就占用了文件系统中几乎全部的 inode。对占用最高的目录使用更大的深度再次运行该命令:

sudo du --inodes -x -d 3 /var/lib/app/cache 2>/dev/null |

  sort -n |

  tail -20

这样在删除任何文件之前先缩小排查范围。

要按父目录分组查看文件数量,可使用:

sudo find /var -xdev -type f -printf '%h\n' 2>/dev/null |

  sort |

  uniq -c |

  sort -n |

  tail -20

在一个包含数百万文件的文件系统上,该命令可能耗时较久。它不会删除任何文件。

安全删除多余的零散小文件

一旦定位到目录,先确认是哪个应用在使用它,以及这些文件是否可以丢弃。常见来源包括:

  • 不会自动清理旧条目的应用缓存。
  • PHP 会话目录。
  • 邮件队列。
  • 容器镜像层与构建缓存。
  • 监控数据与轮转日志。
  • 失败任务产生的临时文件。

删除旧临时文件前,先预览它们:

sudo find /tmp -xdev -type f -mtime +7 -print

如果列表中只包含可安全删除的文件,则加上 -delete 重新运行:

sudo find /tmp -xdev -type f -mtime +7 -delete

在了解应用目录的保留规则之前,不要对其执行范围过广的 find ... -delete 命令。如果存在应用自身的清理命令,应优先使用它。

在 Ubuntu 和 Debian 上,使用以下命令清理已下载的软件包归档:

sudo apt clean

检查系统日志(journal)当前占用的空间:

sudo journalctl --disk-usage

要删除超过 7 天的归档日志数据:

sudo journalctl --vacuum-time=7d

清理完成后,确认 inode 已重新可用:

df -ih /var/lib/app/cache

排查仍被打开的已删除文件

此项排查对应问题的另一种情形:df 报告文件系统已满,但 du 却无法解释这些用量。某个进程可能仍打开着一个已删除的文件。其目录项虽已消失,但内核在进程关闭文件描述符之前不会释放其数据块。

列出仍处于打开状态的已删除文件:

sudo lsof +L1
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF NLINK NODE NAME
java     4217 app     7w   REG  259,3 8589934592     0 8123 /var/log/app.log (deleted)

示例显示有一个 8 GB 的已删除日志仍被一个 Java 进程占用。请在合适的维护窗口期内重启所属服务:

sudo systemctl restart app.service

请使用你进程的实际服务名。确认该文件已从 lsof +L1 的输出中消失,并再次用 df -h 检查空间。

关于已删除但被打开文件的详细说明,请参阅 lsof 命令指南。

检查 tmpfs 与容器文件系统

像 /run、/dev/shm,有时还有 /tmp 这样的路径可能使用 tmpfs,它由内存支撑并拥有独立的容量上限。直接检查报错的路径:

df -h /run /dev/shm /tmp

如果某个 tmpfs 挂载已写满,请删除相关应用拥有的陈旧文件,或在确认可用内存后再提高该挂载的容量上限。

容器还会通过叠加文件系统和挂载卷写入数据。宿主文件系统可能仍有空闲空间,而某个容器专属的挂载点或存储池却已满。在宿主机上,使用 findmnt -T 定位路径与挂载点;在容器内部,运行 df -h 与 df -i 来查看其自身的视角。

对于 Docker,可使用以下命令查看镜像、容器、本地卷与构建缓存所占用的空间:

docker system df

该命令会报告 Docker 的存储用量,但不会删除任何内容。在决定清理哪些对象之前,请先查看其详细输出。

检查 Btrfs 的数据与元数据分配

Btrfs 在独立的块组中分别管理数据与元数据。即使 df 报告的空闲空间总量看起来充足,其写时复制(copy-on-write)的分配方式仍可能返回“设备上没有空间”错误。

如果 findmnt 报告受影响路径的文件系统为 btrfs,则使用以下命令查看其分配详情:

sudo btrfs filesystem usage /affected/path

请综合查看 Device unallocated(设备未分配)、Free (estimated)(估算空闲)、Data(数据)和 Metadata(元数据)这几个数值。一个文件系统可能拥有空闲的数据空间,却没有足够的未分配容量来创建另一个元数据块组。

首先,根据保留策略删除不需要的文件或快照。随后,你可以使用以下命令回收完全未被使用的数据与元数据块组:

sudo btrfs balance start -dusage=0 -musage=0 /mountpoint

请将 /mountpoint 替换为 findmnt 显示的 Btrfs 挂载点。usage=0 的过滤项只选择空的块组,且不需要额外的工作空间。不要将未加过滤的 balance 作为通用的“磁盘已满”修复手段,因为它会重写所有被选中的块组,可能耗时很久,并且需要临时的空闲空间。

检查 inotify 监视限制

编辑器、构建工具、日志采集器与文件同步守护进程都通过 inotify 监视文件。每一个被监视的文件或目录都会消耗一个监视项,而内核会对每个用户施加数量限制。当某个程序超出该限制时,inotify_add_watch() 会失败并返回与“设备上没有空间”相同的错误,即便文件系统拥有空闲的数据块与 inode。应用程序常常以各自的措辞报告这一问题:

Error: ENOSPC: System limit for number of file watchers reached, watch '/srv/app/src'

查看当前限制:

sysctl fs.inotify.max_user_watches
fs.inotify.max_user_watches = 65536

该数值因发行版与内核版本而异。要找出是哪个进程在消耗监视项,可按文件描述符统计其数量:

sudo sh -c "grep -c '^inotify' /proc/*/fdinfo/* 2>/dev/null" |

  awk -F: '$2 > 0 {print $2, $1}' |

  sort -rn |

  head
62184 /proc/2811/fdinfo/23
1204 /proc/1533/fdinfo/9
118 /proc/994/fdinfo/12

PID 显示在路径中,因此 ps -p 2811 -o comm= 可以列出持有这些监视项的程序。如果该程序正在监视一个它并不需要监视的目录(例如依赖目录或构建输出目录),应在程序自身的配置中将该目录排除,而不是提高限制。

当工作负载确实更需要更多的监视项时,可提高正在运行系统的限制:

sudo sysctl fs.inotify.max_user_watches=524288

让该修改在重启后仍持久生效:

echo 'fs.inotify.max_user_watches=524288' | sudo tee /etc/sysctl.d/90-inotify.conf

每个监视项都会消耗少量无法被换出(swap)的内核内存,因此应将限制设置为与工作负载相匹配,而不是设为系统所能接受的最大值。

检查文件系统与内核错误

如果空间与 inode 都可用,但写入仍然失败,请检查近期的内核消息:

sudo dmesg --level=err,warn | tail -50

同时检查系统日志:

sudo journalctl -k -p warning --since "1 hour ago"

请留意 Btrfs 分配警告、I/O 错误、文件系统损坏、只读重新挂载或存储设备故障。诸如 Input/output error(输入/输出错误)和 Read-only file system(只读文件系统)这类错误,需要的修复方式与“设备上没有空间”不同。可能需要进行文件系统检查,但不要在已挂载的文件系统上运行 fsck。应安排停机时间,先卸载文件系统,再按相应的恢复流程操作。

故障排查

df -h 显示有空间,但 df -i 显示 100%:文件系统已耗尽其 inode 表。找到包含海量文件的目录,然后依据所属应用的保留策略删除或归档文件。

df 显示的用量多于 du:很可能是某个进程仍打开着一个已删除的文件。运行 sudo lsof +L1,定位该进程,并重启所属服务以释放数据块。

错误仅发生在 /tmp、/run 或 /dev/shm:该路径可能是容量受限的 tmpfs 挂载。请针对该确切路径运行 df -h 与 df -i,而不是检查 /。

主机有空闲容量,Docker 却报告没有空间:使用 docker info、docker system df 与 findmnt -T /var/lib/docker 检查 Docker 的存储路径与文件系统。在删除之前,先查看未使用的镜像、容器、构建缓存与卷。

df 显示有空闲容量,Btrfs 却报告没有空间:运行 sudo btrfs filesystem usage /affected/path,对比数据、元数据与未分配空间。在删除不需要的文件或快照之后,限定为 -dusage=0 -musage=0 的 balance 可以回收空的块组,而无需启动一次完整的 balance。

编辑器、构建工具或同步守护进程报告没有空间,而 df 与 df -i 看起来都正常:该进程已触及 inotify 监视限制。检查 fs.inotify.max_user_watches,排除程序无需监视的目录,并在工作负载确实需要时于 /etc/sysctl.d/ 中提高该限制。

常见问题

什么是 inode?inode 存储文件或目录的文件系统元数据,例如其大小、所有者、权限、时间戳以及数据块的位置。每个文件系统的 inode 数量都是有限的,而每个文件无论大小通常都会消耗一个 inode。

能否为现有的 ext4 文件系统增加更多 inode?inode 数量是在创建文件系统时选定的,无法通过在线设置更改来增加。实际的解决办法是删除多余文件、将工作负载迁移到另一个文件系统,或者以更小的 bytes-per-inode(每 inode 字节数)比例重新创建文件系统,以适应大量小文件。

为什么删除大文件后空间没有立即释放?一个正在运行的进程可能仍打开着那个已删除的文件。Linux 只在最后一个文件描述符关闭时才释放数据块,因此 df 仍会将其报告为已用空间。运行 sudo lsof +L1 找到该进程,并重启所属服务。

为什么在拥有数 TB 空闲空间的系统上也会出现该错误?失败的写入受限于总容量之外的其他因素,最常见的是特定挂载点的 inode 数量、容量受限的 tmpfs、容器存储、Btrfs 分配或 inotify 监视限制。请始终检查报错的确切路径,而不是根文件系统。

总结

当 df -h 显示有空闲空间时,应使用 df -ih 检查受影响路径,并借助 findmnt 确认其挂载点与文件系统类型。inode 耗尽、已满的嵌套挂载点或 tmpfs、容器存储、Btrfs 分配以及 inotify 监视限制,各自需要不同的修复方式;而 lsof +L1 则解决另一种情形——df 已满,但 du 却无法解释这些用量。