jimyag's Blog

Kubernetes CNI 接口与 Pod 网络生命周期

Pod 已经被调度到节点,不代表容器可以立刻启动。kubelet 还要通过 CRI 请求容器运行时创建 Pod sandbox;运行时为 sandbox 准备网络命名空间,再执行 CNI 插件。只有这一步成功,Pod 才有可供 init container 和业务容器共享的网络环境。

排查网络问题时,经常会看到 CNI ADD failedfailed to destroy networkNetworkPluginNotReady。这些信息对应的不是同一个阶段:有的在创建单个 Pod 的网络,有的在清理 Pod,有的只检查插件能否继续接收新请求。

本文以 CNI 1.1.0 规范为基准,说明 ADDDELCHECKSTATUSGCVERSION 分别处理什么,以及它们怎样进入 Kubernetes 的 Pod 生命周期。

这里需要先区分两套独立的版本号:1.1.0 是运行时与插件之间的 CNI 协议规范版本v1.3.1containernetworking/cni 代码库的 软件发布版本,对应 libcnipkg/skel 等 Go 包。代码库发布到 v1.3.1,不代表协议规范也变成了 1.3.1。截至本文编写时,官方 SPEC.md 标注的规范版本仍是 1.1.0,并明确说明它与代码库及插件的发布版本相互独立。

先区分 kubelet、容器运行时和 CNI 插件

CNI 规定的是「运行时如何执行网络插件」,不是 kubelet 直接调用某个 CNI Daemon 的 API。这个调用链可以简化为:

1
2
3
4
5
kubelet
  -> CRI RunPodSandbox / StopPodSandbox / Status
    -> containerd、CRI-O 等容器运行时
      -> 执行 CNI 插件二进制
        -> 配置网卡、IP、路由、规则或外部网络状态

CNI 插件通常是一个可执行文件。运行时通过环境变量传入 CNI_COMMAND、容器 ID、网络命名空间和接口名,通过标准输入传入 JSON 配置;插件通过退出码和标准输出返回结果。它不是 CSI 或 Device Plugin 那种先向 kubelet 注册、再通过长期运行的 gRPC 服务接收请求的接口。

从 Kubernetes 1.24 开始,kubelet 不再负责 cni-bin-dirnetwork-plugin 等 CNI 管理参数,CNI 配置和插件加载属于容器运行时的职责。Kubernetes 当前要求插件至少兼容 CNI v0.4.0,并推荐兼容 v1.0.0,详见 Kubernetes Network Plugins 文档。这也意味着:规范里存在某个操作,不等于集群当前的运行时和插件一定会调用、实现它。

六个操作分别在哪个阶段生效

操作作用对象常见触发阶段主要作用
VERSION插件二进制加载配置、选择协议版本时查询插件支持的 CNI 规范版本,不修改 Pod 网络
STATUS整个插件或网络配置运行时初始化、CRI 状态检查或后台健康检查时判断插件是否还能处理新的 ADD 请求
ADD一个网络 attachmentRunPodSandbox 创建网络时把 sandbox 加入网络,创建或调整接口、IP、路由等
CHECK已存在的 attachmentADD 成功后到 DEL 前,由运行时按需触发对照上次 ADD 结果检查网络状态是否仍符合预期
DEL一个网络 attachmentStopPodSandbox、删除 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 JSONcniVersion、网络名、插件配置和运行时能力参数

成功的 ADD 会返回 CNI Result,其中可以包含接口、IP 地址、路由和 DNS。运行时必须持久保存插件链的最终结果,因为后续 CHECKDEL 需要使用它。

在 Kubernetes 中,这一步处于 Pod sandbox 建立阶段。Kubernetes 的 PodReadyToStartContainers 条件会反映 sandbox 和网络是否已经准备完成。网络配置成功后,kubelet 才继续拉取镜像、创建 init container 和业务容器。因此:

  • ADD 失败通常会让 Pod 停留在 ContainerCreating,并产生 FailedCreatePodSandBox 事件;
  • 重启 Pod 内的单个业务容器通常不会再次调用 ADD,因为它仍使用原来的 Pod sandbox 网络命名空间;
  • sandbox 被删除并重建时,才会重新建立网络并调用新的 ADD
  • hostNetwork: true 的 Pod 使用宿主机网络命名空间,通常不经过默认 Pod 网络的 CNI ADD

containerd 的 CRI 架构说明也把这段顺序写成:CRI 创建 Pod 网络命名空间,用 CNI 配置网络,然后再创建和启动 sandbox 及业务容器。不同运行时的内部对象顺序可以不同,但 CRI 返回成功前必须完成 sandbox 所需的网络配置。

DEL:sandbox 清理时生效

DEL 撤销对应 ADD 创建或修改的状态,例如:

  • 释放 IPAM 分配;
  • 删除主机侧接口;
  • 撤销端口映射、防火墙或流量控制规则;
  • 删除外部网络系统中的 endpoint。

正常路径上,kubelet 请求运行时停止 Pod sandbox,运行时先执行网络清理,再删除网络命名空间。本文检查的 containerd 提交可以在 StopPodSandbox路径中看到这段调用。

DEL 有三个容易忽略的约束:

  1. 即使 ADD 返回失败,运行时也必须继续尝试对应的删除操作,因为前面的插件可能已经分配了 IP 或创建了主机资源。
  2. CNI_NETNSDEL 是可选参数。namespace 可能已经消失,插件仍应尽可能释放 IPAM、主机规则和外部资源。
  3. 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_CONTAINERIDCNI_NETNSCNI_IFNAME,检查的是插件是否已经准备好处理新的 ADD

如果插件依赖外部服务,或者 IP、硬件队列等有限资源已经耗尽,可以通过 STATUS 返回错误。规范定义了两个相关错误码:

  • 50:插件不可用,不能处理新的 ADD
  • 51:插件不可用,并且现有容器的网络也可能受限。

STATUS 是信息性的。插件不能依赖运行时一定先调用它;即使它返回失败,规范也不保证运行时会阻止后续所有 ADDDEL

在 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 时排斥 ADDDEL:开始 GC 前要等正在执行的增删结束,GC 完成前也不能发起新的增删。这避免插件把刚创建、但尚未写入有效集合的 attachment 当成垃圾回收。

是否主动执行 GC 取决于容器运行时。网络配置也可以通过 disableGC: true 禁止它,例如同一个网络配置由多个运行时共享、单个运行时无法给出完整有效集合时。排查 IP 泄漏时,不能因为插件声明支持 CNI 1.1.0,就假设运行时一定已经执行了 GC。

VERSION:协议协商阶段生效

VERSION 查询插件支持哪些 CNI 规范版本。它不需要 container ID、网络命名空间或接口名,也不会创建或检查 Pod 网络。

网络配置通过 cniVersioncniVersions 声明可用版本,插件通过 VERSION 返回 supportedVersions。运行时可以利用这些信息选择双方和配置都支持的版本。CNI 规范版本与 containernetworking/cni Go 库或 CNI plugins 发布版本彼此独立,看到软件包版本号时不能直接把它当成规范版本。

在日志里看到 VERSION 失败,通常意味着插件二进制、网络配置和运行时之间无法就协议版本达成一致;它发生在 attachment 真正建立之前,不表示某个已经运行的 Pod 突然断网。

插件链:ADD 正序,DEL 逆序

CNI 配置可以是一条插件链。下面的示例先用 bridge 创建接口和分配 IP,再应用 tuning,最后增加 portmap 规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "cniVersion": "1.1.0",
  "name": "pod-network",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "ipam": {
        "type": "host-local",
        "subnet": "10.244.0.0/16"
      }
    },
    {
      "type": "tuning"
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

执行顺序是:

1
2
3
4
5
ADD:    bridge -> tuning -> portmap
CHECK:  bridge -> tuning -> portmap
DEL:    portmap -> tuning -> bridge
GC:     bridge -> tuning -> portmap
STATUS: bridge -> tuning -> portmap

ADD 中,后一个插件通过 prevResult 接收前一个插件的结果并继续修改。运行时保存最后一个插件返回的 Result。CHECKDEL 则都接收这份最终结果,其中 DEL 逆序执行,先撤销后加的规则,再删除底层接口和 IPAM 状态。

这里还有两种「插件调用插件」的情况,不要和运行时的插件链混淆:

  • bridge 等主插件可以把 IP 分配委托给 host-localdhcp 等 IPAM 插件;
  • Multus 等 meta plugin 可以根据附加网络定义继续调用其他 CNI 插件。

被委托的插件仍要参与相应的 DELCHECKGCSTATUS。如果委托的 ADD 失败,上层插件应尝试执行对应的 DEL,避免留下半完成资源。

从 Kube-OVN 和 Cilium 看实现差异

规范只定义调用契约。一个具体插件注册了哪些回调、CNI 配置选择了哪个规范版本、回调内部是直接操作网络还是请求常驻 Agent,必须回到对应版本的源码确认。

下面对比两个固定源码快照:

  • Kube-OVN v1.17.0 开发分支提交 a39b9533
  • Cilium 1.19.0-dev 提交 2c59ba7d
操作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: cmdAddDel: 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,也早于 STATUSGC 所在的 v1.1.0,运行时不会对这份默认配置发起这些新操作。

源码依赖的 CNI skeleton 对空回调直接返回成功,因此如果另行把配置版本提高到 v1.1.0,CHECKSTATUSGC 可能表现为「调用成功但没有执行检查或回收」。所以判断能力时不能只看 VERSION 输出或退出码,还要检查插件注册的 CNIFuncs 和实际配置版本。

Cilium:CNI 二进制与 cilium-agent 共同完成生命周期

Cilium 的 PluginMain注册了 AddDelCheckStatus,并显式声明支持到 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。也就是说,二进制虽然实现了 CHECKSTATUS,默认配置的协议版本不会让运行时调用它们。只有配置版本、运行时和插件三者都支持,对应操作才会进入实际调用链。

这两个实现还共同说明一件事:常驻 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,再持续维护数据面;
  • 由控制器更新外部网络系统;
  • DELGC 时撤销节点和外部状态。

因此,Pod 的 ADD 成功只证明运行时接受了插件返回结果,不自动证明跨节点通信、Service 转发、DNS 或 NetworkPolicy 都已经正确工作。排查时仍要按接口、地址、路由、邻居、规则、隧道和控制面状态逐层确认。

用生命周期定位问题

遇到 CNI 错误时,可以先根据阶段缩小范围:

现象优先检查
Pod 长时间 ContainerCreating,事件含 FailedCreatePodSandBoxADD 输入、插件日志、IPAM 容量、主机接口与外部控制面
删除 Pod 或 sandbox 失败DEL 是否拿到原配置和缓存结果、netns 消失后能否继续回收、插件链逆序清理
NetworkPluginNotReadyCNI 配置是否加载、二进制是否存在、运行时 Status 细节、插件 STATUS 依赖
Pod 已运行但怀疑网络状态漂移运行时是否实际触发 CHECK;再独立检查接口、路由、规则和端到端流量
节点长期出现 IP 或规则泄漏DEL 失败记录、运行时缓存、是否实现并调度 GC、有效 attachment 集合是否完整
插件报不支持 CNI 版本网络配置版本、运行时支持版本和插件 VERSION 结果

最后可以把六个操作压缩成三个层次:

  1. ADDCHECKDEL 管理一个具体 attachment 的创建、校验和销毁。
  2. STATUSGC 管理网络插件整体的可服务状态和遗留资源。
  3. VERSION 只负责协议能力协商,不改变网络状态。

在 Kubernetes 中,kubelet 看到的是 CRI 的 sandbox 和运行时状态,真正执行 CNI 操作的是容器运行时。把这个边界分清,再判断错误发生在创建、存续、删除还是运行时维护阶段,通常比笼统地说「CNI 插件有问题」更接近根因。

参考资料

#Kubernetes #Cni #容器网络 #Containerd