1. 问题本质:为什么“磁盘空间不足”报错会出现在 mkdir 这种看似不写数据的操作上?
在 Linux 系统里敲下mkdir testdir,终端却冷不丁甩出一句mkdir: cannot create directory ‘testdir’: No space left on device,你第一反应肯定是——打开df -h看一眼,发现/分区还剩 20GB,/home更有 80GB 余量,明明硬盘没满,怎么就“空间不足”了?这绝不是系统在跟你开玩笑,而是 Linux 文件系统底层机制在发出明确警告:设备级资源已耗尽,连创建一个空目录都做不到。
这个报错的核心关键词是No space left on device,注意它说的是device(设备),而不是disk(磁盘)或partition(分区)。它指向的不是我们日常理解的“存储容量”,而是文件系统层面两个独立且并行管理的资源池:块(block)和 inode。绝大多数人只关注块(即df -h显示的“Available”列),却忽略了 inode 这个隐形守门员。而mkdir操作,恰恰是对 inode 资源最敏感的动作之一——它不需要写入任何用户数据块,但必须向文件系统申请一个全新的 inode 来描述这个新目录的元数据(权限、所有者、时间戳、目录项指针等)。如果 inode 池枯竭,哪怕磁盘还有 1TB 空间,mkdir、touch、cp(新建文件)、git clone(创建大量小文件)全部都会被无情拒绝。
这个问题在实际运维中高频出现,尤其集中在三类场景:一是日志系统(如 Nginx、Java 应用日志滚动产生海量小文件);二是容器环境(Docker/Kubernetes 的临时卷、镜像层叠加导致 inode 快速消耗);三是开发环境(Node.js 项目node_modules动辄数万甚至数十万个文件,Git 仓库频繁 checkout 切换分支生成大量临时 inode)。我曾在一台 500GB 的生产数据库服务器上见过df -h显示根分区使用率仅 42%,但df -i却显示 inode 使用率高达 99.8%,结果连systemctl restart nginx都失败——因为重启服务时 systemd 会尝试写入新的 journal 日志条目,需要申请新 inode。
所以,当你看到这个报错,第一件事不是删大文件,而是立刻执行df -i。这是诊断的起点,也是区分“真空间不足”和“假空间不足”的唯一金标准。它不是 bug,是 Linux 文件系统设计的严谨性体现:inode 是文件系统的“身份证发放中心”,没有身份证,再大的房子也住不进新人。
2. 根本原因深度拆解:inode 耗尽的五大典型路径与底层逻辑
No space left on device报错背后,inode 耗尽并非偶然,而是有清晰可追溯的成因链条。下面我结合多年处理线上事故的经验,把最常见的五种路径掰开揉碎讲透,不仅告诉你“是什么”,更要说明“为什么偏偏是它”。
2.1 日志风暴:小文件海啸式生成
这是企业级服务器上最经典的 inode 杀手。以 Nginx 为例,每处理一个 HTTP 请求,就会在access.log中追加一行;当开启error.log的debug级别时,单个请求可能生成数十行日志。假设一个高并发 API 服务每秒处理 500 个请求,access.log每天新增约 4300 万行。如果未配置 logrotate 或轮转策略,这些日志会持续追加到同一个文件,看似只占一个 inode。但问题在于:当日志文件被重命名(如mv access.log access.log.1)或截断(> access.log)时,旧文件的 inode 并不会立即释放。Linux 的文件删除机制是“引用计数归零才回收”,只要还有进程(比如正在 tail -f 的监控脚本、或尚未关闭日志句柄的 Nginx worker 进程)持有该文件的打开句柄,这个 inode 就永远处于“已删除但未释放”状态,持续占用 inode 池。
实操验证:lsof +L1命令能直接列出所有被删除但仍被进程占用的文件(即“幽灵文件”)。我曾在一个客户现场执行此命令,发现nginx进程竟持有了 17 个已被logrotate重命名的旧日志文件,每个文件大小从几 MB 到几百 MB 不等,但它们共同吞噬了超过 20 万个 inode。这才是真正的“看不见的消耗”。
2.2 容器与临时文件系统:overlay2 的 inode 陷阱
Docker 默认使用overlay2存储驱动,其工作原理是在宿主机上为每个容器创建一层“upperdir”(可写层)和“lowerdir”(只读镜像层)。每次容器内执行apt install、npm install或pip install,都会在 upperdir 中创建海量小文件(.so库、.pyc字节码、package.json等)。更致命的是,容器退出时,upperdir 目录本身不会被自动清理。如果你频繁启停测试容器(比如 CI/CD 流水线每构建一次就起一个新容器),/var/lib/docker/overlay2/下会堆积成千上万个残留的 upperdir 目录,每个目录本身就是一个 inode,其内部文件又消耗更多 inode。docker system prune -a可以清理,但很多团队只记得prune镜像,忘了-v参数(--volumes)才能清理 volumes,而 upperdir 的 inode 就卡在 volumes 的阴影里。
一个量化案例:一个基础 Ubuntu 镜像docker run -it ubuntu:22.04 bash启动后,df -i /var/lib/docker显示 inode 使用率 12%;执行apt update && apt install -y curl后,飙升至 28%;若重复此过程 10 次(模拟 10 个不同构建环境),即使删除所有容器,inode 使用率仍停留在 65% 以上——因为 overlay2 的 cleanup 机制并不彻底。
2.3 临时文件与缓存:/tmp 和 /var/tmp 的“黑洞”
/tmp目录通常挂载为tmpfs(内存文件系统),其 inode 数量默认与内存大小强相关。例如,一台 16GB 内存的机器,tmpfs的 inode 上限可能是 200 万。但问题在于:很多程序(尤其是 Java 应用、Python 的tempfile.mkstemp())会在此创建临时文件,却因异常退出未能清理。这些文件在tmpfs中不占磁盘块,但每个文件都独占一个 inode。更隐蔽的是/var/tmp,它通常是磁盘分区的一部分,但常被忽略。systemd-tmpfiles服务负责定期清理/tmp,但/var/tmp的清理策略由管理员自定义,一旦配置疏漏(如AgeSec=设置过大),数月积累的临时文件就能填满 inode。
经验判断:find /tmp -type f -mtime +30 | wc -l如果返回值超过 10000,基本可以判定/tmp是 inode 消耗大户。而/var/tmp则需检查ls -la /var/tmp,重点关注systemd-private-*开头的目录——这是 systemd 为每个服务实例创建的私有 tmp 目录,若服务崩溃未清理,它们就是永久性 inode 占位符。
2.4 文件系统碎片化与小文件密集型应用
Ext4/XFS 等现代文件系统虽支持海量文件,但 inode 分配并非无限。创建文件系统时,mkfs.ext4默认按每 16KB 数据块分配 1 个 inode(可通过-i参数调整)。这意味着一个 100GB 的分区,若按默认比例,最多只能创建约 650 万个 inode。当你的应用是典型的“小文件密集型”时,这个上限很快见顶。典型代表包括:
- Git 仓库:每个 commit 对象、tree 对象、blob 对象都是独立文件,一个中等规模仓库(10k commits)轻松生成 50 万+ objects;
- 数据库 WAL 日志:PostgreSQL 的
pg_wal目录下,每个 WAL 段文件(通常 16MB)就是一个 inode,高负载下每分钟生成多个; - 编译缓存:
ccache默认将每个编译单元的缓存存为独立文件,C++ 项目头文件爆炸式增长时,cache 目录 inode 消耗速度惊人。
关键洞察:df -i显示的IUse%是全局统计,但它无法告诉你 inode 分布在哪。debugfs -R "stat /" /dev/sda1(Ext4)或xfs_info /dev/sda1(XFS)能查看文件系统创建时的 inode 总数,而find /path -xdev -type f | wc -l才是定位具体目录的精确手段。
2.5 用户态文件系统(FUSE)与挂载点污染
这是一个容易被忽视的“高级陷阱”。当你使用sshfs、rclone mount或gocryptfs挂载远程或加密文件系统时,这些 FUSE 实现会在内核中注册一个新的虚拟文件系统。每个挂载点本身会消耗少量 inode,而更重要的是,挂载点下的文件操作,其 inode 申请行为完全由 FUSE 后端控制,不受宿主机df -i统计约束。例如,rclone mount gdrive:/myfolder /mnt/gdrive后,在/mnt/gdrive下mkdir test,实际是向 Google Drive API 发起请求,成功与否取决于云端配额,而非本地 inode。但此时,如果挂载过程出错(如网络中断导致 FUSE 进程僵死),/mnt/gdrive目录会变成一个“不可访问的黑洞”,ls /mnt/gdrive可能卡住,df -i却看不到它——因为它不属于任何本地块设备。这种情况下,No space left on device报错往往伴随Transport endpoint is not connected,提示你问题不在本地 inode,而在挂载点本身。
排查口诀:遇到报错,先mount | grep fuse,再ps aux | grep rclone\|sshfs,确认所有 FUSE 进程状态。强行umount -l /mnt/gdrive(lazy unmount)往往是最快恢复手段。
3. 排查全流程:从现象到根因的七步精准定位法
面对No space left on device,盲目rm -rf可能引发灾难。我总结了一套经过上百次线上故障验证的七步法,确保每一步都有明确目标、可执行命令和预期输出,帮你像侦探一样锁定真凶。
3.1 第一步:确认报错根源——区分 block 与 inode
这是所有后续动作的前提。执行两条命令,缺一不可:
df -h # 查看磁盘块使用率 df -i # 查看 inode 使用率重点观察Use%列。如果df -h显示Use%在 85% 以下,而df -i显示Use%接近或等于 100%,则 100% 确定是 inode 耗尽。反之,若df -h的Use%也高达 95% 以上,则需优先处理大文件。永远不要跳过这一步。我曾见过运维同事直接du -sh /* 2>/dev/null | sort -hr | head -20扫描大文件,结果发现/var/log只有 2GB,却花了 20 分钟,而df -i早在 5 秒内就指出/var/lib/docker的 inode 使用率是 99.3%。
提示:
df -i的IUsed列是已用 inode 数,IFree是剩余 inode 数。计算公式:IUse% = (IUsed / (IUsed + IFree)) * 100。注意,IFree为 0 并不意味着绝对无法创建文件——文件系统会保留少量 inode(通常是 5%)给 root 用户,所以IUse%达到 95% 就该预警。
3.2 第二步:定位高 inode 消耗分区——聚焦根因所在
df -i输出可能包含多个挂载点(/,/home,/var,/boot等)。找出IUse%最高的那个。例如:
Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda1 5242880 5242879 1 100% / /dev/sdb1 26214400 12000000 14214400 46% /home显然,根分区/是问题核心。此时,切勿在/下盲目搜索,因为/proc、/sys等虚拟文件系统会干扰结果。正确做法是限定搜索范围:
# 只扫描真实块设备挂载点,排除虚拟文件系统 find / -xdev -type d 2>/dev/null | head -20-xdev参数是关键,它告诉find不要跨越文件系统边界,确保只在/分区内部搜索。接下来,我们需要找到哪个目录下的文件最多。
3.3 第三步:揪出 inode 密集目录——按文件数量排序
在确定的高消耗分区(如/)内,执行:
# 统计每个子目录下的文件总数(含子目录) find / -xdev -type f 2>/dev/null | cut -d "/" -f 2 | sort | uniq -c | sort -nr | head -10这条命令的精妙之处在于:
find / -xdev -type f:只找普通文件,排除目录、设备文件、socket 等;cut -d "/" -f 2:提取路径的第二段(即/var/log中的var,/usr/bin中的usr),这是顶层目录;sort | uniq -c | sort -nr:统计每个顶层目录的文件数,并按降序排列。
典型输出:
428912 var 210345 usr 87654 home这说明/var是罪魁祸首。接着,我们深入/var:
# 进入 /var,统计其下二级目录文件数 find /var -maxdepth 2 -type f 2>/dev/null | cut -d "/" -f 3 | sort | uniq -c | sort -nr | head -10输出可能显示log占比最高。此时,ls -l /var/log会看到一堆app.log.1,app.log.2.gz,nginx-access.log等文件。但这还不够,我们需要确认是否是“幽灵文件”。
3.4 第四步:检测幽灵文件——被进程占用的已删除文件
执行:
lsof +L1 2>/dev/null | awk '{print $1,$9}' | sort | uniq -c | sort -nr | head -10+L1是lsof的专属参数,专门列出链接数(link count)为 0 的文件,即已被rm删除但仍有进程打开的文件。awk '{print $1,$9}'提取进程名和文件路径,sort | uniq -c统计每个进程占用的幽灵文件数。
常见结果:
17 nginx 8 java 3 rsyslogd这直接指向了 Nginx 和 Java 应用。下一步,查看 Nginx 具体持有哪些文件:
lsof -p $(pgrep nginx) | grep deleted输出类似:
nginx 1234 root 5w REG 0,41 123456789 12345678 /var/log/nginx/access.log (deleted)12345678是 inode 号,(deleted)是关键标识。此时,重启 Nginx (systemctl restart nginx) 就能释放这批 inode。切记:重启前务必确认日志轮转配置已生效,否则重启后问题会重现。
3.5 第五步:分析容器残留——Docker overlay2 深度清理
如果df -i指向/var/lib/docker,执行:
# 查看 overlay2 下各目录的 inode 占用 du -s --inodes /var/lib/docker/overlay2/*/diff 2>/dev/null | sort -n | tail -10--inodes参数让du统计 inode 数而非磁盘大小。输出可能显示某个long-hash-string/diff目录占用 50 万个 inode。这极大概率是已停止但未清理的容器。此时,安全清理步骤是:
# 1. 列出所有已停止但未删除的容器 docker ps -a --filter "status=exited" --format "{{.ID}}" # 2. 删除所有已退出容器(谨慎!确认无重要数据) docker rm $(docker ps -a -q --filter "status=exited") # 3. 清理 dangling 镜像和 build cache docker image prune -f docker builder prune -f # 4. 最后,强制清理 overlay2(仅当上述无效时) # 注意:此操作会删除所有未运行容器的文件系统层 docker system prune -a -f --volumes注意:
docker system prune -a -f --volumes是“核武器”,执行前必须docker volume ls确认无重要持久化卷。我建议先用docker system df -v查看详细磁盘使用,它会明确列出Local Volumes、Build Cache的大小和数量,比盲目prune更安全。
3.6 第六步:检查挂载点健康——FUSE 与 NFS 的隐形故障
如果df -i无异常,但mkdir仍报错,立即检查挂载状态:
# 列出所有挂载点,特别关注 type 为 fuse.* 或 nfs 的 mount | grep -E "(fuse|nfs)" # 检查对应挂载点是否可访问 ls -la /mnt/myfuse 2>/dev/null || echo "Mount point inaccessible"若ls卡住或报错Transport endpoint is not connected,则问题在挂载点。解决方案:
- 对于
sshfs:fusermount -u /mnt/sshfs卸载,然后重新sshfs user@host:/path /mnt/sshfs; - 对于
rclone:killall rclone,再umount -l /mnt/gdrive,最后重启rclone mount; - 对于 NFS:
showmount -e nfs-server-ip测试服务端可达性,umount -f /mnt/nfs强制卸载。
3.7 第七步:终极验证——创建测试文件确认修复
所有清理操作完成后,必须进行闭环验证:
# 1. 创建一个新目录 mkdir /tmp/test_inodes_$(date +%s) # 2. 在其中创建 100 个空文件 for i in {1..100}; do touch /tmp/test_inodes_$(date +%s)/file$i; done # 3. 检查是否成功 ls -l /tmp/test_inodes_$(date +%s) | wc -l # 应输出 100 # 4. 清理测试目录 rm -rf /tmp/test_inodes_$(date +%s)如果mkdir和touch全部成功,且df -i的IFree值明显上升(例如从 12 增加到 10000),则问题已解决。不要跳过验证。我曾遇到一次案例,df -i显示IFree从 0 变为 5,但mkdir仍失败——原因是文件系统保留的 5 个 inode 只供 root 使用,普通用户权限不足。此时需sudo mkdir测试,或调整sysctl fs.inotify.max_user_watches(虽然这不直接影响 inode,但常被误认为相关)。
4. 解决方案与长效防护:从急救到免疫的完整体系
排查只是开始,真正体现专业性的,是建立一套既能快速止血又能杜绝复发的防护体系。下面是我为不同角色(开发者、运维、SRE)定制的解决方案矩阵,覆盖从命令行急救到架构级预防。
4.1 立即生效的急救命令集(5 分钟内恢复业务)
当报警响起,业务中断,你需要的是“开箱即用”的救命命令。以下命令均经过生产环境千锤百炼,按优先级排序:
第一优先级:释放幽灵文件(最常见,最快见效)
# 一键查找并 kill 占用 deleted 文件的进程(慎用!先确认进程可重启) lsof +L1 2>/dev/null | awk '{print $2}' | sort -u | xargs -r kill -HUP # 更安全的做法:针对特定进程重启 sudo systemctl restart nginx apache2 rsyslog sudo pkill -f "java.*application.jar" && sudo systemctl start myapp第二优先级:清理容器垃圾(Docker 环境必做)
# 安全清理:只删已退出容器、悬空镜像、构建缓存 docker container prune -f docker image prune -f docker builder prune -f # 彻底清理(确认无数据后执行) docker system prune -a -f --volumes第三优先级:日志轮转强制触发(Nginx/Apache/Java 通用)
# 对于 logrotate 管理的日志 sudo logrotate -f /etc/logrotate.conf # 对于 systemd-journald sudo journalctl --vacuum-size=500M # 限制日志总大小 sudo journalctl --vacuum-time=2weeks # 删除 2 周前日志第四优先级:临时扩容 inode(应急之选,不推荐长期使用)
# Ext4 文件系统:在线扩容 inode(需有足够未分配空间) # 此操作风险极高,必须先 `e2fsck -f /dev/sda1`,且无法缩减 # 生产环境强烈建议重建文件系统,而非在线扩容 # (此处不提供具体命令,因误操作将导致数据丢失)注意:所有
kill和restart操作前,务必systemctl status service-name确认服务状态,并评估业务影响。kill -HUP是平滑重启信号,比kill -9安全得多。
4.2 长效防护机制:从配置到监控的三层防线
急救解决当下,防护保障未来。我将防护体系分为三个层级,层层递进:
第一层:配置加固——在问题发生前就筑墙
日志轮转(Log Rotation):
编辑/etc/logrotate.d/nginx(或其他服务配置),确保包含:/var/log/nginx/*.log { daily missingok rotate 30 # 保留 30 天 compress delaycompress notifempty create 0644 nginx nginx # 确保新文件权限正确 sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` fi endscript }关键点:
rotate 30限制数量,compress减少磁盘块占用,postrotate中的kill -USR1通知 Nginx 重新打开日志文件,避免幽灵文件。容器生命周期管理:
在 Docker Compose 或 Kubernetes YAML 中,强制设置restart: unless-stopped和stop_grace_period: 30s,确保容器异常退出后能被自动拉起,且优雅终止。对于 CI/CD 流水线,添加docker system prune -f作为 job 结束步骤。临时目录自动清理:
编辑/etc/tmpfiles.d/custom.conf:# Type Path Mode UID GID Age d /tmp/appcache 0755 root root 1d d /var/tmp/build 0755 root root 3dsystemd-tmpfiles --create会按此规则自动清理。
第二层:监控告警——让问题在爆发前就浮现
仅靠人工df -i是落后的。必须接入监控系统:
Prometheus + Node Exporter:
关键指标:node_filesystem_files_free{mountpoint="/",fstype="ext4"}和node_filesystem_files{mountpoint="/",fstype="ext4"}。
告警规则(PromQL):(node_filesystem_files{mountpoint="/"} - node_filesystem_files_free{mountpoint="/"}) / node_filesystem_files{mountpoint="/"} * 100 > 90触发阈值设为 90%,留出 10% 缓冲。
Zabbix 自定义监控项:
vfs.fs.inode.pfree[/](inode 空闲百分比),触发器表达式:{HOSTNAME:vfs.fs.inode.pfree[/].last()}<10。Shell 脚本轻量监控(无监控系统时):
# /usr/local/bin/check-inodes.sh THRESHOLD=90 INODE_USE=$(df -i / | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$INODE_USE" -gt "$THRESHOLD" ]; then echo "ALERT: Inode usage on / is ${INODE_USE}%" | mail -s "Inode Alert" admin@example.com logger "Inode usage critical: ${INODE_USE}%" fi加入 crontab:
0 * * * * /usr/local/bin/check-inodes.sh
第三层:架构优化——从源头消灭小文件
这是最高阶的防护,需要开发与运维协同:
日志聚合:
放弃文件日志,改用fluentd+Elasticsearch或Loki。日志以流式方式发送,后端按时间分片存储,单个日志条目不再对应一个文件 inode。对象存储替代本地存储:
将node_modules、vendor、build artifacts等非核心数据,通过rclone同步到 S3/MinIO。本地只保留符号链接或 manifest 文件,彻底规避 inode 消耗。数据库替代文件系统:
对于需要“海量小文件”的场景(如用户上传的头像、文档),设计为将文件内容存入 PostgreSQL 的BYTEA字段或使用pg_largeobject,文件元数据存入表。一个INSERT操作只消耗 1 个 inode,而非n个。
4.3 开发者避坑指南:代码层面的 inode 友好实践
作为一线开发者,你的代码习惯直接影响服务器健康。以下是必须遵守的铁律:
禁止在
/tmp创建永久性文件:tempfile.mkstemp()创建的文件,必须用try/finally确保os.unlink();Node.js 的fs.writeFileSync('/tmp/cache.json', data)后,必须fs.unlinkSync('/tmp/cache.json')。更好的方案是使用with tempfile.NamedTemporaryFile(delete=False)(Python)或fs.promises.mkdtemp()(Node.js),让框架管理生命周期。压缩小文件集合:
Java 的maven-assembly-plugin、Python 的setuptools,都支持将依赖打包成单个 JAR/WHEEL 文件。npm pack生成 tarball 也比node_modules目录节省 99% 的 inode。Git 优化:
对于大型二进制文件,启用git-lfs;对历史仓库,用git filter-repo --strip-blobs-bigger-than 10M清理大文件。git gc --aggressive能合并松散对象,减少 inode。构建缓存共享:
CI/CD 中,ccache、sccache的缓存目录应挂载为网络共享存储(如 NFS),而非每个 runner 独立一份,避免缓存副本爆炸。
5. 常见问题与独家排错技巧实录:那些文档里找不到的真相
在上千次No space left on device故障处理中,我积累了大量“只可意会不可言传”的实战技巧。这些不是教科书知识,而是踩过坑、流过血后的真实记录。
5.1 “df -i 显示有剩余,但 mkdir 仍失败” —— root 保留 inode 的迷雾
现象:df -i显示IFree为 1234,IUse%为 99.8%,但sudo mkdir /test成功,mkdir /test却失败。
真相:Ext4 文件系统默认保留 5% 的 inode 给 root 用户(reserved blocks percentage的 inode 版本)。df -i的IFree包含这部分,但普通用户无法使用。计算公式:可用 inode(普通用户) = IFree - (Total Inodes * 0.05)
因此,当IFree < Total * 0.05时,普通用户就彻底失去创建权限。
独家技巧:sudo tune2fs -l /dev/sda1 | grep "Reserved block count"查看保留值,sudo tune2fs -m 1 /dev/sda1可将保留比例降至 1%(仅限非系统分区,谨慎操作)。
5.2 “lsof +L1 什么都没输出,但 inode 还是满了” —— XFS 文件系统的特殊性
现象:在 XFS 文件系统上,lsof +L1返回空,但df -i显示 100%。
真相:XFS 的 inode 管理机制与 Ext4 不同,lsof +L1对 XFS 的支持不完善。XFS 的“幽灵文件”更常表现为xfs_info显示ino64(64 位 inode)但df -i统计异常。
独家技巧:XFS 下,优先执行xfs_info /dev/sda1确认文件系统状态,然后用xfs_db -r -c "freesp -d" /dev/sda1检查空闲空间位图,再结合xfs_spaceman -t /dev/sda1查看详细分配。更简单的方法是xfs_info /dev/sda1 | grep "imaxp",imaxp值过低(如 < 1000)表明 inode 分配组已满。
5.3 “重启服务后,inode 使用率不降反升” —— 日志轮转的“假释放”
现象:systemctl restart nginx后,df -i的IUse%从 99.2% 变成 99.5%。
真相:logrotate 的copytruncate模式会先复制日志文件,再清空原文件。复制操作本身会创建一个新 inode,而原文件的 inode 因被进程持有并未释放,导致“一进一出”,净增 1 个 inode。
独家技巧:禁用copytruncate,改用create模式,并在postrotate中用kill -USR1通知服务 reopen 日志。这样,旧文件 inode 被释放,新文件 inode 被创建,总量不变。
5.4 “容器内 mkdir 失败,但宿主机 df -i 正常” —— cgroups 的 inode 配额
现象:Docker 容器内执行mkdir报错,但docker exec -it container df -i显示IUse%仅 20%,宿主机df -i也正常。
真相:Kubernetes 或 Docker 的--ulimit或cgroups v2可能对容器设置了pids.max或io.max,间接影响 inode 分配。更常见的是,容器使用的overlay2lowerdir 是只读的,upperdir 的 inode 配额由overlay2自身管理,df -i无法反映。
独家技巧:进入容器,执行cat /proc/1/mountinfo | grep overlay,找到upperdir路径,然后在宿主机上df -i /var/lib/docker/overlay2/xxx/diff查看真实 inode 使用。
5.5 “vscode 在 WSL2 中提示 no space left on device” —— WSL2 的虚拟磁盘膨胀
现象:Windows 上的 VS Code 连接 WSL2 Ubuntu,编辑文件时弹窗报错,但wsl -d Ubuntu df -i显示一切正常。
真相:WSL2 底层是一个ext4.vhdx虚拟磁盘文件。当大量小文件被创建(如npm install),vhdx文件会动态增长,但 Windows 的磁盘空间不足时,vhdx无法扩展,