[2026 实战手记] 为什么 df -h 显示 0% 却狂报 No space left on device?/dev/shm Inode 耗尽与多公网 IP DNAT 排障实录(Debian 13 真机版)
🛸 站长实战导读(Anti Gravi's Practice Note)
在生产一线排障中,有两类底层问题最容易让研发与运维陷入迷茫:一是明明
df -h显示磁盘还有 99% 空间,业务却疯狂报错No space left on device (ENOSPC);二是在多公网 IP 经由边界网关 DNAT 映射至单内网 IP 的混合云拓扑下,对入出站地址与内核半连接队列丢包的误判。本文基于 Proxmox VE 的 Debian 13 (
Linux 6.12.96+deb13-cloud-amd64) 真机实验,将tmpfs (/dev/shm)的 Inode 配额耗尽与在线无损扩容机制,以及 “1 个内网 IP + 3 个公网 IP DNAT 映射”拓扑下的内核协议栈排障方法论 凝练为可复用的工程标准。
一、 悬案复盘:为什么 df -h 明明还有大把空间,程序却报 No space left on device?
在生产排障中,很多研发同学喜欢把高频读写的临时锁文件、PHP Session 或分片状态直接往 /dev/shm 里塞,理由是“内存盘比 SSD 快得多”。
然而某天凌晨业务突然大面积报错 No space left on device (ENOSPC),值班运维连上机器敲了一句 df -h /dev/shm,却看到了诡异的一幕:磁盘容量明明只用了不到 1%,为什么内核死活不让创建新文件?
底层真相:tmpfs 的双重紧箍咒(size 容量上限 vs nr_inodes 节点上限)
/dev/shm 挂载的是 Linux 内核基于内存页(Page Cache / Swap)实现的 tmpfs 文件系统。在内核源码 mm/shmem.c 中,每创建一个文件不仅消耗数据块(由 size= 控制,默认物理内存的 50%),还必须在内核内存中分配一个 shmem_inode_info 结构体(由 nr_inodes= 控制)!
| 触发条件 | df -h (容量视图) |
df -i (Inode 视图) |
内核返回错误码 | 常见罪魁祸首 |
|---|---|---|---|---|
| 大文件塞满容量 | Use% = 100% |
IUse% < 5% |
ENOSPC (No space left on device) |
在 /dev/shm 存放未清理的转码视频切片、Core Dump 或大体积缓存,同时直接吃光机器 50% 物理内存诱发 OOM Killer |
| 海量零碎小文件耗尽 Inode | Use% = 0% ~ 2% |
IUse% = 100% |
ENOSPC (No space left on device) |
定时脚本或并发 Worker 在 /dev/shm 下每秒生成成百上千个 0~4B 的 .lock / sess_* 零碎文件且未设 TTL 清理 |
二、 PVE Debian 13 真机还原:“0% 容量占用却报满盘”与在线零损热扩容
我们在 Debian 13 (10.0.0.140) 真机上先查看系统默认 /dev/shm 的配额(1GB 内存机器默认分配 484M 容量与 123,895 个 Inode),随后构建一个受限的 tmpfs 现场还原这场幽灵故障:
=== [1. INSPECT DEFAULT /dev/shm TMPFS SIZE & INODE LIMITS ON DEBIAN 13] ===
Filesystem Type Size Used Avail Use% Mounted on
tmpfs tmpfs 484M 0 484M 0% /dev/shm
Filesystem Type Inodes IUsed IFree IUse% Mounted on
tmpfs tmpfs 123895 1 123894 1% /dev/shm
=== [2. REPRODUCE THE GHOST 'No space left on device' (0% Disk Used, 100% Inodes Used!)] ===
--- Creating 64 tiny 0-byte lock/session files in tmpfs... ---
--- Attempting to create one more file (sess_65.lock): ---
touch: cannot touch '/mnt/shm-lab/sess_65.lock': No space left on device
--- [Mystery Check 1] Ops runs 'df -h /mnt/shm-lab' (Shows 0% used, 64M available?!): ---
Filesystem Type Size Used Avail Use% Mounted on
tmpfs tmpfs 64M 0 64M 0% /mnt/shm-lab
--- [Mystery Solved] Ops runs 'df -i /mnt/shm-lab' (100% Inodes exhausted!): ---
Filesystem Type Inodes IUsed IFree IUse% Mounted on
tmpfs tmpfs 64 64 0 100% /mnt/shm-lab
看上面真机输出的反差:
df -Th显示:Used: 0, Avail: 64M, Use%: 0%;- 但
touch却当场报错:No space left on device; - 只有敲下
df -Ti,才会看到IUse%: 100% (IFree: 0)!
生产急救:如何在不丢失 /dev/shm 现有内存数据的前提下在线扩容?
遇到 tmpfs 容量或 Inode 告急时,千万不要 umount(一卸载内存里的数据全没了)!Linux tmpfs 支持直接通过 mount -o remount,size=...,nr_inodes=... 毫秒级在线热调整配额:
# 在线将 tmpfs 容量与 Inode 上限动态扩容,存量内存文件 100% 完好无损:
sudo mount -o remount,size=128M,nr_inodes=4096 /mnt/shm-lab
Debian 13 真机验证输出:
=== [3. ZERO-DATA-LOSS ONLINE HOT REMOUNT OF TMPFS (size + nr_inodes)] ===
--- After 'mount -o remount,size=128M,nr_inodes=4096': ---
Filesystem Type Size Used Avail Use% Mounted on
tmpfs tmpfs 128M 0 128M 0% /mnt/shm-lab
Filesystem Type Inodes IUsed IFree IUse% Mounted on
tmpfs tmpfs 4096 65 4031 2% /mnt/shm-lab
/mnt/shm-lab/sess_65.lock
架构治本建议:
- 绝不要把无过期机制的零碎文件直接散落在全局
/dev/shm根目录下;- 对于
systemd托管的服务,优先在 Unit 文件中声明RuntimeDirectory=myapp(由systemd在/run/myapp自动创建独立生命周期的内存目录,服务停止时自动销毁清空);- 高频键值状态优先收敛至带 TTL 的 Redis 或启用了
WAL模式的单文件 SQLite 中。
三、 生产网络深水区:“1 个内网 IP + 3 个公网 IP(DNAT 映射)”排障铁律
在生产服务器环境中,经常会遇到这样一种经典云端/机房混合网络拓扑:
- 服务器自身的
eth0网卡只配置了 1 个内网 IP(例如10.10.0.6); - 边界防火墙或云厂商网关将 3 个不同的公网 IP(EIP 1 / EIP 2 / EIP 3) 通过 DNAT(目的地址转换) 统一映射到了这台机器的
10.10.0.6上。
在排查这类拓扑下的奇怪网络问题时,必须牢记以下三条物理定律:
1. 为什么在机器内部 ip a 或 ss 看不到那 3 个公网 IP?
因为 DNAT 发生在流量进入 eth0 之前的边界网关上!当数据包到达服务器网卡时,IP 头部的目的地址(Dst IP)早已被改写成了内网 IP 10.10.0.6。所以在宿主机内部:
- 入向监听:Nginx / 业务进程只需绑定
0.0.0.0:443或10.10.0.6:443即可同时承接 3 个公网 IP 的入站流量; - 出向路由(
ip route get):当服务器主动向外发起连接(如ip route get <目标公网IP>)时,内核路由表永远显示via 10.10.0.1 dev eth0 src 10.10.0.6,随后由边界网关执行 SNAT 出去。
2. DNAT 高并发下的隐形杀手:内核协议栈半连接溢出与 nstat 诊断
当 3 个公网 IP 的流量同时汇聚打向单张内网网卡时,如果出现偶发连接超时而 CPU 并不高,不要只看 top,请立即在 Debian 13 下运行 ss -s 与 nstat -az 检索内核协议栈丢包计数器:
=== [4. KERNEL NETWORK STACK & DNAT DIAGNOSTICS ON DEBIAN 13 (ss -s & nstat)] ===
$ ss -s
Total: 147
TCP: 13 (estab 1, closed 0, orphaned 0, timewait 0)
$ nstat -az | grep -E "(TcpActiveOpens|TcpPassiveOpens|TcpAttemptFails|TcpEstabResets|TcpRetransSegs|TcpExtListenDrops)"
TcpActiveOpens 62 0.0
TcpPassiveOpens 75 0.0
TcpAttemptFails 0 0.0
TcpEstabResets 6 0.0
TcpRetransSegs 0 0.0
TcpExtListenDrops 0 0.0
重点盯死两个指标:
TcpExtListenDrops/TcpExtListenOverflows:若大于 0 且持续增长,说明全连接队列(Accept Queue,net.core.somaxconn与应用listen backlog取较小值)被打满,内核正在静默丢弃握完手的连接;dmesg | grep nf_conntrack:若开启了状态防火墙且出现nf_conntrack: table full, dropping packet,需调大net.netfilter.nf_conntrack_max并缩短nf_conntrack_tcp_timeout_time_wait。