zsh — anti@lateresonance:~ (微重力交互终端 v1.0)
按 `~` 或 `Esc` 展开/收起
🛸 欢迎接入 Late Resonance(迟到的共鸣 · 微重力前哨站) 交互式终端。输入 help 查看可用指令(支持 `ls`, `cat`, `status`, `speedtest`, `anti`, `jelly`)。
anti@lateresonance:~ %

[2026 实战手记] 为什么 df -h 显示 0% 却狂报 No space left on device?/dev/shm Inode 耗尽与多公网 IP DNAT 排障实录(Debian 13 真机版)

📅 发布于: 2026-11-04 21:40:00 · 🔄 更新于: 2026-11-04 21:40:00 · 📖 4403 字 (约 15 分钟阅读)

🛸 站长实战导读(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

架构治本建议:

  1. 绝不要把无过期机制的零碎文件直接散落在全局 /dev/shm 根目录下;
  2. 对于 systemd 托管的服务,优先在 Unit 文件中声明 RuntimeDirectory=myapp(由 systemd 在 /run/myapp 自动创建独立生命周期的内存目录,服务停止时自动销毁清空);
  3. 高频键值状态优先收敛至带 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。
🛸 安缇 (Anti Gravi) · 云端伴读舱 微重力巡航中

*(小水母 Gravi-Jelly 在半空轻轻闪烁着荧光青触角,帮你盯死每一行配置与 1C512M 内存水位 🪼✨)*

前哨站内存: -- MB | 系统已用: -- / -- MB
守望灯塔: Zero-G Anchor · 在线值守
微重力前哨站 · 内存: --MB · 负载: --