首页 运维干货磁盘空间明明够,为什么还提示 No space left on device ?

磁盘空间明明够,为什么还提示 No space left on device ?

运维派隶属马哥教育旗下专业运维社区,是国内成立最早的IT运维技术社区,欢迎关注公众号:yunweipai
领取学习更多免费Linux云计算、Python、Docker、K8s教程关注公众号:马哥linux运维

1. 先复现一个典型场景

应用写 /var/lib/app/cache/item 时报 No space left on device,值班人员执行 df -h / 却看到根分区还有几十 GiB,于是怀疑应用代码有问题。真正的写入路径可能在独立挂载的 /var/lib/app,或容器的 writable layer、/tmp、NFS、用户配额之下。ENOSPC 只告诉你一次资源分配失败,不能说明整台机器的所有磁盘都满了。

本文按“错误的实际路径→挂载点→块与 inode→配额和容器→已删除仍占空间→文件系统状态”逐层排查。示例目录、输出均为教学用途;清理、扩容和重启进程会影响数据,先确认归属与回滚。

# 从应用日志找完整路径、时间、容器/进程
journalctl -u app.service --since '30 min ago' --no-pager | tail -100


# 对实际出错的目录查文件系统,不只查 /
findmnt -T /var/lib/app/cache -o TARGET,SOURCE,FSTYPE,OPTIONS

2. 理解 ENOSPC 的几种来源

Linux 文件写入需要数据块、inode、目录项以及底层文件系统可以分配的空间。df -h 主要看块;df -i 看 inode。一个 100 GiB 分区只有几十万个 inode,若被海量小文件用尽,即使块剩余很多,也可能无法创建新文件。ext4 还有保留块;非特权用户看到的可用空间可少于总剩余。容器配额、项目配额和网络存储的服务端限制,也可能先于物理盘限制触发。

现象第一条命令下一步
数据块耗尽df -h /实际路径找目录占用、清理或扩容
inode 耗尽df -i /实际路径找小文件集中目录
容器独有失败findmnt、运行时状态查 overlay、临时层与配额
df 与 du 差很大lsof +L1查已删除仍被打开的文件
单用户失败quota、项目限额查用户/项目额度
应用只在 /tmp 失败findmnt -T /tmp查 tmpfs 限额与内存

不要把 EDQUOT(配额超限)与 ENOSPC 混为同一个 errno:不同文件系统、运行时与应用可能把上层错误描述得相似。日志应保留原始 errno、写入路径和底层调用。

3. 确认真正失败的是哪条路径

应用配置可能写 data_dir=/data,但临时文件先落 /tmp,日志写 /var/log,数据库事务日志写 /var/lib/mysql,镜像层在 /var/lib/containerd。如果日志只显示“写文件失败”,可查看应用配置、文件描述符和系统调用跟踪。不要未经授权就对生产进程长期运行 strace,它有开销并可能暴露敏感数据。

systemctl cat app.service

sudo ls -l /proc/1234/fd | head -30


# 受控短时采样:只跟踪文件相关失败,先在测试环境评估开销
sudo strace -f -p 1234 -e trace=openat,write,creat -o /tmp/app.strace

应用报错时注意“临时路径”与“最终路径”。某些编辑、上传或数据库操作先在同目录写临时文件,再用 rename 原子替换。目标文件系统即使已有原文件,也必须为新临时文件和元数据留空间。

4. 检查挂载点与容量

对实际路径运行 findmnt -T,可以看到哪个文件系统承载它。再对同一路径执行 df -h 与 df -i。df -h / 和 df -h /var/lib/app 可能分别落在不同设备;容器内外看到的挂载命名空间也可能不同。

findmnt -T /var/lib/app/cache -o TARGET,SOURCE,FSTYPE,OPTIONS


df -hT /var/lib/app/cache

df -i /var/lib/app/cache


stat -f -c 'type=%T blocks=%b free=%f available=%a inodes=%c ifree=%d' /var/lib/app/cache

free 与 available 可能不同:前者表示空闲块,后者表示当前用户可用的块,文件系统保留空间会影响后者。所有结果要在应用相同的 mount namespace 和身份下解释。

5. inode 用尽怎么定位

小文件集中在日志切片、会话缓存、邮件队列、临时目录或 CI 工作目录时,du -sh 可能显示占用不大,却把 inode 用光。先找 inode 数量最多的目录,不要直接 rm -rf:有些目录是数据库数据文件、对象存储索引或正在写入的队列。

# 粗查目录级文件数量,避免跨文件系统
sudo find /var/lib/app -xdev -type f | wc -l

# 按一级子目录统计;大目录执行可能较慢
for d in /var/lib/app/*; do
  [ -d "$d" ] || continue
  printf '%s ' "$d"
  sudo find "$d" -xdev -type f | wc -l
done


# 找较早且由应用确认可清理的缓存文件,先只列出
find /var/lib/app/cache -xdev -type f -mtime +30 -print | head -50

清理策略应由文件所有者确认:按时间、状态或应用提供的命令删除。一次删除百万小文件可能对文件系统和业务 IO 造成冲击,可分批执行并观察 inode 释放与延迟。长期方案是生命周期管理、减少小文件数量或选适合工作负载的存储。

6. df 有空间,用户却无法写:ext4 保留块

ext4 可给特权用途保留块,以便根分区接近写满时仍保有恢复余地。普通用户可能在 df 的 Avail 归零时失败,而 root 仍能创建文件。用 tune2fs -l 查看保留块比例与数量;修改需评估根分区恢复空间和文件系统类型,不建议为了让监控好看随意调为零。

findmnt -T /var/lib/app -o SOURCE,FSTYPE


# 只读查看 ext4 元信息;把设备名替换为 findmnt 的 SOURCE
sudo tune2fs -l /dev/mapper/data-lv | rg 'Block count|Free blocks|Reserved block count|Block size'

# 用应用账号重现,可验证是否身份相关
sudo -u appuser sh -c 'touch /var/lib/app/cache/.write-check && rm /var/lib/app/cache/.write-check'

上面的探测会实际写入一个文件,只在确认路径安全且有权限时运行。若生产账号不能写而 root 可以,除保留块外还需核对配额、权限和只读挂载,不能单凭差异锁定 ext4。

7. df 与 du 对不上:已删除但仍被打开

Linux 删除目录项不会立刻释放正在被进程打开的文件数据。日志轮转后旧日志被删除,但进程仍持续写旧文件描述符,du 看不到它,df 仍显示已占用。先查“链接数为零但仍打开”的文件和进程,再让应用安全重新打开日志;不要直接向数据库或重要进程发送信号。

sudo lsof +L1


# 聚焦指定文件系统,避免输出过大
sudo lsof +L1 /var/lib/app

# 查看某进程的文件描述符目标
sudo ls -l /proc/1234/fd | rg 'deleted'

通常按应用的日志重新打开机制处理,例如服务支持的 reload 或轮转脚本。若只能重启,先评估业务中断与副本可用性。不要对正在打开的大文件随意 truncate /proc/PID/fd/N,可能破坏应用状态或日志完整性。

8. /tmp 与 /dev/shm:tmpfs 是另一种容量

/tmp 可能是磁盘也可能是 tmpfs;/dev/shm 常为 tmpfs。某模型服务、数据库或 Python 程序在共享内存中写临时文件时,宿主磁盘空闲不代表 tmpfs 有空间。容器内 /dev/shm 的限制可能更小。

findmnt -T /tmp -o TARGET,SOURCE,FSTYPE,OPTIONS


df -h /tmp /dev/shm
   bash# 在容器里执行才看到该容器的挂载限制
df -h /dev/shm

若是 tmpfs,扩容可能消耗内存;若宿主机内存紧张,盲目增大上限会将问题从 ENOSPC 变成 OOM。先找谁在使用、是否有过期临时文件,并评估程序的清理逻辑。

9. 容器 writable layer 与 overlay

容器内 df / 看到的 overlay 与宿主机的数据盘不一定同一个视角。镜像层、容器可写层、日志和卷各占不同路径。Kubernetes 还可能有 ephemeral-storage 请求/限制与驱逐机制;具体报错可能由文件系统返回或由 kubelet 处理,不能只从宿主机 df / 判断。

# 宿主机确认容器运行时目录落在哪个挂载
findmnt -T /var/lib/containerd -o TARGET,SOURCE,FSTYPE


# Pod 内检查真正写入路径
kubectl -n demo exec myapp-0 -- sh -c 'df -hT /tmp /app/data; df -i /tmp /app/data'


# 查看 Pod 的存储请求、限制与事件
kubectl -n demo describe pod myapp-0 | rg -n 'ephemeral-storage|Evicted|DiskPressure' -A 4

容器日志若由运行时写在宿主机,删除容器内临时文件并不会解决宿主机日志占用。对业务持久化数据应使用正确的卷,而不把长期数据放在可写层。

10. 配额和项目限制

用户配额、组配额、XFS 项目配额、容器卷配额都可能让一个应用先达到自身额度,而整盘仍有大量空间。首先看 findmnt 的文件系统类型与挂载选项,再用相应配额工具查询。工具不可用或配额未开启时不要凭命令失败推断“没有配额”。

findmnt -T /var/lib/app -o FSTYPE,OPTIONS

# 系统启用用户配额时查询应用账号
sudo quota -u appuser


# XFS 项目配额需要确认实际挂载点和项目配置
sudo xfs_quota -x -c 'report -h' /var/lib/app

修改额度前对照租户/项目归属和计费策略;配额本身可能是有意保护其他业务。若误把共享目录纳入错误项目,修正映射后还要验证所有受影响账号。

11. 网络文件系统与远端卷

NFS 客户端 df 看到的空间只是服务端提供的视图;服务端配额、快照保留或子目录限制可能先触发写入失败。分布式存储如 Ceph、云文件系统还有自身的池容量和配额。先定位 FSTYPE、挂载源与服务端状态,别在客户端运行本地 tune2fs。

findmnt -T /mnt/share -o TARGET,SOURCE,FSTYPE,OPTIONS

df -hT /mnt/share


# NFS 场景看挂载详情,需按运行环境选择工具
nfsstat -m

跨团队工单应附路径、挂载源、账号、文件大小、发生时间及原始错误;服务端管理员才能准确对照额度与空间。

12. 文件系统变只读与 ENOSPC 要分开

I/O 错误使文件系统被重挂为只读时通常表现为 EROFS 或 I/O 错误,而不是典型 ENOSPC。应用日志可能只给出笼统“写失败”,需要查看内核日志和挂载选项。不要在怀疑硬件故障时继续通过大量写入测试证明磁盘没满。

findmnt -T /var/lib/app -o OPTIONS

journalctl -k --since '1 hour ago' | rg -i 'I/O error|read-only|EXT4-fs|XFS'


lsblk -f

出现底层 I/O 错误时先保护数据、联系存储团队并核对备份,不能通过删除文件或调整 ext4 保留块解决。

13. 一个从错误到根因的虚构案例

假设应用每秒创建大量 2 KiB 缓存对象。df -h /var/lib/app 显示还有 80 GiB,df -i 却显示 100%。find 统计 /var/lib/app/cache 有约 200 万小文件,且大多数超过 30 天未访问。此时可与业务确认缓存重建代价,先分批删除过期对象,每批观察 inode 与接口延迟,再在应用层实现 TTL 与数量上限。这个数字是教学示例,不是生产建议。

find /var/lib/app/cache -xdev -type f -mtime +30 -print | wc -l


# 受控删除示意:先确认清单,再每批处理;真实路径需审批
find /var/lib/app/cache -xdev -type f -mtime +30 -print0 \
  | xargs -0 -r -n 1000 rm --

删除前可先将名单导出到受控位置、抽样审阅;对可能包含业务数据的目录不执行这条命令。长期把大量小文件存于该文件系统而不设置生命周期,故障会复发。

14. 容量监控要同时看块和 inode

节点监控至少看实际挂载点的可用块比例、可用 inode 比例、写入速率和增长趋势。只监控根分区会漏掉 /var、/data 和容器运行时所在卷。对关键挂载点设低阈值预警和增长率告警,避免等到归零才处理。告警标签用挂载点和文件系统,别把无关的只读镜像挂载当成生产数据盘。

100 * node_filesystem_avail_bytes{mountpoint="/var/lib/app"}
/ node_filesystem_size_bytes{mountpoint="/var/lib/app"}

100 * node_filesystem_files_free{mountpoint="/var/lib/app"}
/ node_filesystem_files{mountpoint="/var/lib/app"}

指标名取决于 node_exporter 版本与过滤配置。判断是否需要扩容,还要看日增长量、峰值批任务与安全余量,不能只用当前 10% 空闲推断还能用多久。

15. 常见问题与执行边界

df -h 空闲很多,为何还能报错? 看实际写入路径、inode、tmpfs、配额和容器层。du 加起来小于 df 的已用空间? 查已删除仍打开文件、权限导致的扫描遗漏、其他挂载或快照。能直接调大 ext4 可用块吗? 保留块是恢复空间,先确认原因和影响。能直接删除最大的日志吗? 先确认应用是否仍打开文件、日志保留要求与轮转方式。

排查顺序先从失败的系统调用与路径开始,找到承载的文件系统,再区别块、inode、配额和仍被占用的已删除文件。恢复后从应用账号重试同一写入,确认业务指标恢复,并把根因对应的容量监控和生命周期策略补齐。

16. 文件系统、容器与配额的特殊情况

df 和 du 为什么常对不上

df 基于文件系统块分配统计,du 沿目录树遍历可访问文件。两者测量对象不同。最常见差距来自已删除仍被打开的文件;此外权限不足导致 du 漏扫、在遍历中目录变化、文件系统元数据、快照或子挂载边界也会影响比较。不要把 du -sh / 与 df -h / 的数字直接相减就认定“丢了空间”。

# 同一文件系统内扫描,避免跨挂载叠加
sudo du -x -h -d 1 /var/lib/app 2>/tmp/du-errors.log | sort -h

wc -l /tmp/du-errors.log


sudo lsof +L1 | head -100

若应用正在快速写入、删除文件,两个命令采样时间不同也会产生显著差异。先判断差距是几十 MiB 还是几百 GiB,再决定是否深入查快照、隐藏挂载与已删除文件。不要在根目录上无差别长时间 du,尤其是网络挂载或大文件树;使用 -x 并选具体故障文件系统。

“删除日志没释放空间”的正确处理

日志文件被应用打开,rm 只移除路径,磁盘块在文件描述符关闭之前仍被占用。此时日志若继续写,进程可能持续向已删除文件写入。先确认是哪一服务、能否通过其官方 reload 信号重开日志;对 systemd 管理的服务优先用应用提供的日志轮转与 reopen 机制。若需重启,先检查副本和维护窗口。

sudo lsof +L1 | rg '/var/log|/var/lib/app'

sudo systemctl status app.service --no-pager


sudo journalctl -u app.service --since '30 min ago' --no-pager | tail -80

处理后再次运行 lsof +L1 与 df -h,确认文件描述符关闭和空间恢复;若应用仍有高增长速率,必须补轮转与保留期,防止第二天重现。

哪些文件不适合靠 rm 解决

数据库数据文件、WAL/binlog、容器运行时层、对象存储索引、软件包数据库、队列消息文件,都有应用级一致性要求。手工删除可能让“磁盘写满”变成“数据不可恢复”。优先使用对应系统的保留、压缩、回收、迁移或扩容流程。对 MySQL binary log,应先核对备份和复制位点,再按数据库命令管理;不能直接删除磁盘上仍在使用的 binlog。

# 只列出大目录和文件,勿自动删除
sudo du -x -h -d 1 /var/lib | sort -h


sudo find /var/lib/app -xdev -type f -size +1G -printf '%s %p\n' | sort -nr | head -30

分组:业务可重建缓存、历史日志、需要应用命令回收的数据、
必须保留的在线数据。各组由所有者确认再操作。

如果文件系统已接近满,执行大量日志输出或压缩可能需要额外工作空间;优先在其他卷保存证据,或先通过安全的保留策略释放小部分空间。

ext4、XFS 与保留空间不要混用

sudo tune2fs -l 适用于 ext 文件系统,不适用于 XFS。XFS 的用户/组/项目配额另用 xfs_quota;有些云卷或容器层又有自己的配额实现。先用 findmnt -T 获取 FSTYPE,再选择工具。通用“把保留块比例设为 0”既可能不适用,也可能破坏系统盘应急写入能力。

findmnt -T /var/lib/app -o TARGET,SOURCE,FSTYPE,OPTIONS

# ext4 只读查询
sudo tune2fs -l /dev/mapper/data-lv | rg 'Reserved block count|Block size'


# XFS 只读配额报告
sudo xfs_quota -x -c 'report -h' /var/lib/app

文件系统类型为 overlay、tmpfs、NFS、ceph 时,以上 ext4/XFS 命令不应机械套用。先定位真正后端,再沿该存储的配额与容量路径查。

容器里写满的是哪一层

Pod 报 ENOSPC 时,先在该 Pod 内执行 df -hT 与 df -i,找报错路径对应的挂载;再在宿主机核对容器运行时目录、Pod 卷及节点 DiskPressure。若是 emptyDir,区分磁盘型与内存型;若是 PVC,检查 PVC 容量和存储后端。容器内根目录还能写,不代表 /dev/shm 或某个挂载的卷可写。

kubectl -n demo exec myapp-0 -- sh -c 'df -hT /app/data /tmp /dev/shm'

kubectl -n demo exec myapp-0 -- sh -c 'df -i /app/data /tmp /dev/shm'


kubectl -n demo get pvc -o wide
   bashkubectl describe node node-1 | rg -n 'DiskPressure|ephemeral-storage' -A 8

emptyDir.medium: Memory 使用 tmpfs,大小和内存资源相关;提高 sizeLimit 可能导致内存压力,不能照着宿主机磁盘空闲量调。PVC 能否在线扩容取决于 StorageClass 和 CSI,变更前查产品能力。

一张用于交接的证据表

| 信息 | 示例结果 | 为什么需要 |
| --- | --- | --- |
| 错误路径 | /var/lib/app/cache | 找挂载点 |
| mount 源/类型 | data-lv/ext4 | 选工具 |
| 块 Avail | 待填 | 排块耗尽 |
| inode IFree | 待填 | 排小文件 |
| 配额 | 待填 | 排用户限制 |
| lsof deleted | 待填 | 排持有文件 |
| 应用账号 | appuser | 验证真实身份 |

证据必须来自故障时刻并在同一 mount namespace 下收集。把容器内的错误和宿主机另一个目录的 df 写在一起,容易得出“磁盘明明够”的错误结论。

一个 inode 爆满事件的演练

教学假设:部署后缓存路径每次请求生成三个临时文件,文件名含请求 ID,清理任务因权限变化连续五天未执行。业务日志首次出现 ENOSPC,df -h /var/lib/app 只有 38% 使用率,而 df -i /var/lib/app 的 IUse% 已达 100%。此时第一步不是扩充数据块,而是恢复 inode 可用量并找到产生小文件的流程。

df -h /var/lib/app


df -i /var/lib/app


find /var/lib/app/cache -xdev -type f -printf '%TY-%Tm-%Td\n' \
  | sort | uniq -c | tail -20

按日期分布看到文件数在发布后暴涨,进一步对照应用版本与清理任务日志。清理前抽样确定文件确属可重建缓存,按日期和批次删除,避免百万级 rm 一次把磁盘 IO 打满。清理后运行相同 df -i、应用账号小文件创建测试和业务接口验证。长期修复是关闭临时文件泄漏或恢复清理任务,不是把 inode 用量从 100% 清到 80% 就结案。

# 先仅预览名单,确认后再使用应用正式清理流程
find /var/lib/app/cache -xdev -type f -mtime +7 -print | head -30

一个已删除日志占满磁盘的演练

教学假设:df -h /var/log 显示 98%,du -x -sh /var/log 只统计到 20%。lsof +L1 发现某 Java 进程持有已删除的 200 GiB 日志文件。日志轮转脚本把旧文件直接删掉,应用未重开描述符。若服务有多副本,可先逐个让应用安全重开日志或滚动重启;若没有,先协调短暂中断和日志保存要求。进程关闭旧 fd 后再核对块回收。

sudo lsof +L1 /var/log | head -40

sudo ls -l /proc/1234/fd | rg 'deleted'


df -h /var/log

不要把 lsof 输出里的文件偏移误当当前物理块大小;稀疏文件、压缩和文件系统分配可能不同。也不要盲目向所有 Java 进程发送 HUP,不同程序对此信号语义不同。

一次应用账号配额触发的演练

教学假设:同一卷中应用 A 写入失败、应用 B 正常,df -h 和 df -i 都有余量。quota -u app_a 或 XFS 项目报告显示 A 的限制已到。先问配额是不是多租户隔离要求;若是,申请增加额度或清理 A 自身过期数据,不能为了恢复 A 把全局配额直接关掉。

id app_a

sudo quota -u app_a


sudo xfs_quota -x -c 'report -h' /data

查不到 quota 命令时先看 FSTYPE 与挂载选项。某些存储后端在服务端限额,客户端不一定暴露相同管理命令;需要联系存储所有者核对。

目录占用统计的成本与技巧

故障时一条 du -ah / 会遍历整个系统,可能进入海量小文件目录或慢 NFS 挂载。先用 findmnt -T 锁定目标文件系统,再按一级目录 du -x -d 1,只对异常目录细查。对于数千万 inode,单次遍历也可能很慢;生产可预先做目录级容量指标或对象生命周期统计。

sudo du -x -h -d 1 /var/lib/app | sort -h


sudo du -x -h -d 1 /var/lib/app/cache | sort -h | tail -30

sudo find /var/lib/app/cache -xdev -type f -size +100M -printf '%s %p\n' \
  | sort -nr | head -30

du 输出代表目录树的已分配块,硬链接可能影响计数方式;结果与 df 不相等时回到测量边界和已删除文件。保存扫描开始、结束时间,不把扫描造成的 IO 峰值误判为原始事故。

文件系统元数据与快照

快照、写时复制和文件系统保留空间可能让“看起来已删除的文件”仍占底层存储。具体行为依 LVM、Btrfs、ZFS、云盘与存储阵列而异;不能用 ext4 的 lsof +L1 解释所有差值。先识别文件系统和快照架构,再用该系统官方工具列出快照占用、保留策略和回收进度。

findmnt -T /data -o TARGET,SOURCE,FSTYPE


lsblk -f

# 若使用 LVM,查看卷和快照;只读
sudo lvs -a -o lv_name,vg_name,lv_size,origin,data_percent

快照往往承载备份和回滚能力,删除前确认依赖与保留期。某些云快照不计在客户机 df 中,不应把任何快照都当作本地 ENOSPC 的原因。

为什么写入失败却还能读取

读取已有文件通常不需要新块和 inode;写入可能需要新增块、文件、目录项或日志事务。因此系统可能仍能提供查询,写入接口却全部失败。先识别业务是否处于只读降级:数据库事务、上传、日志、队列消费与会话写入可能受影响。恢复空间后不仅测一次 touch,还要验证有代表性的业务写路径和持久化一致性。

块耗尽:新增数据、扩展文件失败。
inode 耗尽:创建新文件、目录项失败。
只读重挂载:写入通常另有 EROFS/I/O 错误。


# 仅在确认是安全测试目录时,用应用身份做创建、写入和删除
sudo -u appuser sh -c 'printf test > /var/lib/app/cache/.probe && cat /var/lib/app/cache/.probe && rm /var/lib/app/cache/.probe'

若 touch 成功但业务写入仍失败,可能实际路径、文件大小、配额或原子替换步骤不同,继续按原应用操作复测。

17. 按失败写路径定位与复现

先找失败路径,而不是先清根分区

ENOSPC 是一次写入操作返回的错误,诊断单位是“那次写入最终落到的文件系统”。应用配置写 /var/log/app,程序也可能把临时文件写到 /tmp,数据库又把 binlog 写到单独挂载的卷。同一台机器上 / 尚有几十 GB,某个 PVC 或 tmpfs 已满并不矛盾。先从报错记录提取完整路径、进程、时间和容器 ID;没有路径时用系统调用跟踪短时间复现,注意跟踪会增加开销。

journalctl -u myapp --since '30 min ago' --no-pager | rg 'ENOSPC|No space left|write|rename'

sudo strace -ff -tt -e trace=openat,write,rename,fsync -p "$(pidof myapp)" -o /tmp/myapp.strace


rg 'ENOSPC' /tmp/myapp.strace.*

strace 只在允许短时间附加的环境使用,结束后及时停止。若错误来自数据库进程,还需核对该进程的 datadir、redo、binlog 和临时表路径;若来自容器,进入容器观察其挂载表,而不是仅看宿主机默认命名空间。

目录、符号链接和挂载点的对应关系

路径可跨符号链接,也可能被 bind mount 覆盖。df /path 会找到路径所在文件系统,findmnt -T 给出源设备、文件系统类型和挂载参数。诊断时对“实际文件所在目录”做这些检查;若文件尚未创建,就对父目录检查。

readlink -f /var/log/myapp


findmnt -T /var/log/myapp -o TARGET,SOURCE,FSTYPE,OPTIONS,AVAIL

df -h /var/log/myapp; df -i /var/log/myapp

df -h 的剩余块并不是写入者一定能用的剩余块。应用账号的文件系统配额、项目配额和 ext4 保留块,都会让 root 与普通账号得到不同结果。把失败的 Unix 用户和容器 UID 一并记录,避免拿 root 的测试写入替应用背书。

同一目录做无害的小写入测试

在得到路径和账户后,可以创建极小的临时文件检查写权限与配额。不要用 dd 一次写满剩余空间,也不要在数据库目录直接建测试文件。应用若是容器内运行,应以相同 UID 和挂载命名空间执行;不方便复现时以观测数据为准。

sudo -u myapp sh -c 'f=$(mktemp /var/lib/myapp/.probe.XXXXXX) && printf x > "$f" && rm -f "$f"'


stat -f -c 'type=%T block_size=%S free_blocks=%f available_blocks=%a free_inodes=%d' /var/lib/myapp

id myapp

测试成功仅证明小文件创建成功,不能证明一个需要数 GB 连续写入或大量 inode 的任务可以完成。应用有预分配、原子替换或落盘后重命名操作时,真实空间需求可能大于最终文件体积。

判断日志轮转是不是造成空间无法回收

logrotate 通常会重命名旧日志,再通知服务重新打开文件。若服务不响应信号,进程仍持有旧 inode,目录里看不到原名字但块仍占用。先对比 lsof +L1 的大小、进程、文件系统,再按服务文档执行 reopen 或滚动重启。直接截断 /proc/PID/fd/N 可能影响写入偏移和审计记录,只能作为已评估过的应急手段。

sudo lsof +L1 | sort -k7,7nr | head -30


sudo lsof -p "$(pidof myapp)" | rg 'deleted|\.log'


sudo systemctl show myapp -p ExecReload -p MainPID

执行 reopen 后复查同一文件系统的 df -B1,确认释放量与该文件大小相符;仍无变化则查是否另有写入者、快照或打开句柄。不要把“删了文件”当作已释放空间的证据。

临时文件与容器可写层的两种陷阱

容器的 emptyDir、镜像可写层、挂载卷和 tmpfs 是不同资源池。容器内 df 看到的可用空间可能是容器运行时暴露的视图;Kubernetes 的临时存储还会因节点 ephemeral-storage 资源紧张而驱逐 Pod。容器根文件系统写大量日志也可能增长 overlay 上层,而 PVC 仍充裕。需要结合 Pod 状态、事件、节点文件系统和挂载声明判断。

kubectl describe pod myapp-0 -n prod | rg -n -A 12 'Volumes:|Events:|ephemeral-storage'
kubectl exec -n prod myapp-0 -- sh -c 'df -h /tmp /var/log/app /data; df -i /tmp /var/log/app /data'


kubectl get pod myapp-0 -n prod -o jsonpath='{.spec.containers[*].resources.requests.ephemeral-storage}'

节点层的 df 和 du 只能协助定位,别直接手工删除容器运行时数据目录。优先明确是哪一个卷、哪个容器路径、哪个应用进程产生了文件,再采取应用级清理或扩容。

inode 需求如何估算

假设每天生成 200 万个独立小文件,保留 14 天,粗略需要 2800 万个 inode,另留目录、索引和安全余量。容量规划不能只算平均每个文件 2 KB 的数据量;极小文件会产生元数据和分配单元开销。若某目录已堆积海量文件,不要在业务高峰对整个文件树反复 du 或 find -ls。

files_per_day, retention_days, headroom = 2_000_000, 14, 1.3
print(int(files_per_day * retention_days * headroom))

df -i /var/lib/myapp


sudo find /var/lib/myapp/cache -xdev -type f -printf '.' | wc -c

find 仍须遍历目录树,可能造成明显 I/O;先看 inode 百分比与业务生成速率,必要时限制任务优先级,在低峰期执行。长期治理通常是聚合小文件、缩短可再生缓存的保留期,或选择适合文件数量的文件系统和布局。

大文件写入的峰值空间

原子更新常先写 .tmp,fsync 后再 rename,旧文件在替换之前也仍占块。覆盖一个 80 GB 文件,如果采用复制后替换,峰值接近新旧两份,再加索引和日志。写入失败时最终目录只剩旧文件,容易误判为“明明还够”。

findmnt -T /data/reports -o SOURCE,FSTYPE,AVAIL

sudo du -sh /data/reports /data/reports/.tmp 2>/dev/null

old_gib, new_gib, reserve_gib = 80, 82, 12
print('estimated_peak_free_gib', new_gib + reserve_gib)

这里的估算是工作流需求,不是文件系统保证值。若使用 CoW、稀疏文件、压缩或 reflink,物理块分配会不同,实际仍应在预生产用同样的写法测峰值。

监控至少覆盖四个维度

块使用率、inode 使用率、写入错误和增长速度要一起看。某磁盘剩余 20% 但每天增长 10%,两天就可能触底;某 inode 只剩 5% 却没有数据量告警,也会提前出故障。node_exporter 指标名称与标签应先在实际环境核实,以下 PromQL 用于展示思路。

1 - node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}

1 - node_filesystem_files_free{mountpoint="/data"} / node_filesystem_files{mountpoint="/data"}

predict_linear(node_filesystem_avail_bytes{mountpoint="/data"}[24h], 24 * 3600) < 0

预测函数只在增长趋势近似稳定时有参考意义。部署、批处理、日志轮转会改变斜率。告警标签应包含节点、挂载点和文件系统;不能把容器的多个 mountpoint 混为一条容量线。

故障关闭条件

空间恢复并不等于服务恢复。应用可能在写入失败后进入只读状态,数据库可能暂停复制,队列可能积压。验证应从同一路径的小写入到业务 API、日志、后台任务,再到余量与增长趋势,并留下原始故障路径和修复动作的记录。

df -h /var/lib/myapp; df -i /var/lib/myapp


journalctl -u myapp --since '10 min ago' --no-pager | rg 'ENOSPC|No space left|read.only|error' || true


curl -fsS https://service.example.com/health

health 端点可能只检查进程活着;应补一条代表真实写路径的受控业务操作。最后确认不会立刻再次写满:记录剩余可用块和 inode、当前增长速率、清理策略的责任人和后续扩容时间。

18. 误判辨析和应急处置

df 显示可用而应用仍报错的诊断矩阵

把故障按“谁写、写到哪里、要多少、资源哪一类不足”四轴拆开。块容量不足时 df -h 往往直接显现;inode 耗尽时 df -i 才明显;配额触发时同目录 root 写入正常而应用账号失败;单文件大小限制、进程文件描述符耗尽通常返回不同错误码,不能仅因文字近似就混进 ENOSPC。还有文件系统只读、I/O 错误、Kubernetes 临时存储驱逐等相邻症状,需保留原始 errno 和内核日志。

sudo journalctl -k --since '1 hour ago' --no-pager | rg -i 'enospc|no space|i/o error|read.only|ext4|xfs'

sudo -u myapp sh -c 'umask 077; f=$(mktemp /var/lib/myapp/.check.XXXXXX) && printf ok > "$f" && cat "$f" && rm "$f"'


findmnt -T /var/lib/myapp -o TARGET,SOURCE,FSTYPE,OPTIONS

若小写入成功而批量任务失败,逐步核对任务会产生多少文件、临时文件是否与目标位于同一卷、是否预分配大文件,以及配额剩余量。测试写入应在批准的业务目录且留好清理动作;用 root 的成功结果不能推断应用账号也能写。

对已删除文件的现场取证

lsof +L1 把链接数为零但仍被进程持有的文件列出来,其中大小和 inode 有助于解释 df 与 du 的差值。输出可能很大,先按挂载点过滤;也要注意某文件已删除但进程正在持续写,释放量会继续变化。查看 fd 的设备号和挂载表,确认不是把另一个盘的 deleted 文件算到目标盘。容器场景下进程 PID 和宿主机路径可能与容器内不同。

sudo lsof +L1 /var/log | head -60

sudo ls -l /proc/1234/fd | rg 'deleted'


sudo stat -Lc 'dev=%D inode=%i size=%s' /proc/1234/fd/7

这里只用示例 PID 1234、fd 7 说明核验方法。/proc/PID/fd/N 可能在查看过程中变化,操作前必须再次确认进程和文件。应用日志应优先通过明确的日志 reopen 接口解决;强制终止进程要考虑未完成交易与数据恢复。

从容器返回宿主机定位路径

Kubernetes 中应用报 /data 写入失败,先看它是 PVC、emptyDir、ConfigMap 还是容器可写层。对 PVC 查容量、实际挂载状态和 StorageClass 是否支持扩容;对 emptyDir 查节点临时存储和 Pod 配额;对 overlay 查容器日志、临时文件和镜像层上方的改写。把同一个路径名当成同一块盘,会在节点和 Pod 之间误判。

kubectl get pod myapp-0 -n prod -o jsonpath='{range .spec.volumes[*]}{.name}{" "}{.persistentVolumeClaim.claimName}{"\n"}{end}'


kubectl get pvc -n prod -o wide

kubectl describe node node-1 | rg -n -A 8 'Allocated resources|DiskPressure|ephemeral-storage'

PVC Bound 仅说明绑定,不证明卷内有余量;StorageClass 允许扩容也不意味着应用文件系统已完成在线扩容。变更前核对存储后端、文件系统类型、快照/备份与供应商流程。不要在节点上进入运行时目录直接 rm 容器层文件。

tmpfs 与内存压力

/dev/shm、某些 /run 或自定义挂载使用 tmpfs,容量取决于挂载限制和可用内存。机器数据盘还有空间,并不代表 tmpfs 也能写。某些应用把中间结果放到 /tmp,但 /tmp 在不同发行版和容器里可能是磁盘,也可能是 tmpfs;先查 FSTYPE 再下结论。tmpfs 写满与系统 OOM 是相关但不相同的故障。

findmnt -T /tmp -o TARGET,FSTYPE,SIZE,AVAIL,OPTIONS


df -h /dev/shm /run /tmp

free -h

若计划调大 tmpfs,先核算机器内存、容器内存限制和峰值使用。仅把 tmpfs 限额翻倍,可能使原本的 ENOSPC 变成 OOM;更稳妥的长期方案是调整中间文件位置、限制并发或改为流式处理。

用户配额与项目配额的实际核查

文件系统可用块充足时,配额是必须检查的分支。传统用户/组配额、XFS project quota、存储服务的卷限额和容器层限制分别在不同位置生效。应用账号和 root 可用量可能不同;在容器里 UID 映射或 rootless 模式也会改变真实写入主体。先查文件系统类型和挂载参数,再用对应工具读取配额,避免在 ext4 上照搬 XFS 命令。

findmnt -T /data/app -o TARGET,FSTYPE,OPTIONS


sudo quota -u myapp 2>/dev/null

sudo xfs_quota -x -c 'report -h' /data 2>/dev/null

配额报告显示硬限制触发时,需要按业务归属清理或扩容配额;若只是软限制,也要核对宽限期。随手把用户配额调为无限,可能掩盖持续增长的任务,下一次就会把整个共享文件系统写满。

ext4 保留块的边界

ext4 常保留一部分块供特权账户和系统服务使用。普通应用可能在 df 看见总空闲块还有余量,却无法写入;核对 f_bfree 与 f_bavail 的差异,以及 tune2fs -l 的保留块配置。XFS 没有同一套 ext4 参数,不能拿 tune2fs 修改它。降低保留比例属于文件系统级变更,先评估根盘与数据盘用途。

stat -f -c 'blocks_free=%f blocks_available_to_unprivileged=%a block_size=%S' /data


sudo tune2fs -l /dev/mapper/vg-data | rg 'Reserved block count|Block count|Block size'


lsblk -f

如果确实是大容量专用数据盘,保留比例可按运维策略调整,但这只能争取有限空间;持续增长的根因、告警阈值和扩容计划仍要解决。绝不在没有确认设备映射的情况下运行文件系统修改命令。

为什么 du 慢且可能漏算

du 逐项遍历目录树,亿级小文件时会与业务争抢元数据 I/O。du -x 可以限制在同一文件系统,避免把挂载在下级目录的其他卷算进去;但它无法计算已经 unlink 而仍被进程持有的文件,也可能受权限和并发写入影响。先用文件系统整体指标定位,再在有限目录上做离峰扫描。

sudo du -xhd1 /var/lib/myapp 2>/dev/null | sort -h

sudo ionice -c3 nice -n 19 du -xhd2 /var/lib/myapp 2>/dev/null | sort -h | tail -30


sudo lsof +L1 /var/lib/myapp | head -30

第二条仍可能很慢,优先使用存储平台和应用本身的容量索引。若 df 占用比 du 大,除 deleted 文件,还要考虑快照、文件系统元数据、稀疏文件与共享块等差异,按具体文件系统排查。

容量恢复之后的增长率核对

应急清理释放 100 GB 之后,若写入速度为每小时 20 GB,五小时就会复发。计算“距离阈值的时间”时用一段稳态窗口,不仅看瞬时剩余空间;保留业务峰值和轮转节奏。对于日志,检查轮转周期、保留天数、压缩和应用是否响应 reopen;对于缓存,确认过期任务和回收机制真正运行。

sudo systemctl list-timers --all | rg 'logrotate|cleanup|prune'

sudo journalctl -u logrotate --since '7 days ago' --no-pager | tail -80

free_gib, growth_gib_per_hour, reserve_gib = 120, 20, 20
print('hours_to_reserve', (free_gib-reserve_gib)/growth_gib_per_hour)

若趋势是锯齿状,简单线性预测会失真;应按业务峰值前的容量和最大一次批任务需求设安全边界。恢复报告既列释放的空间,也列未来增长和下次动作时间。

一份十五分钟应急记录

第一分钟记录原始错误路径和进程;随后确认目标挂载的块、inode、配额与读写状态;接着定位写入源、删除文件句柄、容器层和临时目录;最后选择可逆的应用级清理、扩容或限流。严重数据盘故障不要通过强行删除数据库文件来“腾空间”。应急时的顺序是先止住无限增长,再释放可再生数据,再恢复写入并验收。

| 时间 | 证据 | 判断 | 操作 | 验收 |
| --- | --- | --- | --- | --- |
| 10:00 | /var/log/app ENOSPC | 待判 | 保留日志 | 待测 |
| 10:05 | inode 100% | 小文件耗尽 | 暂停生成任务 | 待测 |
| 10:12 | inode 68% | 清理生效 | 恢复任务限速 | 写入成功 |


findmnt -T /var/log/app -o TARGET,SOURCE,FSTYPE,OPTIONS,AVAIL

记录谁批准清理、哪些文件可再生、是否有备份、回滚能否恢复。若文件系统进入只读或内核报告 I/O 错误,容量排障要转为存储故障处理;不要为了追求“恢复写入”继续向不稳定设备加压。

19. 扩容规划、业务验收与复盘

空间不足时不能盲删的目录

数据库数据目录、WAL/redo、binlog、容器运行时目录、正在打开的应用工作目录都可能含恢复所需状态。即便 du 看到它们最大,也不意味着文件可直接删除。数据库日志需通过数据库支持的保留与清理机制处理;容器镜像和日志需由运行时或编排系统管理;对象存储挂载的缓存需查专用工具。先确认目录所有者、生成机制、可再生性、保留策略和备份,再决定清理。

sudo du -xhd1 /var/lib 2>/dev/null | sort -h | tail -20

sudo fuser -vm /var/lib/mysql 2>/dev/null | head -30


sudo systemctl status mysql containerd --no-pager

这些只读命令也可能对大目录带来元数据负载,离峰使用。应急优先暂停无限生成的任务、清理已确认过期的应用缓存或扩容,绝不在数据库进程运行时用 rm 直接删除日志和数据文件来腾空间。

快照和 CoW 文件系统会改变直觉

在支持快照和写时复制的系统里,删除当前目录的大文件后,旧块仍可能被快照引用;du 显示目录变小,底层空间却没有按预期释放。稀疏文件的逻辑大小又可能远大于实际占块;reflink 复制的多个文件共享块,逐个 du 统计也不能简单相加解释物理使用。要使用存储平台或文件系统自身的空间统计查看快照、共享块与保留策略。

ls -lh /data/archive.img


du -h /data/archive.img

findmnt -T /data -o TARGET,SOURCE,FSTYPE,OPTIONS

如果文件系统是 Btrfs/ZFS 或云盘快照,进一步命令和释放规则必须按实际实现核查;不要把 ext4 的 df/du 解释原样套到所有存储。云盘扩容还涉及分区和文件系统扩展,控制台显示卷变大不等于挂载目录已变大。

申请扩容前的峰值模型

简单计算“当前占用 + 每日增长 × 保留天数”只是起点。还应加一次最大批任务的临时副本、日志轮转重叠期、索引重建或备份工作空间,以及故障处理安全余量。按 90 天留存的业务,不可把今天写入量外推为永远不变;至少准备最近高峰、促销或批处理峰值的实际数据。若 inode 是瓶颈,只扩展卷大小不一定按预期增加可用 inode,具体取决于文件系统和创建方式。

current_gib=450
peak_daily_growth_gib=12
retention_days=30
rebuild_workspace_gib=180
reserve_gib=100
print('planning_gib',current_gib+peak_daily_growth_gib*retention_days+rebuild_workspace_gib+reserve_gib)
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS

这 1090 GiB 只是演示参数求和,且可能重复计算留存中的已有数据;真实模型应把当前数据与未来净增长拆开。扩容前核对备份、文件系统在线扩展支持、容量配额和回滚路径,扩容后从应用路径验证。

真实用户症状为何会延迟出现

写失败可能先发生在日志、缓存或异步队列,前台 API 仍短时返回 200;随后日志丢失、任务积压和数据库事务失败才扩散到用户。监控不能只盯文件系统利用率,还要观察应用写错误、队列长度、数据库提交和用户操作成功率。恢复空间后需要补处理失败的任务,确认幂等与重试策略,而不是只看 df 的数字。

journalctl -u myapp --since '1 hour ago' --no-pager | rg -i 'write failed|enospc|retry|queue'

curl -fsS https://service.example.com/health

before={'failed_jobs':120,'pending_jobs':450}
after={'failed_jobs':0,'pending_jobs':30}
print('pending_reduced',before['pending_jobs']-after['pending_jobs'])

上述任务数字是教学示例。健康检查可能不覆盖写路径,验收应有一条受控的真实写操作和一个异步任务完成证据;还要确认重放不会造成重复扣费或重复写入。

复盘里应保留哪些事实

复盘不需要堆满全量日志,但要能回答哪个路径、哪个文件系统、哪个账号、哪一类资源、何时从健康变为耗尽、哪条操作释放了多少容量、业务何时恢复。图表标注部署和批处理时间点,区分 df 块、inode、配额和应用错误。若初始误判是看了根分区而遗漏挂载卷,应把路径映射写入值班手册,让下一次首分钟就能定位。

| 事实 | 现场值 | 证据源 |
| --- | --- | --- |
| 失败写路径 | /var/lib/myapp/cache | 应用日志 |
| 目标文件系统 | /dev/mapper/vg-data | findmnt -T |
| inode 使用率 | 待填 | df -i |
| 业务恢复时间 | 待填 | 写入探针与用户指标 |


findmnt -T /var/lib/myapp/cache -o TARGET,SOURCE,FSTYPE,AVAIL

最终行动项要具体到责任、阈值与验证方式:例如缓存目录保留天数、inode 告警、日志 reopen 演练和卷扩容。这样 ENOSPC 不再只是一次“删除几个文件就好”的临时处置。

常见误判的逐项复核

“df 有余量,所以不是空间问题”:必须确认 df 参数是失败文件的父目录,而不是默认根分区;随后查 df -i、配额、tmpfs 与容器层。“删了几个 GB,df 没动”:查打开但已删除的文件、快照保留、持续写入和测量是否在同一挂载点。“root 能写,应用不能写”:按应用 UID、目录权限与配额重新验证,别把不同身份的结果混用。

“扩容卷后仍报错”:看云盘容量、分区容量、文件系统容量是否逐层扩展,lsblk、findmnt 和 df 要对应同一个设备。若是 inode 固定数量耗尽,扩容后的 inode 变化需现场验证。“日志太大就删”:若进程仍持有旧 inode,不会立即释放;应用日志与数据库事务日志还涉及恢复和审计,要用对应产品支持的轮转或清理流程。

lsblk -f


findmnt -T /var/lib/myapp -o TARGET,SOURCE,FSTYPE,AVAIL

df -h /var/lib/myapp; df -i /var/lib/myapp

一条稳妥的结论应能指出“哪次写、哪一层、哪种资源耗尽、为什么此前的容量截图不能说明问题”,而不是只写“磁盘满了”。如果所有空间指标都健康,回到原始 errno 和系统调用确认应用是否把其他错误包装成 ENOSPC。

按文件系统类型留存操作记录

不同文件系统的保留块、配额、快照、在线扩容和 inode 行为不同。交接表应记录 findmnt 的 FSTYPE 与源设备,再记录所用工具和结果。例如 ext4 才谈 tune2fs 的保留块;XFS 项目配额用对应工具;NFS 或对象存储网关还要看服务端限额和连接状态。教程里的命令是诊断路径,不是所有文件系统通用的维修脚本。

findmnt -T /data -o SOURCE,FSTYPE,OPTIONS,TARGET


stat -f -c 'free=%f available=%a inodes_free=%d type=%T' /data

stat -f 的块数需乘块大小才能与字节比较,且 available 面向非特权账户也未必覆盖外部配额。形成最终容量结论时写清采集账户、目录、时间、工具输出和计算过程,方便下一班值守复核。

最后核验:写入路径、剩余资源和增长源

完成排障时,用失败应用的身份在同一目录验证真实写路径,确认原错误在日志中停止、块与 inode 都留有余量、配额未再次触顶。若是缓存清理造成恢复,还要确认生成任务已经限速或到期回收任务正常执行。记录前后 df、findmnt、应用写入成功率和预计再次触阈时间;任一项未知都应列为待跟进事项,而不是直接关闭事件。

findmnt -T /var/lib/myapp -o TARGET,SOURCE,FSTYPE,AVAIL

df -h /var/lib/myapp; df -i /var/lib/myapp

根因描述应精确到 inode、块、配额、已删除文件句柄、快照或 tmpfs,而非笼统的“磁盘问题”。这也决定了下次告警该落在哪一层。

参考资料

  • Linux df(1): https://man7.org/linux/man-pages/man1/df.1.html
  • Linux du(1): https://man7.org/linux/man-pages/man1/du.1.html
  • Linux findmnt(8): https://man7.org/linux/man-pages/man8/findmnt.8.html
  • Linux ext4 文档: https://docs.kernel.org/admin-guide/ext4.html
磁盘空间明明够,为什么还提示 No space left on device ?插图

本文链接:https://www.yunweipai.com/archives/49452

网友评论comments

发表回复

您的电子邮箱地址不会被公开。

暂无评论

Copyright © 2012-2022 YUNWEIPAI.COM - 运维派 京ICP备16064699号-6
扫二维码
扫二维码
返回顶部