ITPub博客

首页 > 云计算 > 容器服务 > Kubelet 会监视 inode。只是不到紧急关头不会。

Kubelet 会监视 inode。只是不到紧急关头不会。

容器服务 作者:ITPUB社群 时间:1970-01-01 08:33:46 0 删除 编辑

发布于 2026 年 10 月 5 日,作者 Onkar Raghunath Shelke。SRE。在四个 KubeStellar 导师项目中担任 CNCF/LFX 导师(2025 年第 2 和第 3 学期)。GSoC 2026 贡献者,就职于 Red Hat,从事 JBoss Web Server 工具开发。CKA、RHCSA、AWS SAA。印度浦那。

本文重点介绍的 CNCF 项目

一个工作节点因 NodeFilesystemFilesFillingUp 触发告警。它即将耗尽 inode。你先检查磁盘,因为这是所有人都会先检查的,结果显示已使用 83%:已用 103G,可用 22G。然后你检查 inode:已使用 67%,已用 11M,可用 5.3M。

磁盘是两者中更满的那个,而触发告警的却是 inode 表。这并不矛盾。NodeFilesystemFilesFillingUp 是一个基于 predict_linear 构建的趋势告警,因此它是在斜率上触发,而不是在水平值上触发。67% 本身没有问题。问题在于它达到这个值的速度,以及 kubelet 中唯一本可以介入的机制恰恰是驱逐 Pod 的那个机制。

关于后续内容有两点说明。我已无法访问那个集群,因此无法粘贴原始的 df、du 或 tune2fs 捕获结果。凡是来自事件记录的数字,我都会注明;凡是我通过算术重建的内容,我都会展示算术过程。后面的 ext4 演示你可以在大约两秒内自行运行。

如果你想在继续阅读前比较自己的节点:

kubelet 实际监视的内容

Kubelet 防止镜像填满节点的防御机制是镜像垃圾回收。一旦镜像文件系统超过 imageGCHighThresholdPercent(默认 85),它就会删除未使用的镜像,最旧的优先,直到使用率降至 imageGCLowThresholdPercent(80)。

这两个都是字节百分比。镜像 GC 从不查看文件数量。

不过,kubelet 并非对 inode 视而不见,这里值得精确说明,因为细节会改变结论。在 Linux 上,默认硬驱逐信号集有五个(pkg/kubelet/eviction/defaults_linux.go,此处引用标签 v1.37.0):

var DefaultEvictionHard = map[ string ] string { "memory.available" : "100Mi" , "nodefs.available" : "10%" , "nodefs.inodesFree" : "5%" , "imagefs.available" : "15%" , "imagefs.inodesFree" : "5%" , }

两个文件系统都有 inode 阈值,且都设为 5% 可用。这是一个仅限 Linux 的文件。它旁边的两个文件更短:defaults_windows.go 和 defaults_others.go 各自包含三个信号,memory.available、nodefs.available 和 imagefs.available,完全没有 inode 阈值。在 Linux 上覆盖是存在的。请对照你正在运行的版本进行检查,因为这些文件确实会变化。

所以 kubelet 会依据 inode 采取行动。问题在于何时行动。5% 空闲意味着 95% 已使用,这就是硬驱逐下限:kubelet 会回收它能回收的资源,并开始驱逐 Pod 以拯救节点的临界点。镜像垃圾回收,这个在一切尚未失控之前就在后台安静运行的机制,会在 85% 时触发,并在 80% 时停止,而这两个阈值都是按字节计算的。

数一数有多少机制在监视每种资源。字节有两个:85% 时的后台镜像 GC,以及 nodefs 上 10% 空闲或 imagefs 上 15% 空闲时的硬驱逐。inode 只有一个,而且它就是驱逐下限。inode 没有提前触发机制,所以当你 inode 不足时,第一件发生的事同时也是可能发生的最具破坏性的事。

为什么小文件会撞上一堵磁盘不会撞上的墙

在看数字之前先问个问题。用一个字节的文件填满文件系统。当你耗尽 inode 时,磁盘使用了百分之多少?猜一猜。

ext4 在创建文件系统时就决定了它会有多少 inode,而这个数字之后永远不会改变。它来自 inode_ratio:每 N 字节容量对应一个 inode。默认的 /etc/mke2fs.conf 使用 16384。而任何非空文件都至少占用一个 4 KiB 块,无论它有多小。

你可以在一个临时镜像文件上看到这个比例。不需要 root:

$ truncate -s 1G img && mkfs.ext4 -q -F -i 16384 img $ tune2fs -l img | grep -E '^(Inode count|Block count|Block size):' Inode count: 65536 Block count: 262144 Block size: 4096

262,144 个块 × 4,096 字节 ÷ 16,384 等于 65,536 个 inode。如果每个文件使用一个块,那么 256 MiB 之后全部 65,536 个 inode 就会耗尽。那是磁盘的 25%。如果你猜得更高,大多数人都会这样。

看着它失败

你不必仅凭算术就相信这一点。mkfs.ext4 -d 可以直接从目录构建文件系统,因此你可以把许多小文件打包进一个镜像,而无需挂载任何东西,也无需 root:

mkdir files for i in $( seq 1 4000) ; do printf x > files/f$i ; done truncate -s 64M img mkfs.ext4 -q -F -i 16384 -d files img dumpe2fs -h img 2 > /dev/null | grep -E ‘^(Inode count|Free inodes|Block count|Free blocks):’

Inode count: 4096 Block count: 16384 Free blocks: 11040 Free inodes: 84

已使用 97.9% 的 inode,32.6% 的块。文件本身只占这 32.6 个百分点中的 24.4 个点:4,000 个文件,每个 4 KiB 块,即 15.6 MiB,而 15.6 MiB 占 64 MiB 的 24.4%。剩余的 8.2 个百分点是日志和文件系统元数据。

再添加 200 个文件,它就不再是理论上的了:

$ for i in $(seq 4001 4200); do printf x > files/f$i; done $ truncate -s 64M img2 && mkfs.ext4 -q -F -i 16384 -d files img2 mkfs.ext4: Could not allocate inode in ext2 filesystem while populating file system

是的,它显示的是 ext2。这是底层库的错误字符串,不是笔误。那个成功的镜像还剩 84 个 inode,三分之二的块空闲。大约在第 4,085 个文件附近,inode 耗尽,从那时起空闲块就不再重要了。在 e2fsprogs 1.47.2 上,整个序列运行时间不到两秒。

逆向推导节点的 mkfs 参数

回到真实节点,这部分是算术推算而非抓取,因为我无法在那个集群上重新运行任何东西。

已使用 11M 个 inode 加上 5.3M 个空闲,总共约 1630 万个 inode。文件系统大约为 124-133 GiB,根据已使用 103G 加上可用 22G 计算,并考虑 ext4 的保留块。按照默认的每 inode 16,384 字节,该大小的文件系统只能获得 810-870 万个 inode。而这个文件系统已经使用了 1100 万个,这超过了默认比例所能分配的数量。所以它不是以 16384 格式化的。

将大小除以 inode 数量,得到大约每 inode 7,500-9,000 字节,这与 8192 相符。在该比例下,全小文件的情况会在磁盘使用 50% 时耗尽 inode,而不是 25%。我无法直接确认,但算术将其限制在约 8192,而在你自己的节点上,对根设备运行 tune2fs -l 一条命令就能确定。

真实节点从来不会全是小文件。层 blob 和日志很大,这就是为什么这里的磁盘数字更高。但镜像 GC 在等待 85%,文件数量以自己的速度攀升,而 kubelet 中没有任何东西将两者进行比较。

找到数百万个文件

知道 inode 在减少并不能告诉你它们去了哪里。这个可以:

du --inodes -xS /var | sort -rh | head -n 20

这两个标志各有其用。-x 让 du 在文件系统边界处停止,因此不会进入 kubelet 在 /var/lib/kubelet/pods 下设置的 tmpfs 和卷挂载。 -S 单独计算每个目录,而不是将其子目录中的内容累加进去。如果省略它,/var、/var/lib 以及其他所有父目录都会排在列表顶部,位于你真正要查找的目录之上。

在这个节点上,该列表顶部既不是日志,也不是 Pod 卷。它是 containerd 的快照存储,即 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/,其中每个编号目录都是一个已解包的镜像层。其中若干快照包含相同路径:app/node_modules/@mui/icons-material,每个都有 21,553 个文件。

这只是一个镜像中的一个包目录。整个镜像包含超过 40,000 个文件:仅这个图标包就占了一半以上,其余部分则是依赖树的其他内容、应用源代码和构建输出,所有这些都随同一个镜像一起发布。将 40,000 乘以节点仍保留的每个版本,数百万个 inode 就不再显得神秘了。

这是每个快照 21,553 个,而不是所有快照合计。containerd 的内容存储会按摘要对压缩层进行去重,但 overlayfs 快照器会将每个不同的层解包到自己的目录中,并且它们之间不共享任何内容。两次构建如果将完全相同的 node_modules 放入具有不同摘要的层中,就会在磁盘上留下两份完整副本。

node_modules 是众所周知的罪魁祸首,但它并不是唯一一个。任何包含大量小文件依赖树的镜像都会有同样的表现,无论是 Python virtualenv、vendored PHP 包,还是一堆 Ruby gem。

问题出在镜像,而不是集群

集群上没有任何配置错误。Kubelet、containerd 和节点镜像都在默认配置下运行。

该镜像采用单阶段构建,因此构建所需的一切最终都进入了发布产物:源代码、构建输出以及所有 node_modules,包括开发依赖,因为 npm install 默认会拉取这些依赖。像这样的单阶段构建通常看起来如下:

FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build

现在看看那个 COPY . .,并问问该仓库是否有 .dockerignore。如果没有排除 node_modules,COPY . . 会将它的一份副本放入一个每次提交都会变化的层中。这是最快增加快照数量的方式,而且很容易被忽略,因为构建仍然可以正常工作。

即使有 .dockerignore,如果 CI 流水线在没有热层缓存的情况下构建,也会每次都重新运行 npm install。即使 package.json 没有变化,这也会产生新的层摘要,因为层内的文件带有新的时间戳。每个新摘要都会在拉取它的每个节点上变成一个快照。containerd 会保留这些快照,直到镜像被移除,而 kubelet 只会在镜像 GC 运行时移除未使用的镜像,这又直接回到了那个 85% 字节阈值。

它之所以未被注意到,是因为大多数人关注的指标——磁盘使用量——从未越过那条线。

修复它

第一个修复属于构建阶段:多阶段 Dockerfile。一个阶段安装依赖并构建。最终镜像是一个小型 Web 服务器,只接收编译后的输出。

FROM node:20 AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/build /usr/share/nginx/html

这应该能将部署镜像从超过 40,000 个文件减少到编译后的资源加上基础镜像,对于 React 构建来说,通常是几十个文件而不是数万个。我还没有并排测量过这两个镜像,所以请将其视为预期形态而非基准测试。如果你尝试一下,对每个镜像运行 docker run –rm <image> find / -xdev -type f | wc -l 就能得到真实的对比结果。

第二个修复是监控,该表扬就表扬:上游告警在这里发挥了作用。NodeFilesystemFilesFillingUp 随 node_exporter mixin 一起提供,正是它在 67% 时捕获了这个问题。不过这是一个预测,当增长暂停一段时间后,预测就会沉寂。我会在它旁边加一个固定阈值,在 kubelet 的 95% 驱逐点之前就触发:

1 - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"} > 0.80

给它设置 for: 30m,这样在滚动发布期间镜像拉取的突发不会触发告警。80% 这条线是主观判断;选择任何能在 95% 之前给你足够行动空间的数值。你不会在上游找到任何与镜像垃圾回收相关的类似配置,而这正是值得记住的区别:镜像 GC 的阈值是字节的百分比,而 kubelet 确实拥有的 inode 阈值属于驱逐机制。它们是独立的机制,只有一个会提前运行。

如果你运行 Prometheus,这个指标值得放在仪表盘上。它按节点从 inode 使用率中减去磁盘使用率,因此任何接近顶部的节点都在以比字节更快的速度消耗 inode,而这些节点正是字节阈值会失效的地方:

sort_desc( (1 - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"}) - (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) )

如果你的 node_exporter 报告根文件系统的方式不同,请调整 mountpoint 标签。

这两个修复都没有处理已经存在于节点上的内容。旧快照会一直保留,直到它们的镜像被删除。crictl rmi –prune 会移除当前没有容器使用的所有镜像,这些镜像在下次有需要时会被重新拉取。你也会看到有人建议用 crictl rm –all 来做这类清理。它会清除所有已停止的容器,并连带删除最近重启过的任何容器的 kubectl logs –previous。紧急情况下可以。我不会把它放进 cron 任务里。

如果你只记住一件事:kubelet 的后台清理是基于字节运行的,因此一个文件数量多而非字节大的工作负载完全得不到早期干预。最终确实会触发一些东西,在 inode 剩余 5% 时,而它的触发方式是驱逐 Pod。那是紧急刹车,不是修复。

五分钟内检查你自己的集群

第一步。运行上面的集群查询,查看任何阳性结果。

第二步。在问题最严重的节点上,找出包含最多文件的目录:

du --inodes -xS /var/lib/containerd | sort -rh | head -n 20

第三步。如果 node_modules 出现在快照存储下,列出该节点上的镜像:

然后去看看这些镜像是如何构建的。

复现它

inode 比率。无需 root 权限。

truncate -s 1G img16 && mkfs.ext4 -q -F -i 16384 img16 truncate -s 1G img8 && mkfs.ext4 -q -F -i 8192 img8 tune2fs -l img16 | grep -E ‘^(Inode count|Block count):’ tune2fs -l img8 | grep -E ‘^(Inode count|Block count):’

你将在 262,144 个块上分别得到 65,536 和 131,072 个 inode。

在磁盘大部分空闲的情况下耗尽 inode。无需 root 权限。

mkdir files && for i in $( seq 1 4200) ; do printf x > files/f$i ; done truncate -s 64M img && mkfs.ext4 -q -F -i 16384 -d files img

这会失败并报错 Could not allocate inode。将循环降到 4,000 个文件后,它成功执行,剩余 84 个 inode 空闲,三分之二的块未被使用。

在节点上复制快照。你需要一个 registry 和一个可以部署的集群。

docker build --no-cache -t < registry > /app:v1 . docker build --no-cache -t < registry > /app:v2 . docker push < registry > /app:v1 && docker push < registry > /app:v2 # 将 v1 和 v2 部署到同一个节点上,然后在该节点上执行:sudo du --inodes -xS /var/lib/containerd | sort -rh | head -n 20

你应该会看到两个 node_modules/@mui/icons-material 目录,它们具有相同的文件数量,但位于两个不同的快照 ID 下。

最初发布于 Dev.to,2026 年 9 月 17 日,网址为 https://dev.to/onkar_s/kubelet-watches-inodes-just-not-until-its-an-emergency-351c。我是作者,并授予在 CC-BY 下重新发布的许可。请将 Dev.to 的 URL 设置为规范链接。


转载声明:本文为转载整理,原文版权归原作者及原站点所有。

原文来源:CNCF Blog

原创地址:https://www.cncf.io/blog/2026/10/05/kubelet-watches-inodes-just-not-until-its-an-emergency/

原文作者:Onkar Raghunath Shelke. SRE. CNCF/LFX mentor acros;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-11-21

  • 博文量
    1148
  • 访问量
    548717