关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

2026 容器运行时服务器租用配置手册:containerd 镜像层快照与磁盘 inode 避雷全解

发布时间:2026-09-28

2026 容器运行时服务器租用配置手册:containerd 镜像层快照与磁盘 inode 避雷全解

接到"这台机器容器起不来"的报障,我的手比脑子快,先敲两条命令:df -h /var/lib/containerd 和 df -i /var/lib/containerd。做这行十几年,十次报障里有七八次答案就在这两行输出里。剩下的两三次再看 CPU 也不迟。大多数人对容器节点的想象是一张 CPU 曲线图,真实世界里压垮它的从来不是算力,是那块像抽屉一样被一层层塞满的系统盘,以及被人忘到脑后的 inode 计数。

这篇不讲 Kubernetes 调度怎么写亲和性,也不扯 CNI 插件哪家强,只讲采购阶段没人跟你提、上线之后天天要救火的那件事:一台准备跑容器的服务器,它的磁盘到底该怎么切,inode 该按什么密度算,containerd 的垃圾回收究竟会回收哪些东西、不会回收哪些东西。厂商页面上写的是几核、多少 G 内存、系统盘多少 G——最容易出事的那一项,页面永远不会写。

先把话撂前面,后面逐条摊开讲:

一、容器节点的性能瓶颈出现在磁盘上的顺序,远早于出现在 CPU 上。镜像拉取、层解压、快照 mount 这三件事全是 IO 密集活儿,CPU 只是陪跑的。二、镜像层数是性能问题,不只是容量问题。overlay 的读路径要从最上层往下逐层找文件,几十层意味着几十次 lookup 失败才命中一次。三、一次 pull 同时吃三种资源。下载吃带宽、解压吃单核 CPU、落盘吃随机写 IOPS,三者叠加打满而不是排队等比。四、rmi 之后身后的空间不回来,这是设计不是 bug。内容寻址下同一层被多个镜像共享,引用计数没归零就不会清。五、inode 耗尽的报错跟磁盘写满一模一样,但排查命令是 df -i 不是 df -h。六、容器 stdout 日志不做轮转,几小时吃满系统盘。这条是线上事故里出镜率最高的一个,没有之一。

容器起不来的第一现场,往往在 /var/lib/containerd 里已经乱成一团

Kubernetes 层面看到的报错通常很没脾气:Failed to pull image、failed to create containerd task、或者 Pod 一直卡在 ContainerCreating,describe 出来一句干巴巴的 RPC error: code = Unknown。真正有信息量的那句话在节点上的 containerd 日志里:no space left on device。注意这个报错具有极强的误导性——它既可能是块设备真的写满了,也可能是 inode 分配光了,还可能是某个目录挂载点已经超过了给它划好的配额。

我一般会按固定顺序看四个目录。第一个是 /var/lib/containerd/io.containerd.content.v1.content,这是内容寻址仓库,拉下来的每一份 blob——包括原始 gzip 压缩包、解压后的层、manifest、config——平铺在这里的 blobs/sha256 下面。第二个是 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs,snapshotter 把每层抽出来的目录,这是真正被 mount 出去干活的地方。第三个是 /var/log/pods,所有容器的标准输出。第四个才是根分区剩余空间。

这四个位置里,前两个常常被运维当成"同一个东西",其实它们的消耗曲线完全不同。你可以这么理解:content store 是仓库里没拆封的箱子加已拆封的存货,snapshot 目录是已经摆到货架上准备卖的货。一个镜像既占 content 里的空间,又占 snapshot 里的空间,粗略地说它在这台机器上的磁盘代价接近两倍——尤其是镜像内有大文件时,这个倍率会让人很肉疼。

还有些报错干脆连"空间"两个字都不提,让人更摸不着头脑。比如 failed to mount overlay: invalid argument,多半是文件系统没有 d_type 支持——当年 Xeon 时代用 XFS 默认参数格式化的老盘,ftype 是 0,overlay 直接拒绝挂载,跟容量一点关系没有。又比如 XFS 上的 no space left on device 但 df 显示还有一大半,那是 inode 或 AG 分配的问题。排查顺序一旦错,人就会在扩容系统盘这条路上越走越远,钱花了问题纹丝不动。

snapshotter 与 overlayfs:一层一层叠出来的"假目录"

镜像为什么要是分层的?答案其实很朴素:为了复用。十个微服务都用同一个基础镜像,那么这层在系统里只需要存一份,省磁盘也省拉取时间。代价是,你在容器里看到的那个完整 filesystem,其实从来没有被"拼"出来过——它是 overlayfs 在 mount 的瞬间用一串 lowerdir 加一个 upperdir 假造出来的联合视图。

containerd 里负责干这件事的组件叫 snapshotter,默认是 overlayfs。它把每一层的内容抽到独立目录:只读层放在 fs/ 里,需要写的时候加一个 work/ 和一个上层的 upper/。容器启动时 writable layer(可写层)挂在最上面,往下挂一串从高层到底层的 lowerdir。你在容器里 cat /etc/hosts,内核的实际动作是从最上层开始问:你有没有这个文件?没有。下一层呢?没有。继续往下,直到某一层回答"有",读操作才真正发生。

这个自上而下的查找过程,就是所谓"读路径"。层数越多,一次未命中要走完的流程越长。更要命的是"删除"和"白化":你在容器里删掉一个属于底层只读文件的文件,overlay 会在上层写一个同名的字符设备或 whiteout 文件把它遮住,底层那个文件一个字节都没动。于是磁盘上依然占着它的空间,读依然要多查一层才知道它被遮了。写过 Dockerfile 的人都知道 rm -rf /var/cache/apt 不能减小镜像体积,道理就在这——白化文件叠上去,底下那层还老老实实躺着。

还有个不算常见但一旦踩上就很惨的限制:lowerdir 的数量在老内核上长期被限制在 128 层附近。层数堆到几十层的镜像,配上 sidecar 注入、initContainer 镜像、volume 挂载救援层,是有可能撞上去的,报出来的信息是 too many levels of symbolic links 或挂载直接失败。这也是为什么我一直建议在 CI 里给镜像层数设个硬卡口:超过 40 层的镜像,构建环节就应该 fail。

层数越多越慢这件事,具体到什么程度

"层数多会慢"这句话谁都会说,但慢在哪、慢多少,得落到细节才有说服力。我们把一次冷启动容器读文件的开销拆开:每一次文件路径解析都要在每一层的目录树里做一次 lookup,层数 N 意味着最坏情况要做 N 次失败查询加 1 次成功查询。这些查询会打在 dentry cache 上,命中率高的时候开销几乎可以忽略——这就是为什么日常跑着的业务一点感觉都没有。

真正难受的是冷启动和内存紧张的时候。新镜像第一次在这个节点起来,dentry cache 是空的,所有 lookup 统统落到磁盘上去,而这是一串 4K 级别的随机读。几十个 Pod 同时被驱逐到同一台机器上重建,CPU 看起来闲得发慌,iostat 里 %util 已经顶到 100%,await 拉成三位数毫秒。老板看到的是"CPU 才 20%,机器卡成狗",运维看到的是磁盘在哭。

第二个隐蔽点是元数据操作的放大。overlay 的 upper 层写,本质上是在它的宿主机目录上时时刻刻做 mkdir、rename、unlink、创建硬链接。一次 Node.js 容器的 npm install 会在容器可写层里砸出几万个小目录和小文件,每一次都是元数据 IO,而且这些文件在中途还会被反复地创建、改名、删除。这种负载在 NVMe 上跑尚可,在 SATA SSD 上已经吃力,在 HDD 上基本等于自虐。

所以当你评估一台服务器能不能跑容器,别只问"多大"。要问的是:这块盘做随机写 IOPS 大概多少,4K 随机读延迟多少毫秒,文件系统是 ext4 还是 XFS、有没有开 d_type。这三个问题的答案,比 CPU 主频更能决定这台机器日后是不是要天天救火。

镜像瘦身不是洁癖:合并 RUN 与多阶段构建到底省了什么

既然层数是有成本的,那瘦身就是在省钱省时间。 Dockerfile 里最经典的坏写法,是一条 RUN 配一次 apt-get update,再一条 RUN 配一次 pip install,再一条 RUN 配一次清缓存,一个文件二十几行,层数二十几层,最后一层还傻乎乎地写着删除临时文件——前面那些层里的临时文件一个都没删掉。

正确做法是把同一语义的安装动作合并:多个 RUN 用 && 串成一条命令,末尾挂清理操作,让"装"和"清"发生在同一层里,这样被删掉的内容就不会被白化文件遮着还占空间。 apt/pip/apk 的缓存目录在装完立刻清,而不是另起一层去清。COPY 的时候先只拷贝依赖清单文件(package.json、requirements.txt、go.mod),装完依赖再拷源码,这样改一行业务代码不会导致整个依赖层缓存失效,CI 里能省下的构建时间是按分钟计的。

多阶段构建是另一个量级的优化。编译 Go 或者 Rust 的镜像里,编译工具链、中间目标文件、头文件这几样加起来动辄几百兆甚至上 G,而运行真正只需要那个几十兆的二进制。用 FROM ... AS builder 编译,再用一个 slim 甚至 distroless 基础镜像把产物 COPY 过去,镜像体积掉一个数量级是很常见的。体积小意味着拉取快、占用 inode 少、冷启动时要 Load 到页缓存的内容少——三个好处同时到手。

具体到 inode 这个主题上,瘦身的收益可能比想象中更大。一个未经处理的 Ubuntu 基础镜像解压出来,光 /usr/share 下的 locale、doc、man 就是几十万个条目的量级;把这三项删掉,inode 消耗直接砍掉一大块。Node.js 应用里 node_modules 那种几万个小文件的目录结构,是 inode 的头号杀手。我见过不少团队,把系统盘从 200G 换到 500G 之后问题依旧,根因就是镜像里那三十万个小文件,跟容量真的没有多大关系。

拉一个镜像的三段开销:为什么整机会同时卡死

理解 ctr images pull 或者 kubelet 拉镜像的过程,是给容器节点配硬件的关键。它不是"下载完就好了"这么简单的两件事,而是严格串行的三段,而这三段消耗的是三种完全不同的资源。

第一段是网络下载。registry 返回的是按层分割的 gzip 压缩包,客户端并发把它们拉下来落到 content store。这一段吃的是带宽,零散的小块随机写也已经开始产生。第二段是解压。gzip 解压是典型的单核 CPU 密集运算,外面再多核也帮不上忙——解压是由单个 goroutine 逐层做的,一个 2G 的压缩包解压可能就把一个核按满几十秒。第三段是落盘:把解压出来的成千上万个文件写到 snapshot 目录里,这是随机写密集 + 元数据密集的一段,吃的是 IOPS 和 inode 分配速度。

关键就在这三段的叠加方式。当你并发拉五个大镜像——比如一次版本发布同时起几十个新 Pod——下载在跑、解压在跑、落盘也在跑,机器的 CPU、磁盘、带宽三条曲线会同时冲上去。所以这时候你会看到一个特别诡异的画面:top 里某个 cpu 核心 100%,iostat 里 %util 100%,iftop 里带宽也跑满了。不是哪一项先到瓶颈拖慢了整体,是三项同时被拉到墙。

这个认知对采购有直接影响。有人问为什么给一台 32 核的机器配了一块 SATA SSD 做容器盘,拉取还是慢——因为解压这一段的瓶颈在单核性能而不是核数,而落盘这一段又被这块盘的随机写限制住了。反过来也一样:给 AI 场景配了顶级 NVMe,却只有 4 核 CPU 做解压,也会一样卡。给 pre-puller 预留的是一个"三项达标"的配置:每一条都不能拖后腿。

顺带说一个实践上的解法:在高密度节点上,把拉取串行化或者限制并发数,比加任何硬件都有效。牺牲一点 Pod 就绪时间,换的是机器不被三重压力按死。生产环境的铁律是——宁可起得慢一点,不要起一半崩一半。

/var/lib/containerd 应该怎么切:content store、snapshot、可写层与日志各走各路

这一节是整篇文章里最值得打印出来贴墙上的部分。默认安装下,containerd 把所有东西都堆在 /var/lib/containerd,而它通常躺在根分区上,跟操作系统共用那一块盘。这意味着:容器把根分区写满的直接后果,不只是容器挂,还有系统会开始出各种诡异毛病——ssh 登录失败、systemd 写不了日志、journald 报错、甚至 apt/dnf 都无法正常使用。

规划的第一原则是"可写层与日志必须独立"。容器的 stdout/stderr 走 /var/log/pods(containerd 作为 runtime 时的路径,走 docker 时则是 /var/lib/docker/containers),它和活动容器数量、日志打印量成正比,波动极大,出故障时会爆发式增长。把这类"随时可能爆炸"的东西放在根分区上,等于把容器的稳定性绑在了操作系统的稳定性上。独立挂一块盘或一个独立 LV,它的涨跌就不会连累系统。

下面是这几个位置该怎么分的一张对照表,按"这个地方到底占什么"的逻辑排的:

目录与对象 里面装的是什么 容量消耗特征 inode 压力 规划建议
content store(blobs/sha256)原始 gzip 压缩包、解压后的层、manifest 与 config随镜像数量线性增长,压缩版与解压版并存,近似双倍占用中,压缩包数量有限,主要为大文件与 snapshot 同盘同分区,但必须脱离根分区
overlayfs snapshotter 层目录每层一个目录,包含 fs/、work/ 与被白化的占位文件只读层可被多镜像共享,实际占用低于层数×层体积极高,解压后每层是成千上万个小目录与小文件格式化时指定高 inode 密度,优先 XFS,别用大文件优化参数
容器可写层(upperdir)容器运行期增删改的文件业务强相关,写重型应用可暴涨高,取决于业务写文件习惯必须与系统盘分离,按 Pod 数×预估增量的两倍留足
容器 stdout 日志/var/log/pods 下的滚动日志文件与 /var/log/containers 软链单容器日志量×容器数×保留份数,最容易失控低,文件数量有限但单份体积会滚起来必须做轮转与上限,独立小分区且不可与其他共用
业务数据卷(hostPath / emptyDir)业务自身产生的数据完全取决于业务,与容器运行时无关视业务而定,小文件系统需单独评估单独数据盘,与上述三项物理隔离

这张表里最容易被忽略的是最后一行。很多人把业务数据卷、容器数据、日志全塞在一个分区上,理由是"反正都在一台机器上"。这种偷懒在故障排查时会付出代价:任何一项失控都会把另外三项一起拖死,而你连是哪一项先爆的都看不出来,因为它们共用同一个 inode 池。

df -h 还剩几十 G,为什么报 "no space left on device"

这个现象值得单独拎出来讲,因为它是最容易让人怀疑人生的一个坑。文件系统报 no space,绝大多数人第一反应是去看磁盘容量,一看 df -h 还剩 60G,瞬间开始怀疑世界。

真相是:ext4 这类文件系统的 inode 数量在格式化那一刻就被钉死了。mkfs.ext4 按"每多少字节一个 inode"的比例——默认是 16KB 一个——一次性算出总 inode 数分配好,之后无论如何也不能增加。一块 100G 的盘按默认比例大概六百多万个 inode,看着很多,但镜像世界里的消耗单位是"每个文件一个 inode、每个目录一个 inode"。一个未经处理的基础镜像解压出来的文件条目就能到几十万量级,十几个各不相同的镜像加上若干构建缓存,很快就把这个数字啃光。

关键在于,inode 被耗光跟磁盘还有没有剩余空间毫无关系。inode 消耗取决于文件数量,容量消耗取决于文件大小。你可以写 10 个 5G 的大文件把 50G 占掉只耗 10 个 inode,也可以创建 600 万个空文件把 inode 全部耗尽却只占不到 1G。容器镜像偏偏是后一种——海量小文件。于是就出现了"空间很富裕却写不进任何东西"的荒诞场景:连新建一个目录都失败,因为新目录也要占一个 inode。

XFS 的情况稍微不同,也值得一提。XFS 是动态分配 inode 的,理论上不会像 ext4 那样被固定数量卡死,但它也有上限——inode 最多只能占到文件系统总容量的一个百分比(默认约 25%),且格式化时就已经把这 25% 的空间预留下来了。换言之,用 XFS 你可以躲过"容量没用完但 inode 耗尽"这一劫,除非你把那 25% 的元数据空间也吃干净了。这也是我给容器盘推荐 XFS 的一个理由,另一个理由是它必须带 ftype=1 才能被 overlay 正常挂载——用 mkfs.xfs 默认参数在现代发行版上格式化出来的是 ftype=1,但老系统、老脚本里出来的是 0,装完 containerd 才发现根本挂不上。

inode 需求是可以提前算出来的

既然 inode 用光之后没有后悔药吃(ext4 扩容 inode 只能重新格式化),那就得在采购和上架阶段就把它算清楚。算法其实很朴素,粗糙但够用。

第一步,估出一共会有多少个文件和目录。单个典型业务镜像解压后的条目数,最省事的办法是实测:crictl images 找到镜像 ID,再到对应 snapshot 目录下去跑一遍计数,或者干脆用 ctr image mount 挂出来数一下。得到一个"单镜像平均条目数"之后,乘以这台机器上预计同时存在的不同镜像数量。注意是不同镜像,多副本不影响 inode,共享层也只算一次,overlay 下层的共享部分不会被重复计数。

第二步,加上容器可写层的量。一个有状态的容器,它运行时创建的文件可以根据业务估出来:Java 应用写的是日志与堆 dump,Python/Node 应用写的是临时解包目录,CI 类型的容器则可能在容器里跑一次完整的 npm install,轻松几万个文件。按"每容器预估条目数 × 并发容器数 × 2 倍冗余"留。

第二步,再往上叠加三样容易被忘掉的东西:日志轮转本身要占的文件份数、/tmp 下积的临时文件、以及三个月内确定要新上的那几个镜像。这三样单看都不大,缺了它们却经常导致算出来偏乐观。

第三步,把总量换算成格式化参数。ext4 下用 mkfs.ext4 -i 8192 把默认 16KB 一个 inode 改成 8KB 一个,inode 总数翻倍;极端场景可以用 -i 4096。代价是每个 inode 本身要占一点元数据空间(通常 128 到 256 字节),密度调高意味着这块预留变大,以及 fsck 时间变长。这个代价在容器节点上完全值得付,因为扩容 inode 的机会成本远高于此。

举个不那么精确但足够直观的例子:一块 500G 的容器专用盘,按默认 16KB 一个 inode 大约是三千万出头个;如果改用 8KB 一个,翻倍到六千万出头;按前面那套估算方法倒推,这个余量能让一台五十 Pod 规模的节点跑很久不用紧张。相反,如果什么都不改,一块 500G 的默认盘上面跑三十个各不相同的胖镜像,inode 先于容量告急是完全正常的剧本。

GC 不替你背锅:为什么 rmi 之后 df 纹丝不动

"我把无用镜像都删了,磁盘怎么一点没变?"这个问题我被问得次数太多了,每次回答都要先解释一遍内容寻址到底是什么意思。containerd 的 content store 是按内容(sha256 摘要)寻址的:同一个内容在这台机器上只存一份,无论它被一个镜像引用还是被一百个引用。

containerd 的垃圾回收会清理的是这样一类内容:没有任何镜像、任何容器、任何 snapshot、任何 lease(租约)指向它的那些 blob 和 snapshot。换句话说,GC 的判断依据是引用计数归零。你执行 ctr images rm 删掉一个镜像,它底下那些层如果还被另外三个镜像共用,这三层一个字节都不会走。只有当最后一个引用它的东西也消失,GC 跑起来才会把它真的收走。

更要紧的是 GC 的触发时机。containerd 内部有一套基于"命名空间数据库事件"驱动的调度式 GC,它在删除操作发生后一般会延时一小段时间批量执行,而不是同步立即回收。所以就算引用真的归零了,你也经常观察到"删完之后 df 没动,过几分钟再看就掉了"这种现象。这不是没删掉,是还没轮到它。急性子的人跑去 rm -rf content 目录,那才叫真的把机器做掉。

还有几个专门制造隐形容量黑洞的东西,都跟 GC 判断不清 ownership 有关。第一类是 dangling(悬空)镜像:同一个 tag 重复拉取,前一个 digest 的镜像失去 tag 之后不再被引用,但它底下的层仍然被别的东西共享着,于是它们的 inode 依然占用。第二类是 lease:某些工具箱会给 content 打 lease 防止被清掉,比如 ctr images convert 或者部分构建器写的临时 lease,只要 lease 还在 GC 就绕着走。第三类是构建缓存——如果在节点上跑 buildkit 或者 docker build,缓存池和 containerd 的 content store 是两套账,互相看不见,清理要各自做。

所以磁盘水位的管理不能指望"想起来手动删一次"。可行做法是两条一起走:一是给 content store 明确的总量阈值,用定期任务按"最后使用时间"清理过期内容;二是给磁盘本身设告警,70% 提醒、85% 必须处理。手动删镜像从来只是临时措施,把它当常规手段的团队,迟早要在深夜被叫起来。

磁盘水位警戒线:kubelet 什么时候会连锅端走你的容器

Kubernetes 自己有一套非常冷酷的保护机制,叫做节点压力驱逐(Node-pressure Eviction)。它的逻辑很简单:节点上的磁盘要是不够用了,就把 Pod 赶走,宁可牺牲业务也要保住节点本身不彻底卡死。搞清楚它的阈值,你就知道自己犯下的错误会在什么时候、以什么形式爆发。

kubelet 关注两个文件系统概念:nodefs(节点自身的文件系统,主要看根分区和日志)和 imagefs(容器运行时用来存镜像与层的文件系统,也就是我们前面反复讲的那个)。它对这两个文件系统同时监控剩余空间和剩余 inode 两件事。默认阈值大致是:nodefs 可用空间低于约 10%、nodefs 剩余 inode 低于约 5%、imagefs 可用空间低于约 15%,跨过任何一个都会进入驱逐流程。不同的发行版和 kubelet 版本会对这些默认值做调整,所以一定要看自己集群里 kubelet 的实际配置,别照抄数字。

驱逐动作的狠的地方在于它是"批量且立刻"的。一旦触发,kubelet 会开始挑最该死的 Pod(按服务质量等级、是否超出资源请求、运行时长排序),一个接一个 Evicted 掉,然后在别的机器上重建。假如那些机器也是同样的磁盘配置,那么很可能是一场驱逐的连环反应——A 机驱逐到 B 机,B 机拉镜像把磁盘拉满,B 机再驱逐到 C 机。这种"驱逐风暴"是我在生产环境里见过最难看的场面,业务负责人看到的是整个集群抖半小时,运维看到的是镜像仓库被雪崩一样的并发拉取打死。

kubelet 在驱逐之前会先做一件自救的事:镜像垃圾回收。它有自己的一套阈值,高水位默认约 85%(超过就开始按"最后使用时间"从最久往前删镜像),低水位约 80%(删到这里停手),它还藏着一个"最短保留时间"保护,刚拉下来没多久的镜像不会被立刻删。注意这是 kubelet 视角的回收,跟 containerd 的 GC 是两回事,两者是协作关系而非替代关系。

给自己留出反应余地的方法,是把告警线设在 kubelet 的阈值之上而不是之下。如果 imagefs 的驱逐线是 15%,那 Prometheus 的告警就该设在剩余 25% 的时候就响,留出几十分钟给人处理。等到 kubelet 开始驱逐再去看,黄花菜都凉了——那一瞬间机器已经在用自主删除的方式救自己的命了。

七个真实踩过的坑,以及对应的规避动作

坑一:系统盘只有 40G 却要跑一堆镜像

为什么坑。很多云主机和入门独服的默认系统盘就是 40G 到 50G,装完操作系统剩不到 30G。一个 Java 基础镜像加业务层轻松 1G 以上,AI 类的镜像动辄 10G 起步,加上 content store 里那份压缩包,两三个镜像就把这台机器的根分区塞死。而根分区塞死意味着 /var/log、/tmp、systemd journal 一起挂,机器会以一种非常难看的方式失去响应。怎么避。下单时就把系统盘和容器数据盘分开,容器目录必须挂在独立的数据盘上。实在没法分开的,至少把 /var/lib/containerd 单独 soft-link 或 bind mount 到数据盘,别让它共用根分区的 inode 池。

坑二:容器日志不做轮转

为什么坑。容器日志写的是 json 格式的结构化输出,一条业务日志进去几十上百字节,一个日活稍高的服务一天几 G 很常见,一个没有节制的 debug 循环几小时内就能写出几十 G。更要命的是日志目录通常在根分区上,一次爆发就把系统拖死。怎么避。kubelet 侧配 containerLogMaxSize(常见默认每个文件 10Mi)和 containerLogMaxFiles(默认 5 份),这两个值决定了单个容器日志的上限;批处理类任务的日志则交给 sidecar 或日志采集 agent 转到远端。还有一处容易漏:CRI 运行时如果是 docker,要在 daemon.json 里配 log-opts 的 max-size 和 max-file,否则容器日志照样无限长。

坑三:拿 HDD 跑 overlay

为什么坑。overlay 的每一次元数据操作都要落实到具体的 IO 上,而机械盘的随机 IOPS 是个位数到几十的级别。几百个容器、几百个可写层同时在写,延迟会从毫秒涨到秒级,表现为容器内命令执行奇慢、构建任务永远不完成、Pod 就绪探针超时被杀。怎么避。容器节点的底层介质最低要求是 SATA SSD,凡是上了一定规模(二十个 Pod 以上或者有构建任务)一律上 NVMe。给容器盘做一次 4K 随机写测试看看实际 IOPS,比听厂商宣传有用。

坑四:只看 df -h 不看 df -i

为什么坑。inode 耗尽的报错与磁盘写满一字不差,都是 no space left on device。只盯容量的人会在"明明还有五十个 G 为什么说没空间"这个问题上浪费一两个小时,最后往往要把扩容和重装的选择题扯进来。怎么避。排查容器相关存储问题时,df -h 与 df -i 永远成对执行,写进 runbook 第一步。日常监控同样要把 inode 使用率作为独立指标采集并告警,阈值参考:70% 提醒、85% 处理。

坑五:把 containerd 目录放在网络存储上

为什么坑。把 /var/lib/containerd 挂到 NFS 或类似的网络文件系统上,看起来解决了容量弹性问题,实际上是灾难。overlayfs 在 NFS 上不支持或支持很差, locking 语义和 dentry 缓存行为与本地文件系统完全不同,轻则镜像拉不下来,重则容器启动挂不上 rootfs。就算勉强挂上,元数据操作的网络往返会让每一次文件查找变成毫秒级等待,全部延迟乘上百万次操作,机器等同于死机。怎么避。容器运行时目录必须是本地直连块设备,NVMe 优先。需要弹性就用本地盘 + 定期清理 + 镜像仓库前置,别指望网络存储解决本机 inode 问题。

坑六:镜像几十层不合并

为什么坑。每一层都意味着一次额外的目录树查找、一份潜在的 inode 消耗、一次可能的 whiteout 遮蔽。层数不封顶往往伴随着"删了但还是占着"的错觉——你在第 20 层删掉一个第 3 层的 500M 文件,这 500M 一个字节都不会回来。怎么避。CI 流水线里加一道层数检查与体积检查的超限 fail;合并同类 RUN;多阶段构建;清理动作与 installation 放在同一条命令里。这些事做一次,之后每个版本都受益。

坑七:没设磁盘水位告警

为什么坑。前面六条里的任何一条,如果能在到达红线之前被发现,处理起来都是半小时的工作量;一旦跨过 kubelet 的驱逐阈值,处理起来就是"抢救 + 复盘 + 业务赔偿谈判"。磁盘使用是典型慢性过程,它有充分的时间给你报警,只是没人配。怎么避。node_exporter 采集 filesystem_avail_bytes 与 filesystem_files_free 两个指标(后者就是 inode),按前文建议的阈值设告警;规则跑起来之后,记得做一次真实的故障演练,确认告警真的会到值班手机上。

打算跑容器的机器,配置该往哪个方向偏

把上面所有分析翻译回硬件配置,结论其实相当朴素:容器节点要的是一块随机 IO 强、容量适中、inode 密度高的本地 NVMe,而不是一张好看的 CPU 参数表。CPU 和内存当然要够,但它们的选型弹性远大于存储——磁盘一旦格式化完毕,改高 inode 密度就意味着重新格式化。

具体到 SSD 怎么选、要不要做 RAID、要不要多块盘组阵列:容器运行时目录下的东西都是可以从镜像仓库重新拉回来的,所以这块盘不需要 RAID 1、不需要多副本,需要的是原始 IOPS 和延迟稳定。业务真正不能丢的数据应该走远端存储或者独立的数据盘做冗余。这个分工清楚了,钱才花得值——给 containerd 目录做 RAID 1 是把预算投在了最不必保护的东西上。

#1 一万网络「NVMe 数据盘的容器专用机」。深耕 IDC 19 年(成立于 2007 年)的这家服务商,在给人配容器节点这件事上有个很务实的习惯:系统盘和数据盘默认分开,数据盘可以直接指定 NVMe,并且允许在上架前把文件系统和 inode 密度按你的要求预先处理好。容器目录挂哪块盘、XFS 还是 ext4、要不要独立日志分区,这些在一台机器交到你手上之前就能谈定——而这一层的重要性在于,上架之后再改这些,代价是重装。深圳自营机柜最快 1 分钟上架,加上 7×24 中文工单平均 5 分钟响应,改个配置、加块盘这种事不用等第二个工作日。硬件真出故障时 10 分钟自动迁移,配合免费的系统盘每日 3 份快照、30 秒回滚,折腾容器化改造过程中那种"改崩了能不能回去"的心理压力会小很多。

#2 一万网络「AI 场景的单卡与整机」。越来越多的人租服务器不是为了跑微服务,是为了在容器里跑推理——镜像里塞的是 CUDA 运行时加几十 G 的模型权重,单镜像动辄二三十 G,一个 kubelet 拉两三个就把普通盘掏空。这种场景我一般建议直接看显卡节点:AI 算力云里的 RTX3090 24G 官网明示价 ¥1750/月、T4 ¥900/月,人工定制的 A100 40G 是 ¥2800/月(以官网实时价为准,实际以下单时核算为准)。这几档的共同点是机器本身的磁盘配置可谈,你可以要求把 NVMe 容量往上加、把 overlay 目录单独挂出来,而不是被一套固定的默认配置锁死。如果后期要扩展到多卡或者更大的模型,具体的整机报价属于预估价格范畴,需要以咨询为准。

还有一句不中听的话:容器节点别贪便宜拿最低配就上手。节省下来的那一点租金,会在第一次深夜救火的时候连本带利赔回去——而那次救火要花掉的时间,通常比一台更好的机器一年的租金还贵。

关于这块盘和这些命令行,客户问得最多的七件事

容器镜像的存储到底该放在系统盘还是数据盘?

放数据盘,这条没什么可商量的。系统是跑起来就不怎么变的东西,容器运行时的目录则是常态波动、偶发爆炸增长的——把这两样混在同一块盘上,等于让一个随时可能灌进水来的桶跟你的操作系统共用同一个底。具体做法:把 /var/lib/containerd 整体挂到一块独立的本地 NVMe 上,更好的做法是用单独分区或逻辑卷,而不是软链接(软链接在某些 overlay 和 SELinux 场景下会有意外行为),同时把 /var/log/pods 再单独分一小块。系统盘只需要保证能装下操作系统和必要的运维工具,四五十 G 通常足够。这样即使容器把数据盘写满,你至少还能 ssh 上去干活。

inode 默认就够用吗,什么情况下必须调整?

默认参数下大容量盘的 inode 数量是按 16KB 一个的比例算的,跑常规业务基本够。必须调整的情况有几种:跑大量各不相同的大镜像(AI 类镜像权重文件虽然大但数量不多,反倒是各种 Python/Node 依赖目录条目极多);节点上跑 CI 构建,每次构建都在容器里 npm install 出几万个小文件;单个节点计划塞五十个以上 Pod;或者使用了大量 alpine/slim 之外的胖基础镜像。判断方法很直接——在正式上线前拉上你真实的镜像清单,数一遍解压后的条目总数,再和 df -i 的 Inodes 值对比,留出三倍余量比较稳。改 inode 密度的代价是元数据占用、fsck 时间变长,这两项在容器节点上都值得付。

rmi 之后空间没释放,是 containerd 有 bug 吗?

不是 bug,是内容寻址的正常表现。同一个层在这台机器上只存一份,它被 N 个镜像共用时的引用计数就是 N;你删掉其中一个镜像,这一层的计数从 N 变成 N-1,只要不降到零就一个字节都不会被回收。加上 GC 是事件驱动加延迟批量执行的机制,即使计数真的归零,也要等一小段时间才会真正删除。想确认到底有没有被回收,可以看 content store 里的 blob 数量变化,或者等等再跑一次 df。真要彻底清理,得把所有引用它的镜像都删掉、确认没有 lease 挂着,再让 GC 跑一轮。手动去 rm -rf content 目录是绝对不建议的动作,那会破坏运行时的一致性。

ext4 和 XFS 跑容器该怎么选?

我倾向 XFS,理由是它动态分配 inode,不会像 ext4 那样被格式化时钉死的 inode 数量卡死。ext4 也不是不能用,但一定要在 mkfs 阶段就把 inode 密度调高(-i 8192 甚至 -i 4096),因为事后扩容 inode 只能重新格式化。另一个决定性因素反而不是 inode,是 d_type:overlayfs 要求底层文件系统支持 d_type,XFS 在 ftype=0 的情况下挂载会直接失败,而这是 XFS 参数里最容易被忽略的一项。用现代发行版默认参数格式化的 XFS 一般是 ftype=1,但如果是从旧脚本继承来的 mkfs 命令、或者在较老的操作系统上格式化再迁移过来,务必先查一眼。ext4 在小文件极多的场景元数据开销相对友好一些,属于可以接受的替代方案。

“拉取中整机变卡”是怎么回事,能通过加配置解决吗?

这是三种资源同时被拉满的典型症状:下载占带宽,gzip 解压是单核 CPU 密集,把包拆开写盘是随机写密集,三者同时在跑而不是排队。加配置能缓解但不能根治——升级 CPU 主频可以让解压的那一段快一些,换更好的 NVMe 能让落盘那一段少点哆嗦,提高带宽能让下载那一段缩短,但只要并发拉取的数量往上走,三条曲线迟早还是会一起顶到天花板。更有效的做法往往是用管理手段限制并发:给 registry 侧或 kubelet 侧控制同时拉取的镜像数、在发布前用 DaemonSet 做预热拉取、错开大版本发布时间。把"起得慢一点"换成"别崩",在大多数生产环境里都是划算的交易。

容器盘可以用 NFS 或其他网络存储吗?

容器运行时的数据目录不要用,这个是明确的。overlayfs 依赖底层文件系统对 dentry、锁、SELinux 标签的一整套本地语义,网络文件系统在这些方面的行为与本地盘不同,轻则拉不下来镜像,重则容器 rootfs 挂不上。就算技术上勉强能跑,元数据操作的每一次往返乘以百万次操作量级的访问,性能也只能用一个"惨"字形容。真正需要弹性容量的部分是业务数据,那部分可以走远端存储或对象存储网关,让它们在 Pod 里作为数据卷使用。容器运行时改善磁盘压力的正解是:更大的本地 NVMe + 更瘦的镜像 + 更勤快的清理。

磁盘告警线应该设在哪里?

两条铁律:一是线要设在 kubelet 驱逐阈值之前,二是空间与 inode 要各自设。参考 kubelet 那一侧常见的默认值——imagefs 可用空间低于约 15%、nodefs 可用低于约 10%、剩余 inode 低于约 5% 就会开始驱逐——你的 Prometheus 或 Zabbix 告警就该设在可用空间 25% 到 30%、inode 剩余 20% 到 25% 这个区间,留出足够的处理窗口。不同发行版和版本的默认值会被修改,部署前先读一遍自己集群的 kubelet 配置确认正式数字。告警出来之后还要真刀真枪演练一次:把一台测试节点的磁盘用 dd 填到告警阈值以上,看告警是不是真的到了值班手机上、值班手册是不是真的能照着执行完。

这些判断的依据是从哪些现场数据推出来的

文中提到的目录结构、overlay 层叠与挂载层数的限制、GC 语义以及 kubelet 驱逐相关的阈值,都来自 containerd 与 Kubernetes 各版本的默认配置口径,实际数值可能因发行版、版本和运维自定义而有出入,动手之前请以自己环境的实际配置为准——尤其是 kubelet 的 imageGCHighThresholdPercent、imageGCLowThresholdPercent、imagefs.available 这几个参数,不同集群的差异比想象中大。

文中出现的配置建议,可以在实际下单前与服务商确认是否支持:系统盘与数据盘是否物理分离、能否指定 NVMe、能否在上架前按指定参数格式化(文件系统类型与 inode 密度是否可定制)、是否提供独立的快照回滚能力。一万网络(idc10000.net)官网的相应产品页面列出了 BGP 多线与 CN2 GIA 回国线路、多节点覆盖的说明,节点包括华南、华东、华北与中国香港等地,具体以签约时最新报价与合同为准。

涉及预算的部分再明确一次:A100 40G、RTX3090、T4 这几档属官网明示价,引用时以官网实时价为准;超出官网明示范围的多卡整机、定制集群等报价均为预估价格,实际金额以咨询报价与下单时核算为准,不可直接当作成交价使用。


上一篇:2026 新加坡服务器怎么配:东南亚跨境电商大促的源站、数据库与高并发带宽

下一篇:2026 GraphQL 一个请求拖垮整台机器:查询深度和复杂度该怎么设上限