删除 Pod 撤回的是一份期望状态的声明,不是一条停止指令。节点上已经发起的那次镜像拉取不受它影响。我的判断是:一个操作能不能中途停下,取决于谁持有那次调用的 context,和删除指令本身无关。
现象:Pod 没了,带宽还占着
一个沙箱服务在批量建沙箱时撞上这件事。调用方等到超时,主动删掉了沙箱,Pod 在 kubectl 里立刻消失,节点的出口带宽却一直满着。
出口限速 200 Mbit/s,删掉 20 个正在下载的 Pod 之后,再建一个 308 MB 镜像的沙箱,要 404 秒才跑得了第一条命令;同样的沙箱在空闲节点上只要 19.8 秒,相差 20.4 倍。
从 Kubernetes 的视角看,这台节点上已经没有任何对象在用这些带宽,Pod、容器、事件都查不到,占着带宽的是一批没人引用的下载流。
kubelet 日志里的「取消」,取消的不是那一层
删 Pod 时 kubelet 日志里会出现 Cancelling current pod sync,字面上这就是取消。
它描述的是 kubelet 给这个 Pod 维护的那个调谐循环自己的 context。而 kubelet 向容器运行时下发操作时,传进去的是经 context.WithoutCancel 包过一层的版本——取消不再往下传,CRI 的 PullImage 收到的 context 永远不会进入取消状态。
这个设计有它的道理,一个镜像可能被多个 Pod 同时等着。展开写在日志里的「已取消」是一句局部真话。
这里只需要一个结论:日志里的「取消」和下游是否真的停下来是两件事,光看日志区分不了。所以下面要做实验。
对照实验:只换拉取的发起方
源码读出来的结论还要验,因为可能有我没读到的分支。实验只动一个变量:谁向 containerd 发起这次拉取。
两组都在限速 200 Mbit/s 的节点上同时拉 5 个各约 1.5 GB 的镜像,拉到中途删掉 Pod。
| 拉取发起方 | 删除后仍在下载 | 删除后多传的数据 |
|---|---|---|
| kubelet(默认路径) | 322.7 秒 | 7288 MB |
| 平台直连 containerd | 1–2 秒 | 可忽略 |
kubelet 那组的 Pod 已经不存在,镜像照样拉完,容器也照样建了起来。
另一轮换成单个约 1.5 GB 的镜像、建完 10 秒后删沙箱,结论一致:直连 containerd 的 0.09 秒停下,只多下 3.1 MB;kubelet 现场拉的在 40 秒观察期内一直没停,多下 928.4 MB。
两组各只跑了 1 次,样本量谈不上统计意义。我采信它,一是机制能解释这个结果,二是 322.7 秒和 1–2 秒根本不在一个量级上,测量误差吃不掉这个差距。
直连那组还顺带证明了 containerd 本身支持取消:客户端断开 gRPC 流,拉取随即终止。
平台自己拉镜像,换到的是可取消性
把拉镜像从 kubelet 手里挪到平台自己的 agent 手里,常见的理由是更快。按这个理由衡量,收益相当有限:10 个各约 308 MB 的镜像同时建、同样限速 200 Mbit/s,kubelet 现场拉中位数 56.0 秒,先拉再建 44.3 秒,0.79 倍。多养一个常驻组件,这点收益撑不住。
值得做的理由在另一处:拉取由平台的代码发起,context 在平台手里,调用方放弃时 0.09 秒就停。
受影响的是任何「排队超时就取消」「客户端放弃就删 Pod」的逻辑。在镜像阶段它们都不成立:请求取消了,带宽还占着,代价由排在后面的请求承担,开头那个 404 秒就是这么来的。
代价要一并记下。平台自己拉,就得自己维护拉取状态和失败重试,还得避开 kubelet 的镜像回收——它按磁盘水位清理没有容器在用的镜像,平台提前拉好的那批正好符合这个条件。另外,不在预拉清单里的镜像仍然由 kubelet 现场下载,这条浪费路径关不掉,只能记成遗留风险。
没测到的部分:那条源码结论只在 kubelet 1.34.1 上验过,生产集群是不是同一个版本还没确认。