Linux 磁盘空间告警:区分块空间、inode、已删除文件与底层容量

143 次浏览6 条回复

适用于 Linux 主机出现 No space left on device、日志无法写入或磁盘使用率突增的场景。命令基线为 Linux 5.4+、coreutils 8.30+、util-linux 2.36+、systemd 245+;lsof、LVM 和配额工具为可选项。读取其他进程的文件描述符、系统日志及块设备信息通常需要 root。先保留告警窗口,不要一开始就删除日志、截断文件、卸载文件系统或清理容器运行时目录。

1. 先把路径映射到实际文件系统

date -u
df -hT
df -ih
findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
lsblk -o NAME,KNAME,TYPE,PKNAME,FSTYPE,SIZE,FSAVAIL,FSUSE%,MOUNTPOINTS

df -hT 看块空间,df -ih 看 inode。块空间有余量而 inode 为 100% 时,新建小文件同样会失败;反之,大文件增长通常先耗尽块空间。示例路径必须替换为报错程序实际写入的路径,因为 bind mount、独立挂载和容器挂载命名空间可能让同一路径字符串落到不同文件系统。还要保留挂载选项;只读重新挂载通常会报告不同错误,不能按容量问题处理。

2. 在同一文件系统内定位增长目录

GNU du 可限制在当前文件系统,避免把网络盘或其他挂载混入统计:

du -xhd1 /path/to/mount 2>/dev/null | sort -h
du -xhd1 /path/to/mount/large-dir 2>/dev/null | sort -h
find /path/to/mount -xdev -type f -size +1G -printf '%s %TY-%Tm-%TdT%TH:%TM:%TS %p\n' \
  2>/dev/null | sort -n | tail -n 30

全盘扫描会产生额外 I/O,繁忙主机应从已知增长目录开始,并在低优先级或受控窗口执行。文件名可能包含换行,以上 find 输出只适合人工初筛,不应直接交给删除脚本。先确认文件的所有者、保留策略和是否仍在写入,再决定清理方式。

3. df 很高但 du 对不上时

最常见原因之一是文件已从目录树删除,但仍被进程打开。安装 lsof 后可检查链接计数小于 1 的打开文件:

lsof -nP +L1

重点记录进程、PID、文件描述符和大小。不要直接写 /proc/<PID>/fd/<FD> 来截断文件;应按应用支持的方式重新打开日志、滚动文件,或在评估影响后重启对应进程。操作后再次执行 lsof +L1df -hT,确认引用释放且空间回收。

另一个原因是文件被后来挂载的文件系统遮住。先检查挂载层级:

findmnt --submounts /path/to/mount
findmnt -R /path/to/mount

不要为了查看被遮住的目录而在故障现场直接卸载生产挂载;应在维护窗口或隔离的挂载命名空间中核对。du 无法解释的空间也可能来自文件系统元数据、快照或保留块,需要继续看文件系统与存储层。

4. 分开检查日志、容器与底层卷

journalctl --disk-usage
du -xhd1 /var/log 2>/dev/null | sort -h
du -xhd1 /var/lib 2>/dev/null | sort -h
systemd-analyze cat-config systemd/journald.conf

日志占用高时先核对 journald 与 logrotate 的最终配置、轮转是否失败及应用是否异常放大日志。容器镜像层、可写层和日志必须通过实际运行时的只读统计命令确认;不要手工删除 /var/lib/docker/var/lib/containerd 或 kubelet 管理的文件。路径大小只是线索,运行时引用关系才决定哪些对象可安全回收。

若路径位于 LVM thin pool,还要检查数据与元数据占用;以下通常需要 root,并且仅适用于 LVM:

lvs -o vg_name,lv_name,lv_size,segtype,data_percent,metadata_percent

文件系统内仍有余量,不代表 thin pool、云盘配额或存储后端仍有可分配空间。ext4 保留块、XFS 项目配额以及用户/组配额也可能使普通服务先于 root 收到容量错误,应根据实际文件系统和已启用的配额工具读取证据,不要直接降低保留比例或放宽配额。

5. 验证闭环

处置后重新对同一应用路径执行 findmnt -Tdf -hTdf -ih,确认检查的是同一个文件系统;lsof +L1 不再显示目标进程持有的大型已删除文件;日志、容器和 LVM 指标没有继续增长;应用能在其原有用户、挂载命名空间和配额约束下恢复写入。

至少观察一个正常轮转或业务周期,并记录释放前后的块空间、inode、增长目录、持有进程以及底层卷指标。只看到使用率短暂下降不算闭环;还应确认增长来源、清理策略和告警阈值彼此一致。

补一个 inode 耗尽时的定位步骤。若 df -i 已确认目标文件系统的 inode 接近上限,可从挂载点开始按目录汇总文件数量热点(GNU coreutils 8.30+):

du --inodes -x -d1 /path/to/mount 2>/dev/null | sort -n
du --inodes -x -d1 /path/to/mount/largest-dir 2>/dev/null | sort -n

逐层进入计数最高的目录,通常能比按块大小扫描更快暴露小文件堆积、异常临时目录或失效分片。-x 保持在同一文件系统,避免把子挂载混入结果。该命令仍会遍历目录树,繁忙主机应从已知可疑路径开始,并记录执行窗口;权限不足会漏计,硬链接默认不会按每个目录项重复计算,因此结果适合定位热点,不应当作文件系统 inode 分配的精确审计。处置后仍需用同一路径的 df -i 和应用原用户的实际写入验证闭环。

再补一个容易被 df 掩盖的分支:同一挂载点看起来仍有余量,但服务用户写入失败、root 却可以写时,应优先核对 ext4 保留块和配额,而不是继续按目录找大文件。先固定实际设备、文件系统和挂载选项:

findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
df -hT /path/to/data

ext4 可只读检查超级块统计(通常需要 root,且设备名必须取自上一步):

tune2fs -l /dev/actual-device | grep -E 'Block count|Reserved block count|Reserved block percentage'

已启用用户或组配额时,可用 quota -s -u appuserquota -s -g appgroup 查看对应主体;XFS 若挂载选项含 uquotagquotaprjquota,可由管理员在目标挂载点执行:

xfs_quota -x -c 'state' /path/to/mount
xfs_quota -x -c 'report -h' /path/to/mount

这些工具的可见范围和输出受权限、文件系统及配额配置影响。不要看到保留块或限额就直接调低;应先确认服务实际 UID/GID、项目 ID 与写入路径。处置后使用原服务用户在原挂载命名空间做一次小文件创建、写入、fsync 和删除,并再次核对 df 与配额计数,才能区分“容量已释放”和“仅绕过了限制”。

补一个“磁盘并未满但仍报 No space left on device”的非存储分支:若错误出现在文件监视、热加载或配置发现阶段,inotify_add_watch(2) 达到同一真实 UID 的监视数上限时也会返回 ENOSPC。此时 df -hdf -i 正常,继续清理文件不会解决问题。Linux 5.4+ 可先只读检查:

sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances
find /proc/[0-9]*/fdinfo -type f -readable \
  -exec grep -Hc '^inotify wd:' {} \; 2>/dev/null \
  | sort -t: -k2,2nr | head -n 30

fdinfo 中每条 inotify wd: 对应一个 watch;路径里的 PID 与 FD 可帮助定位持有者。读取其他用户进程通常需要 root,容器 PID 命名空间也会造成视图不完整;而限制按真实 UID 汇总,不能只看单个进程。另需注意,创建 inotify 实例触及 max_user_instances 时常见的是 EMFILE,应保留失败系统调用和 errno,避免仅凭错误文案判断。

不要直接把 sysctl 数值持续放大。先确认是监视目录规模合理,还是进程反复添加且未释放 watch;修复或受控重启持有进程后,再观察 fdinfo 计数在一个正常业务周期内是否稳定,并从原服务用户、原命名空间验证文件监视恢复。若失败系统调用不是 inotify,再回到块空间、inode、配额和底层卷分支。

再补一个 Btrfs 特有的 ENOSPC 分支。适用于 Linux 5.4+、btrfs-progs 5.4+;先由 findmnt 确认实际文件系统,以下 Btrfs 查询通常需要 root:

findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
btrfs filesystem usage -T /path/to/mount
btrfs device usage /path/to/mount
btrfs filesystem df /path/to/mount
btrfs device stats /path/to/mount
journalctl -k --since '-30 min' --no-pager \
  | grep -Ei 'btrfs|enospc|no space'

Btrfs 分开分配 Data、Metadata 和 System 块组;某类块组接近满、设备未分配空间不足或配置的 DUP/RAID profile 需要额外设备空间时,即使通用 df 看起来还有余量,新的元数据分配也可能失败。判断时应同时保留 UsedAllocatedUnallocated、profile 和内核日志,不能只看一个百分比。

快照与 qgroup 也需要单独核对:

btrfs subvolume list -s /path/to/mount
btrfs quota status /path/to/mount
btrfs qgroup show -reF /path/to/mount

最后两条仅在启用了 quota/qgroup 时有意义;共享 extent 会使普通 du 和快照表面大小无法直接代表可释放空间。不要据此批量删除快照,也不要在空间已经紧张时直接执行无过滤的全盘 balance:balance 会重写块组并需要工作空间,scrub 则用于校验而不是回收容量。应先确认增长来源、快照保留策略、设备错误和当前 balance 状态,再依据对应 btrfs-progs 版本文档制定受控处置。处置后重复上述 usagedevice usage 与内核日志检查,并从原服务用户和原挂载命名空间完成小文件创建、fsync、删除验证。

补一个容器或隔离服务中的挂载命名空间分支。宿主机对同名路径执行 df,不一定看到报错进程实际使用的挂载;Linux 5.4+、util-linux 2.36+ 可先通过容器运行时或服务管理器的只读信息取得目标进程在宿主机上的 PID,再在其 mount namespace 内取证。进入其他进程的命名空间通常需要 root 或相应能力:

PID=<target-host-pid>
sudo nsenter -t "$PID" -m -- findmnt -T /path/to/data \
  -o TARGET,SOURCE,FSTYPE,OPTIONS
sudo nsenter -t "$PID" -m -- df -hT /path/to/data
sudo nsenter -t "$PID" -m -- df -ih /path/to/data

路径必须使用报错进程看到的路径,PID 也不能靠名称猜测;进程重启后 PID 和命名空间都可能变化,应同时记录采集时间与进程启动时间。若结果显示 overlay、独立的 tmpfs 或容器专属挂载,而宿主机原路径落在另一文件系统,就应沿该挂载的实际上层文件系统、容量限制和运行时引用关系继续检查。不要直接清理 overlay 的 upperdirworkdir 或运行时管理目录。

还要注意,nsenter -m 只切换 mount namespace,并不会自动复现应用的 UID/GID、用户命名空间、cgroup 或项目配额身份,所以用 root 在其中成功写入不能证明应用已经恢复。处置后的闭环应通过运行时支持的方式,以原服务用户和原约束执行一次小文件创建、写入、fsync、删除,并再次在同一命名空间核对块空间与 inode;这样可以避免把“宿主机有空间”误判成“容器可写”。

周末Lv1#5

补一个容器或隔离服务中的挂载命名空间分支。宿主机对同名路径执行 df,不一定看到报错进程实际使用的挂载;Linux 5.4+、util-linux 2.36+ 可先通过容器运行时或服务管理器的只读信息取得目标进程在宿主机上的 PID,再在其 mount namespace 内取证。进入其他进程的命名空间通常需要 root 或相应能力:

PID=<target-host-pid>
sudo nsenter -t "$PID" -m -- findmnt -T /path/to/data \
  -o TARGET,SOURCE,FSTYPE,OPTIONS
sudo nsenter -t "$PID" -m -- df -hT /path/to/data
sudo nsenter -t "$PID" -m -- df -ih /path/to/data

路径必须使用报错进程看到的路径,PID 也不能靠名称猜测;进程重启后 PID 和命名空间都可能变化,应同时记录采集时间与进程启动时间。若结果显示 overlay、独立的 tmpfs 或容器专属挂载,而宿主机原路径落在另一文件系统,就应沿该挂载的实际上层文件系统、容量限制和运行时引用关系继续检查。不要直接清理 overlay 的 upperdirworkdir 或运行时管理目录。

还要注意,nsenter -m 只切换 mount namespace,并不会自动复现应用的 UID/GID、用户命名空间、cgroup 或项目配额身份,所以用 root 在其中成功写入不能证明应用已经恢复。处置后的闭环应通过运行时支持的方式,以原服务用户和原约束执行一次小文件创建、写入、fsync、删除,并再次在同一命名空间核对块空间与 inode;这样可以避免把“宿主机有空间”误判成“容器可写”。

补充一个会影响楼上取证准确性的细节:只切换 mount namespace 并不等于切换进程的根目录。目标进程若使用了 chrootpivot_root 或容器 rootfs,直接在 nsenter -t "$PID" -m 后查询 /path/to/data,绝对路径仍可能从调用者保留的 root 解析,结果未必对应目标进程。

util-linux 2.36+ 可在目标根目录中执行;但这要求目标 rootfs 内存在 findmntdf 及其动态库:

sudo nsenter -t "$PID" --mount --root -- \
  findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
sudo nsenter -t "$PID" --mount --root -- df -hT /path/to/data
sudo nsenter -t "$PID" --mount --root -- df -ih /path/to/data

精简镜像没有这些工具时,可以继续使用宿主机工具,但把查询路径锚定到目标进程的 root;PID 必须是宿主机视角的 PID:

TARGET=/path/to/data
sudo nsenter -t "$PID" --mount -- \
  findmnt -T "/proc/$PID/root$TARGET" -o TARGET,SOURCE,FSTYPE,OPTIONS
sudo nsenter -t "$PID" --mount -- df -hT "/proc/$PID/root$TARGET"
sudo nsenter -t "$PID" --mount -- df -ih "/proc/$PID/root$TARGET"

执行前应确认 /proc/$PID/root 可访问,并记录 /proc/$PID/stat 的启动时间字段,避免进程退出后 PID 被复用。两种方式任选一种,核心是同时固定目标 mount namespace 与目标 root;随后仍应按楼上所述,用原 UID/GID、用户命名空间和配额身份完成写入闭环。