一次由inode耗尽引发的Docker&Gogs故障排查实录

背景

我的一台 Linux 云主机上,Docker 开始频繁报错:
no space left on device

但用 df -h 查看磁盘空间,发现磁盘容量还有大量剩余。 乍一看非常反直觉,但这恰恰是 Linux 系统中一个经典且隐蔽的问题。

一、初步现象:磁盘有空间,Docker 却“满了”

最初的检查:

df -h

结果显示磁盘容量充足,但 Docker:
容器启动失败
写文件失败
构建失败
这说明问题并不在 block space(磁盘容量)。

二、关键一步:检查 inode

随后检查 inode 使用情况:

df -i

结果非常关键:

Filesystem      Inodes   IUsed   IFree IUse% Mounted on
/dev/vda2      3932160 3929865    2295  100% /

根分区 inode 100% 用尽。
结论:
系统已经无法创建任何新文件,即使磁盘还有空间。

Docker、Git、日志、临时文件,本质上都依赖 inode。

三、避免 find,全用 du –inodes 定位

在 inode 即将耗尽的系统上:
不要 find /
不要全盘扫描

正确方式是逐层缩小范围:

du --inodes -d 1 / | sort -nr | head

结果非常震撼:

3648582 /opt
149766  /var

/opt 一个目录,占用了 92% 以上的 inode

四、锁定元凶:/opt 下的 Gogs

继续下钻:

du --inodes -d 1 /opt | sort -nr | head

发现其中有一个:

/opt/docker/gogs

服务器上运行着一个 Docker 部署的 Gogs(Git 服务)。

五、Gogs 为什么会吃掉几百万 inode?

深入查看后发现:
Gogs 的 Git 仓库位于宿主机 /opt/docker/gogs/data/git
每个仓库除了主仓库,还会自动创建一个:
**.wiki.git**

关键认知: Gogs 的 Wiki 本质上是一个“独立的 Git 仓库”
而 Git 的存储特点是:
每个 object = 一个小文件
每个小文件 = 一个 inode
Wiki 编辑频繁,但几乎从不 GC
loose objects 会无限增长

六、为什么 GC 会失败?

尝试执行:

git gc --aggressive --prune=now

却直接失败:

unable to create packed-refs.new: No space left on device

原因很简单:
Git GC 本身也需要创建新文件, 而 inode 已经 100% 用尽
这是一个典型的“死锁场景”:

想 GC → 需要 inode
inode 已满 → GC 无法执行

七、关键突破:删除 *.wiki.git

在确认 Wiki 内容完全不重要后,果断执行:

rm -rf /opt/docker/gogs/data/git/gogs-repositories/*/*.wiki.git

效果立竿见影:
inode 使用率立刻下降
系统恢复可写
Docker / Git 恢复正常
Wiki 仓库正是 inode 爆炸的主因之一

八、问题解决后的收尾工作

再次执行 Git GC(此时已能成功)
在 Gogs 中禁用 Wiki 功能:
DISABLE_WIKI = true

规划长期方案:
Git 仓库迁移到独立磁盘
定期 git gc
inode 监控与告警

九、总结

这次问题的本质并不是:
Docker 的问题
Linux 的问题
磁盘容量的问题
而是:
inode 被 Git(尤其是 Wiki 仓库)长期、无感知地耗尽

经验教训
df -h 看不到一切,df -i 同样重要 Git ≠ 数据库,更不适合存放大量小变更 Wiki 是隐藏的 inode 杀手 Docker 只是“第一个报警的人”

结语

这是一次非常典型且极具价值的排障案例。 如果你的服务器上同时存在:
Docker
Git 服务(Gogs / Gitea / GitLab)
根分区运行多年
强烈建议你现在就检查一次 inode