Kubernetes CNI 接口与 Pod 网络生命周期
Pod 已经被调度到节点,不代表容器可以立刻启动。kubelet 还要通过 CRI 请求容器运行时创建 Pod sandbox;运行时为 sandbox 准备网络命名空间,再执行 CNI 插件。只有这一步成功,Pod 才有可供 init container 和业务容器共享的网络环境。
排查网络问题时,经常会看到 CNI ADD failed、failed to destroy network 或 NetworkPluginNotReady。这些信息对应的不是同一个阶段:有的在创建单个 Pod 的网络,有的在清理 Pod,有的只检查插件能否继续接收新请求。
本文以 CNI 1.1.0 规范为基准,说明 ADD、DEL、CHECK、STATUS、GC 和 VERSION 分别处理什么,以及它们怎样进入 Kubernetes 的 Pod 生命周期。
这里需要先区分两套独立的版本号:1.1.0 是运行时与插件之间的 CNI 协议规范版本;v1.3.1 是 containernetworking/cni 代码库的 软件发布版本,对应 libcni、pkg/skel 等 Go 包。代码库发布到 v1.3.1,不代表协议规范也变成了 1.3.1。截至本文编写时,官方 SPEC.md 标注的规范版本仍是 1.1.0,并明确说明它与代码库及插件的发布版本相互独立。
先区分 kubelet、容器运行时和 CNI 插件
CNI 规定的是「运行时如何执行网络插件」,不是 kubelet 直接调用某个 CNI Daemon 的 API。这个调用链可以简化为:
| |
CNI 插件通常是一个可执行文件。运行时通过环境变量传入 CNI_COMMAND、容器 ID、网络命名空间和接口名,通过标准输入传入 JSON 配置;插件通过退出码和标准输出返回结果。它不是 CSI 或 Device Plugin 那种先向 kubelet 注册、再通过长期运行的 gRPC 服务接收请求的接口。
从 Kubernetes 1.24 开始,kubelet 不再负责 cni-bin-dir、network-plugin 等 CNI 管理参数,CNI 配置和插件加载属于容器运行时的职责。Kubernetes 当前要求插件至少兼容 CNI v0.4.0,并推荐兼容 v1.0.0,详见 Kubernetes Network Plugins 文档。这也意味着:规范里存在某个操作,不等于集群当前的运行时和插件一定会调用、实现它。
六个操作分别在哪个阶段生效
| 操作 | 作用对象 | 常见触发阶段 | 主要作用 |
|---|---|---|---|
VERSION | 插件二进制 | 加载配置、选择协议版本时 | 查询插件支持的 CNI 规范版本,不修改 Pod 网络 |
STATUS | 整个插件或网络配置 | 运行时初始化、CRI 状态检查或后台健康检查时 | 判断插件是否还能处理新的 ADD 请求 |
ADD | 一个网络 attachment | RunPodSandbox 创建网络时 | 把 sandbox 加入网络,创建或调整接口、IP、路由等 |
CHECK | 已存在的 attachment | ADD 成功后到 DEL 前,由运行时按需触发 | 对照上次 ADD 结果检查网络状态是否仍符合预期 |
DEL | 一个网络 attachment | StopPodSandbox、删除 sandbox,或 ADD 失败后的清理阶段 | 撤销 ADD 创建的网络资源 |
GC | 一个网络配置下的全部 attachment | 运行时维护、重启恢复或周期性垃圾回收时 | 根据有效 attachment 集合清理遗漏的陈旧资源 |
这里的 attachment 不是 Kubernetes API 对象。CNI 用 (CNI_CONTAINERID, CNI_IFNAME) 唯一标识一次网络连接。一个 Pod sandbox 的默认网卡通常形成一个 attachment;附加网络也可能形成其他 attachment。
sequenceDiagram
participant K as kubelet
participant R as CRI runtime
participant P as CNI plugin chain
K->>R: RunPodSandbox
R->>R: 创建 sandbox 和网络命名空间
R->>P: VERSION(按需协商)
R->>P: ADD(正序执行插件链)
P-->>R: 接口、IP、路由和 DNS 结果
R-->>K: sandbox 网络配置完成
Note over K,R: 随后才创建 init container 和业务容器
opt attachment 存续期间,运行时支持并主动触发
R->>P: CHECK
P-->>R: 当前状态是否符合 ADD 结果
end
opt 运行时健康检查
R->>P: STATUS
P-->>R: 是否可继续处理 ADD
end
opt 运行时维护或恢复
R->>P: GC(携带仍有效的 attachment)
P-->>R: 清理陈旧资源
end
K->>R: StopPodSandbox
R->>P: DEL(逆序执行插件链)
R->>R: 删除网络命名空间
这张图表达的是规范能力和常见生命周期,不是要求每个运行时在每个 Pod 上依次调用六个操作。对单个 Pod 来说,稳定存在的主路径是创建时的 ADD 和销毁时的 DEL;其他操作是否出现,要看 CNI 版本、容器运行时实现和配置。
ADD:Pod sandbox 创建网络时生效
ADD 是最常见的 CNI 操作。容器运行时先创建网络命名空间,再为目标接口执行 CNI 插件。插件可以:
- 在 sandbox 中创建接口,例如 veth 的容器侧;
- 调整已经存在的接口;
- 分配 IP,写入路由和 DNS 结果;
- 在主机或外部系统中建立转发、隧道、端口映射等状态。
调用时的关键输入包括:
| 输入 | 含义 |
|---|---|
CNI_COMMAND=ADD | 本次执行的操作 |
CNI_CONTAINERID | 运行时分配的 sandbox 或容器标识 |
CNI_NETNS | 目标网络命名空间路径 |
CNI_IFNAME | 要在目标命名空间中创建或修改的接口名,通常是 eth0 |
| stdin JSON | cniVersion、网络名、插件配置和运行时能力参数 |
成功的 ADD 会返回 CNI Result,其中可以包含接口、IP 地址、路由和 DNS。运行时必须持久保存插件链的最终结果,因为后续 CHECK 和 DEL 需要使用它。
在 Kubernetes 中,这一步处于 Pod sandbox 建立阶段。Kubernetes 的 PodReadyToStartContainers 条件会反映 sandbox 和网络是否已经准备完成。网络配置成功后,kubelet 才继续拉取镜像、创建 init container 和业务容器。因此:
ADD失败通常会让 Pod 停留在ContainerCreating,并产生FailedCreatePodSandBox事件;- 重启 Pod 内的单个业务容器通常不会再次调用
ADD,因为它仍使用原来的 Pod sandbox 网络命名空间; - sandbox 被删除并重建时,才会重新建立网络并调用新的
ADD; hostNetwork: true的 Pod 使用宿主机网络命名空间,通常不经过默认 Pod 网络的 CNIADD。
containerd 的 CRI 架构说明也把这段顺序写成:CRI 创建 Pod 网络命名空间,用 CNI 配置网络,然后再创建和启动 sandbox 及业务容器。不同运行时的内部对象顺序可以不同,但 CRI 返回成功前必须完成 sandbox 所需的网络配置。
DEL:sandbox 清理时生效
DEL 撤销对应 ADD 创建或修改的状态,例如:
- 释放 IPAM 分配;
- 删除主机侧接口;
- 撤销端口映射、防火墙或流量控制规则;
- 删除外部网络系统中的 endpoint。
正常路径上,kubelet 请求运行时停止 Pod sandbox,运行时先执行网络清理,再删除网络命名空间。本文检查的 containerd 提交可以在 StopPodSandbox路径中看到这段调用。
DEL 有三个容易忽略的约束:
- 即使
ADD返回失败,运行时也必须继续尝试对应的删除操作,因为前面的插件可能已经分配了 IP 或创建了主机资源。 CNI_NETNS对DEL是可选参数。namespace 可能已经消失,插件仍应尽可能释放 IPAM、主机规则和外部资源。DEL必须可以重复执行。目标资源已经不存在时,插件应把它当成清理完成,而不是制造一个无法收敛的错误。
所以 DEL 更接近「幂等、尽力而为的回收接口」,不是对 namespace 内接口做一次简单的 ip link del。
CHECK:attachment 存续期间的状态校验
CHECK 从 CNI v0.4.0 引入,用于检查一次已经成功建立的 attachment。运行时要把上次 ADD 的最终 Result 作为 prevResult 传给插件,插件据此检查自己负责的状态,例如:
- 预期接口、IP 和路由是否仍存在;
- 防火墙、流量控制和 IP 预留是否仍有效;
- 插件依赖的外部 Daemon 是否可用;
- 是否已经知道该 sandbox 整体不可达。
它只允许在 ADD 成功之后、DEL 之前调用,网络配置中的 disableCheck: true 可以显式禁止检查。
CHECK 不是 Kubernetes 的 Pod readinessProbe,也不是规范化的端到端连通性探测。它检查的是插件所知道的配置状态;业务进程能否访问某个 Service、DNS 是否返回预期结果,仍需其他探针和观测手段。
还要注意「支持接口」和「实际调用」的差别。本文检查的 containerd 提交依赖 go-cni v1.1.14;这个库暴露了 Check 方法,但 containerd CRI 的常规 sandbox 创建、状态查询和停止路径没有自动调用它。因此不能看到 CNI 版本大于 v0.4.0,就假设 kubelet 会周期性地替每个 Pod 执行 CHECK。
STATUS:检查插件能否继续服务 ADD
STATUS 是 CNI v1.1.0 的插件级健康检查。它不携带某个 Pod 的 CNI_CONTAINERID、CNI_NETNS 或 CNI_IFNAME,检查的是插件是否已经准备好处理新的 ADD。
如果插件依赖外部服务,或者 IP、硬件队列等有限资源已经耗尽,可以通过 STATUS 返回错误。规范定义了两个相关错误码:
50:插件不可用,不能处理新的ADD;51:插件不可用,并且现有容器的网络也可能受限。
但 STATUS 是信息性的。插件不能依赖运行时一定先调用它;即使它返回失败,规范也不保证运行时会阻止后续所有 ADD 或 DEL。
在 Kubernetes 这一层,kubelet 通过 CRI Status 获取 NetworkReady。容器运行时怎样得出这个状态属于运行时实现。在本文检查的 containerd 提交中,CRI Status 会调用 go-cni.Status();后者对 CNI v1.1.0 及以上的配置执行插件链 STATUS,较老版本则跳过。因此 NetworkPluginNotReady 可能来自配置尚未加载,也可能来自插件的 STATUS 失败,排查时要继续看运行时错误消息,不能只靠状态名判断根因。
GC:清理 DEL 遗漏的陈旧资源
GC 也是 CNI v1.1.0 引入的操作。它面向整个网络配置,而不是某一个 Pod。运行时把仍然有效的 (containerID, ifname) 集合传给插件,插件可以据此清理集合之外的 IPAM 预留、防火墙规则等陈旧资源。
它主要处理节点崩溃、运行时重启或异常清理留下的孤儿状态,但不能替代 DEL。原因是 GC 调用时可能已经没有原来的 namespace 和完整 attachment 上下文,有些只有 DEL 才能安全撤销的资源已经无法识别。
规范还要求运行时在执行 GC 时排斥 ADD 和 DEL:开始 GC 前要等正在执行的增删结束,GC 完成前也不能发起新的增删。这避免插件把刚创建、但尚未写入有效集合的 attachment 当成垃圾回收。
是否主动执行 GC 取决于容器运行时。网络配置也可以通过 disableGC: true 禁止它,例如同一个网络配置由多个运行时共享、单个运行时无法给出完整有效集合时。排查 IP 泄漏时,不能因为插件声明支持 CNI 1.1.0,就假设运行时一定已经执行了 GC。
VERSION:协议协商阶段生效
VERSION 查询插件支持哪些 CNI 规范版本。它不需要 container ID、网络命名空间或接口名,也不会创建或检查 Pod 网络。
网络配置通过 cniVersion 和 cniVersions 声明可用版本,插件通过 VERSION 返回 supportedVersions。运行时可以利用这些信息选择双方和配置都支持的版本。CNI 规范版本与 containernetworking/cni Go 库或 CNI plugins 发布版本彼此独立,看到软件包版本号时不能直接把它当成规范版本。
在日志里看到 VERSION 失败,通常意味着插件二进制、网络配置和运行时之间无法就协议版本达成一致;它发生在 attachment 真正建立之前,不表示某个已经运行的 Pod 突然断网。
插件链:ADD 正序,DEL 逆序
CNI 配置可以是一条插件链。下面的示例先用 bridge 创建接口和分配 IP,再应用 tuning,最后增加 portmap 规则:
| |
执行顺序是:
| |
ADD 中,后一个插件通过 prevResult 接收前一个插件的结果并继续修改。运行时保存最后一个插件返回的 Result。CHECK 和 DEL 则都接收这份最终结果,其中 DEL 逆序执行,先撤销后加的规则,再删除底层接口和 IPAM 状态。
这里还有两种「插件调用插件」的情况,不要和运行时的插件链混淆:
bridge等主插件可以把 IP 分配委托给host-local、dhcp等 IPAM 插件;- Multus 等 meta plugin 可以根据附加网络定义继续调用其他 CNI 插件。
被委托的插件仍要参与相应的 DEL、CHECK、GC 或 STATUS。如果委托的 ADD 失败,上层插件应尝试执行对应的 DEL,避免留下半完成资源。
从 Kube-OVN 和 Cilium 看实现差异
规范只定义调用契约。一个具体插件注册了哪些回调、CNI 配置选择了哪个规范版本、回调内部是直接操作网络还是请求常驻 Agent,必须回到对应版本的源码确认。
下面对比两个固定源码快照:
| 操作 | Kube-OVN 快照 | Cilium 快照 |
|---|---|---|
VERSION | 使用 CNI 库的 version.All 返回库支持版本 | 显式声明支持 v0.1.0 到 v1.1.0 |
ADD | 解析参数后,通过 Unix socket 请求 kube-ovn daemon 的 /api/v1/add | 请求 cilium-agent 获取配置和 IPAM,再创建 veth/netkit、配置接口并创建 endpoint |
DEL | 通过 Unix socket 请求 daemon 的 /api/v1/del | 删除 Agent endpoint,先释放委托 IPAM,再尽力删除 namespace 内接口 |
CHECK | 没有注册实际处理回调 | 检查 Agent endpoint 健康状态,并对照 prevResult 检查接口和 IP 地址 |
STATUS | 没有注册实际处理回调 | 检查 cilium-agent healthz;使用委托 IPAM 时继续调用下层插件的 STATUS |
GC | 没有注册实际处理回调 | 没有注册实际处理回调 |
Kube-OVN:CNI 二进制把 ADD 和 DEL 转给 daemon
Kube-OVN 的 cmd/cni/cni.go只在 skel.CNIFuncs 中注册了 Add: cmdAdd 和 Del: cmdDel。
cmdAdd 自己不直接完成整套 OVS/OVN 配置。它从 CNI 参数中取出 Pod、namespace、container ID、netns 和 ifname,再通过 server_socket 请求节点上的 kube-ovn daemon。daemon 只暴露 /api/v1/add 和 /api/v1/del 两个 CNI 内部接口,实际完成地址等待、网卡、OVS 端口及相关节点配置。CNI 二进制最后把 daemon 返回的接口、IP、路由、MTU 和 DNS 转成标准 CNI Result。
这个快照随镜像安装的 01-kube-ovn.conflist使用 cniVersion: 0.3.1,插件链是 kube-ovn -> portmap。因此创建时先执行 kube-ovn 的 ADD,再添加端口映射;删除时顺序相反。这个配置版本早于引入 CHECK 的 v0.4.0,也早于 STATUS、GC 所在的 v1.1.0,运行时不会对这份默认配置发起这些新操作。
源码依赖的 CNI skeleton 对空回调直接返回成功,因此如果另行把配置版本提高到 v1.1.0,CHECK、STATUS 或 GC 可能表现为「调用成功但没有执行检查或回收」。所以判断能力时不能只看 VERSION 输出或退出码,还要检查插件注册的 CNIFuncs 和实际配置版本。
Cilium:CNI 二进制与 cilium-agent 共同完成生命周期
Cilium 的 PluginMain注册了 Add、Del、Check 和 Status,并显式声明支持到 CNI v1.1.0。
在非 chaining 模式下,ADD 会先连接 cilium-agent,获取节点配置并请求 IPAM,然后由 CNI 进程创建 veth 或 netkit、把容器侧接口移入 netns,最后把 endpoint 交给 Agent。数据面的后续维护不局限在这次短暂的 CNI 进程里,常驻的 cilium-agent 还会继续管理 endpoint 和 eBPF 状态。
它的 CHECK 会执行两层校验:先通过 Agent 查询 (containerID, ifname) 对应 endpoint 的健康状态,再进入目标 netns,对照 prevResult 检查接口及预期 IP。STATUS 则查询 cilium-agent 的 healthz;如果配置使用 host-local 等委托 IPAM,还会把 STATUS 继续传给 IPAM 插件。
不过,Cilium 这个快照由 Agent 生成的默认独立模式配置仍是 cniVersion: 0.3.1。也就是说,二进制虽然实现了 CHECK 和 STATUS,默认配置的协议版本不会让运行时调用它们。只有配置版本、运行时和插件三者都支持,对应操作才会进入实际调用链。
这两个实现还共同说明一件事:常驻 Daemon 的内部健康接口不是新的 CNI 操作。Kube-OVN 的 Unix socket API 和 Cilium Agent API 都是插件自己的实现细节;对容器运行时暴露的边界仍是 CNI 的标准命令。
哪些网络行为不是独立的 CNI 操作
Kubernetes Service、NetworkPolicy、集群路由、隧道和 eBPF 数据面并没有各自对应一个标准 CNI_COMMAND。CNI 只规定运行时和插件之间的 attachment 生命周期接口,具体网络方案可以用不同方式实现其他能力:
- 在
ADD时写入接口、路由或策略; - 由节点上的常驻 Agent watch Kubernetes API,再持续维护数据面;
- 由控制器更新外部网络系统;
- 在
DEL或GC时撤销节点和外部状态。
因此,Pod 的 ADD 成功只证明运行时接受了插件返回结果,不自动证明跨节点通信、Service 转发、DNS 或 NetworkPolicy 都已经正确工作。排查时仍要按接口、地址、路由、邻居、规则、隧道和控制面状态逐层确认。
用生命周期定位问题
遇到 CNI 错误时,可以先根据阶段缩小范围:
| 现象 | 优先检查 |
|---|---|
Pod 长时间 ContainerCreating,事件含 FailedCreatePodSandBox | ADD 输入、插件日志、IPAM 容量、主机接口与外部控制面 |
| 删除 Pod 或 sandbox 失败 | DEL 是否拿到原配置和缓存结果、netns 消失后能否继续回收、插件链逆序清理 |
NetworkPluginNotReady | CNI 配置是否加载、二进制是否存在、运行时 Status 细节、插件 STATUS 依赖 |
| Pod 已运行但怀疑网络状态漂移 | 运行时是否实际触发 CHECK;再独立检查接口、路由、规则和端到端流量 |
| 节点长期出现 IP 或规则泄漏 | DEL 失败记录、运行时缓存、是否实现并调度 GC、有效 attachment 集合是否完整 |
| 插件报不支持 CNI 版本 | 网络配置版本、运行时支持版本和插件 VERSION 结果 |
最后可以把六个操作压缩成三个层次:
ADD、CHECK、DEL管理一个具体 attachment 的创建、校验和销毁。STATUS、GC管理网络插件整体的可服务状态和遗留资源。VERSION只负责协议能力协商,不改变网络状态。
在 Kubernetes 中,kubelet 看到的是 CRI 的 sandbox 和运行时状态,真正执行 CNI 操作的是容器运行时。把这个边界分清,再判断错误发生在创建、存续、删除还是运行时维护阶段,通常比笼统地说「CNI 插件有问题」更接近根因。