手工执行22 的 ip 命令可以构造连通路径,但节点运行时还要重复地给新进程挂接网络,并在失败时回收地址、设备与路由。CNI 规范定义调用方与插件之间的配置、输入环境、输出和操作接口,不是一个指定必须使用 bridge 或 VXLAN 的数据面。

把意图、调用、对象串起来

调用方给插件网络配置、容器 ID、netns 路径和接口名,执行 ADD;插件/链式配置可能交给 IPAM 分配地址并创建 veth、bridge 连接、路由,再返回地址和接口信息。CHECK 用于适用版本的状态核对;DEL 释放自己创建的网络资源。命令成功也不能单独证明业务连通,应该把输出配置的 IP 对应到 namespace inode、内核网络对象及一次真实请求。各操作的幂等、失败恢复前提需要对选定插件的固定版本源码再验证,不对所有插件宣称绝对原子。

正常实验必须在专用 VM 固定 CNI spec、插件和 IPAM 版本,先建两个自有 netns,逐个执行 ADD/CHECK/DEL,前后读取 veth、地址租约、路由;异常例故意使第二步失败,再调用 DEL 并确认设备与 IP 真正释放,二次执行结果需可核对。不可对节点上已投入使用的 Pod 插件配置做故障注入。运行材料给出需要填写的变量与清理步骤;共用探针附件用作请求检查。

本机没有 CNI 插件、ip、节点 runtime;本文核对了 CNI 库 v1.3.1 和标准插件 v1.7.1 的选定源码,仅为 DOC_VERIFIED 的阅读定点,不是安装版本。真实 ADD/CHECK/DEL、失败回滚仍 NOT_RUN。此前网络 33的一份静态 JSON 不能转为本篇 ADD 成功日志。本篇 bridge + host-local 配置、仅用于自建 netns 的失败输入与配置校验可以独立获取;插件二进制必须在实验 VM 按所选版本另装并计算实测 SHA-256。

接口并不是网络拓扑

CNI 规范规定调用方怎样向插件传入网络配置及运行时参数:配置经标准输入传递,CNI_COMMAND 指定 ADD、CHECK、DEL 等操作,CNI_CONTAINERID、CNI_NETNS、CNI_IFNAME 标识这次附着以及目标接口,CNI_PATH 决定在哪里查找可执行插件。插件按规范返回结果或错误;结果里的 interfaces、IPs 与 routes 告诉上层本次配置声称得到什么对象,而不是保证应用已经对外提供 HTTP 服务。所谓 CNI 网络的实际数据面可以由 bridge、点对点路由、隧道或其他插件组合实现,这篇选 bridge + host-local 只是为了让接口和 Linux 对象能在独占 VM 一一核验,不是 Kubernetes 所有集群默认的网络选择。

特别要把 ID 与 netns 分开:container ID 是调用方给出的不空身份,用于重复调用与回收同一次附着;netns 路径是打开目标网络视图的句柄,可以通过 ip netns add 的 bind mount 名称取得,接口名例如 eth0 则是在目标视图中要创建/检查的名字。旧 namespace 句柄失效时,DEL 能否凭存储的状态释放全部资源要看插件具体实现;不是“路径不存在则 CNI 绝不泄漏”或“任何 DEL 都会成功”的规范承诺。容器和 Pod 的生命周期由更高层决定,CNI 请求自身既不会创建应用进程,也不负责 kubelet 的重启策略。对节点上正在服务的 Pod,只能做读取已有对象的观测;故障注入必须使用本篇自建的两个 netns 和独立 IPAM 路径。

规范中 CHECK 不是“应用健康检查”。它让支持相应版本的插件检查此前建立的网络配置与预期是否一致;返回成功不会对 probe-app 的 /health 发 HTTP,不知道 socket 是否只绑定 loopback、DNS 是否解析了错误名字或 NetworkPolicy 是否丢包。CNI v0.4.0 引入 CHECK;本篇静态配置声明 cniVersion: 1.0.0,调用前仍要确认所选二进制通过 VERSION 协商支持这一配置,并检查 CHECK 的真实返回。拿较早插件版本和新版配置强制组合后的“不支持”结果,不能被写成当前 network namespace 连接失败。CNI 规范不是验证端到端业务 SLO 的接口,19 和 22–23 的请求证据必须另采。

IPAM 租约、veth 与 bridge 谁留下

本篇配置把测试网络命名为 containers-24,指定桥名 cni24br0 和 host-local IPAM;地址池在 VM 私有实验网段 198.51.100.0/28,dataDir 是专用 /tmp/containers-24-ipam。桥名和目录如果在 VM 上与他人冲突,应停止,不得删除别人资源。host-local 会为具体 (container ID, ifname) 管理自建网络的地址记录,bridge 插件把容器一侧 veth 放入 netns 并接上桥端口,具体网关/IP 的实际值必须从 ADD 的 JSON 输出、ip、租约文件三处一致地读出。isGateway 是对本次实验桥的配置要求,不能从一个 JSON 字段推断该 VM 宿主已打开所有跨网段转发能力;ipMasq:false 明确不以本次插件配置自动加入 IP masquerade 作为成功条件,测试只关心自建网段内互访。

固定版本源码支持较窄的机制判断。containernetworking/plugins v1.7.1 的 plugins/main/bridge/bridge.go 在 cmdAdd 中先解析配置并设置 bridge、veth,然后在有 IPAM 配置时调用 ipam.ExecAdd,读取 IP 地址和路由,再设置接口;取得 IPAM 结果后设有失败路径调用 ipam.ExecDel 的延期清理。plugins/ipam/host-local/backend/allocator/allocator.go 和 backend/disk/backend.go 展示租约保留、按 ID/接口释放及磁盘存储的实现。因此 ADD 失败后查租约目录与 Linux 链路对象是必要的:一段源码里有延期清理,不等于所有失败时点已经由实际 VM 验证“没有残留”;甚至错误发生在 IPAM 分配之前、或插件进程被外部强行终止,也不一定经过相同的 defer 分支。实验只调用可信、已固定版本的插件,不向其它网络注入故障。

本篇并列提供一个可查问题:A 先 ADD 后,B 的 ADD 是否从同一地址池拿到不同地址?返回的 interfaces 是否和 A/B 各自 netns 里的实际 ifindex、veth 对端一致?如果 CHECK 在二者都存在时成功,暂停 B 的自建 HTTP 进程再 CHECK,可能仍报告网络对象正常;它不负责应用存活。这是插件接口验证与业务探针验证的刻意分离。若请求失败,先看 B 的监听地址与 22 的路由,再看 23 的 bridge 转发与 DNS;不拿 CHECK 为 0 就直接责怪业务代码,也不拿 /health=200 推导 IPAM 已正确释放所有旧地址。

库调用、插件内部调用和直接执行要区分

当网络配置是一个 .conflist,libcni 可以按列表顺序执行各个插件的 ADD,将前一步结果作为 prevResult 传给下一步;反向 DEL 的行为则需要检查当次实现。containernetworking/cni v1.3.1 的 libcni/api.go 中,AddNetworkList 顺序调用并在错误时直接返回错误;这段路径没有对之前已成功的插件执行自动逆序 DEL。相同源码的 DelNetworkList 倒序调用插件,并处理缓存的上次结果。因此不能只因“使用了 libcni”就保证多插件链的 ADD 对用户原子;调用方在链失败后是否发 DEL、某插件是否因 netns 已消失而跳过、IPAM 租约是否释放都要看本次调用与真实对象。规范允许插件链,不保证所有运行时的回滚策略相同。

本篇准备的文件是单个 bridge 插件配置,不是 .conflist;bridge 内部又按它的 ipam.type 调用 host-local。直接执行 bridge 二进制并设置 CNI 环境变量,验证的是这个插件及其内部 IPAM 调用路径,不是 libcni.AddNetworkList 的缓存、自动编排或容器运行时。如果要进一步演示多插件链失败,需要一套受控调用方和第二插件,并对它的每次 ADD/DEL 输出记账;不能把 direct plugin 成功日志称为真实 Kubernetes 调用链。这里读 libcni/api.go 是为了界定依赖回滚的责任,而现场只有在专用 VM 实际运行相应 caller 才能判定其清理是否充分。

版本一致性也不能靠两个可执行文件同名来保证。VM 里的 bridge、host-local 应有确定的上游 release、二进制 SHA-256 与执行权限;二者在同一个受控 CNI_PATH 中,让 bridge 调用 IPAM 时不会意外用节点预装的另一套二进制。VERSION 不需要已经存在的容器进程,用它检查支持的 spec 版本后,才能将配置的 cniVersion 与插件能力对齐。输出解析也应尊重 JSON 的实际内容:如果返回的地址与配置预期不同,保留输出和 IPAM 的实际租约,不能按配置表硬编码“分到 198.51.100.2”。VM 初始已有租约、插件重复调用方式等因素都可能改变本次分配,先查冲突而不是强行清空全局 CNI state。

正常完成要核实四种状态

先核验 ADD 的 stdout JSON 与退出码;再在 A/B netns 内读接口、地址、路由,并在宿主读只属于本篇 bridge 的端口;第三到专用 IPAM 目录找两只容器 ID 对应的租约;最后让 A 的可信应用进程通过 B 的数字地址请求同一探针的 /health。四层分别回答“插件说它配置了什么”“内核目前有哪些网络对象”“地址是否被占用”和“真实业务流量是否通”。其中任何一步不成立都不能记成这篇的完整正常案例。若 A 只有接口却没有地址,先看 host-local 的返回和 bridge 的接口设置;若有 IP 而 HTTP 超时,转查 22–23 的路由、监听和过滤,不应仅用 ADD 返回 0 推断应用已就绪。

然后对 A/B 各执行适用的 CHECK,保存命令参数、stdout/stderr 与退出码。插件如果只检查已配置的网络对象,它不会替业务探针产生数据包;再次调用 ADD 也不应当直接当作 CHECK 的替代。重复 ADD 是需要实测的幂等性边界:按 CNI spec 对“同一个容器 ID 的重复调用”有要求,具体插件版本怎样识别已有 ifname 和已保留的租约还要查源码/实测;在没有固定 plugin 的机器上谈“一定有两个 veth”或“一定是原来 IP”都没有证据。实验在自己 netns 内先记录重复调用前的接口索引/租约,再执行第二次 ADD,比较原始值;发现异常则优先清理本篇对象,不运行无界重试。

最后执行 DEL 后并列检查:A/B 对应 eth0 是否还在,cni24br0 是否还有本篇端点、dataDir 是否有该 container ID 的租约;必要时再对同一自建 ID执行一次 DEL,记录返回值而不把重复 DEL 成功推广到所有插件。一个不再存在的 veth 不等于租约也已释放;一个已释放的租约不意味着 netns 里的旧进程已经停止。如果桥设备在所有容器离开后仍在 VM 中,需要先确认它由本次配置创建且不被其它对象引用,才在专用 VM 里清理;不能用 ip link delete 去清理节点预置 bridge。VM 结束前比较网络和租约目录的前后快照,确认只有本篇对象发生变化。

失败发生在 IPAM 前还是 IPAM 后

可以在独占 VM 的 B 自建 netns 中让配置副本的 ipam.type 指向不存在的实验插件名,同时保持网络名、桥名与 IPAM 专用目录均指向本篇对象。这样 bridge 插件可能已经走到创建 bridge/veth 的步骤,而调用 IPAM 时失败;这比删掉整个插件二进制更能观察部分配置后的现场。但具体是否留下一侧 veth/桥由 v1.7.1 代码路径和 VM 的 netlink 结果决定,不能在配置文件里先写好预期结果。失败后记录 ADD 退出码、插件 stderr、B 的 netns 接口/宿主桥、租约目录,再用原本正确配置及相同 ID 调用 DEL 并验证回收;如果因配置不一致需要手工删除,只能针对自己刚创建的端点和目录执行,并说明普通 DEL 的失败。直接删完就只记录“无残留”,会失去定位失败在哪一步的价值。

另一个无需碰已存在设备的负例是指定一个不存在的 netns 路径。插件在打开目标视图时应该返回错误,正好可以检查错误发生在获取 IPAM 租约之前还是之后;具体顺序可对照选定 cmdAdd 源码,但现场仍要查租约文件和设备,因为版本、权限与其它错误可能先于目标打开路径出现。由于这轮根本没有可运行应用,不能要求 HTTP 503 作为 CNI 失败的“标准表现”;真实结果可能是运行时在 sandbox 准备阶段直接报错,容器 ID 也可能尚未持久化。专用 VM 中用两个不同的自建 ID 分别测试无效 netns 与无效 IPAM,避免错误后残留与下一轮重试混在一起。

重复 ADD 的风险不只是多分一个地址:如果某次调用方 ID 重用而原 netns 句柄已失效,IPAM 的记录与现有接口可能指向不同生命周期。先比较完整 CNI_CONTAINERID、接口名、目标 netns inode 和 IPAM 数据目录,再决定是不是“同一调用重试”。不同容器复用同名 eth0 很常见,因为它们在不同 netns;不能从两个 eth0 这个字符串就认定有冲突。B 的旧 IP 与 A 的新 IP 若出现重复,需要按租约存储与网络对象查证,不能以“bridge 决定了 IP”解释一个本该由 host-local 管理的现象。业务请求若到错实例,应从请求中的目标地址、实际端点和本次 ADD 返回记录反向关联,不从 Pod 昵称回推地址。

CHECK 的返回语义还有一层版本约束。libcni v1.3.1 的 CheckNetworkList 对配置版本做门槛检查,再取缓存的上次 result 并按顺序让各插件 CHECK;本篇直接调用 bridge 二进制,不携带 libcni 的缓存,读者不能拿库调用结果解释直接调用的实际行为。DEL 用库链倒序的逻辑也属于库实现,直接插件里桥接与 IPAM 的清理顺序须按该插件自身版本源码和原始输出核对。把规范、库和插件的三层结论拆开,恰好能解释为什么“ADD 成功”不等于“网络完全验收”,“DEL 返回 0”也不等于“所有地址都归还”。如果待补实验缺所选插件可执行文件,就保留配置 JSON 作为 STATIC_CHECKED,不写不存在的命令退出码。

在这里直接调用 v1.7.1 bridge 的 cmdCheck 还有一个具体要求:源码先调用 IPAM 的 CHECK,随后读取传入配置中的 prevResult,缺少它就会报 Required prevResult missing。所以 VM 正常轮须将刚才 ADD 的原始 JSON 结果作为 prevResult 拼到相同配置文件后再调用 CHECK;不能原样把 ADD 的 stdin 重定向一遍,就把因为缺字段而失败的 CHECK 判成设备没创建。这个临时拼接文件不是新规范版本,也不应该修改通过 SHA-256 校验的原始附件,生成后还须存它的内容、文件 SHA 和创建命令。DEL 的输入要按所选插件实现另行确认,不能无差别复制 CHECK 的缓存前提。

Kubernetes 节点上的调用路径还在上游:26 会把 kubelet、CRI 的 Pod sandbox 创建与运行时网络设置关联起来,CNI 可以是 runtime/节点网络管理组件执行的一段动作。CNI 的 container ID 不必直接等于 Pod UID;若想将这篇的接口实验延伸到一个真实 Pod,至少还要把 sandbox ID、所用插件路径/版本、namespace inode、分到的 Pod IP 和请求结果逐项对应。在没有 CRI/Pod UID 的当前云端,这一部分完全 NOT_RUN;不能把两只 ip netns add 的示例称为 Kubernetes Pod 已配网。网络安全策略是否生效还依赖额外插件和配置,CNI 核心接口本身不是 NetworkPolicy 的实现保证。

为后续节点排障留下最小因果链

一次完整 ADD 记录至少由配置原始 JSON、bridge/host-local 二进制 SHA、环境变量、容器 ID、netns 句柄与 inode、插件 stdout/stderr 和退出码组成;随后是实际 ip addr/route/link、桥端口、专用目录租约和一次探针 HTTP 请求。缺少前半段,看到一只 veth 不知道哪个调用创建;缺少后半段,插件输出的 IP 只是尚未与内核状态核对的声明。CHECK 和 DEL 用相同 ID、IFNAME、NETNS 与配置原文,逐步核查更改后的状态,并记录清理失败时哪些对象仍存在。清理记录有两个粒度:插件自身返回是否成功,VM 环境中本篇的对象是否真正不见,两者不能互相顶替。

无效 IPAM 失败后,不能直接把配置副本换回正确值继续 ADD 而不先采集残留;否则新增对象可能覆盖原本需要取证的状态。对同一自建容器 ID,先读 veth 对端、租约目录及 bridge 端口;再调用正确配置下的 DEL,并验证它释放的是原失败调用留下的对象;最后用新的 ID 做正常重试。若 DEL 清理不掉部分对象,停止重试,记录原始错误和手工仅清理本篇接口/bridge 的命令。所有数据目录由专用 VM 中本篇唯一名称创建,绝不能清除节点全局的 /var/lib/cni/networks 或其它 Pod 使用的池。即使是在一次性 VM,删除之前也要检查目录前后文件列表与关联的 container ID。

从 ip netns 视角清理也须小心:若 A/B 的 HTTP 探针仍在运行,命名空间的引用可能依旧存在,删除名称并不自动证明进程退出;实验先停自建服务并 wait,执行 DEL,然后删命名空间并核对 PID 消失。相反,在 ADD 失败后若提前删掉 netns 名称再去调用 DEL,插件可能失去清理接口所需句柄。对故障演示而言,先留证、再用原始 ID 清理,最后销毁 namespace 是更可解释的顺序。共享云端无 ip 和 CNI 二进制,不能因为这个顺序写得明确就宣称已经在本机验证;复现入口标 NOT_RUN 并要求 VM 采样证据。

把同一观察迁移到不同实现时,首先问这个组件扮演的是 CNI 调用方、插件、还是 IPAM 内部插件,失败发生在哪一步;其次核对对应版本、配置和清理范围;最后才讨论 HTTP 是否可达。调用方失败而未调用逆序 DEL、插件自身在 IPAM 之后抛错且 defer 尝试释放、IPAM 返回地址但路由未就位,是三种完全不同的残留风险,不能都归到“CNI 不可靠”。本篇选定版本源码帮助预测该去哪儿取证,不代替具体机器上的结果。真正的验收仍要求两个 netns 的正常请求、一次受控失败、重复操作和最终对象/地址租约对照,每一项都要有原始命令与实际返回。

配置文件的 SHA-256 只保证同步到另一工作区时字节未变,不等于该配置已经被 CNI 消费;实验时还须保存插件实际读取的标准输入,因为手工把另一份 JSON 重定向到 stdin 会绕过目录里受校验的文件。版本号也应分三项:CNI 配置的 cniVersion、libcni 库源码的 tag、bridge/host-local 二进制的 release 与实测 hash。仅报告配置文件写着 1.0.0,无法知道是哪一个插件二进制执行了 ADD。实验 VM 的内核和 ip 版本、NETNS mount 路径以及数据目录权限都要跟着记录,便于判断返回码的差异是配置、实现还是权限所致。素材目录只携带可信教学配置和通用 HTTP 探针,不附加会自动修改宿主网络的启动器;所有带副作用的命令只能在自建 netns 的独占 VM 上由操作者执行。

如果实验中发现 ADD 返回 0 但 B 请求 A 得不到 HTTP 状态,先用插件输出的 IP 去查目标进程地址,再查 veth 和路由,最后查监听;CHECK 成功只是说明插件所覆盖的条件成立,不会让应用监听一个尚未绑定的端口。反过来即使 B 偶然收到 HTTP 200,也要检查它是否通过预期的 bridge 端口到达,避免读到了 VM 上旧实验的另一只服务。

现象 核验对象 适用边界
ADD 返回 IP 实际 netns 设备/路由 不等于业务连通
CHECK 不支持 插件与 spec 版本 不可直接称为链路坏
DEL 后地址仍占用 IPAM 租约及自建网卡 需分层清理

练习

  1. 先预测同一容器 ID 重复 ADD 在所选插件版本下会怎样,先读该版本规范与源码,再在实验 netns 上记录设备与地址分配是否重复。
  2. 预测 ADD 后请求超时而 CHECK 报成功时可能缺什么;按监听、网络策略、路由与真实请求逐项排查,不把插件返回码当 HTTP 验收。

上一篇:23:bridge/端口/DNS;下一篇:25:跨主机网络。

系列总目录:从进程隔离到运行时与编排。

参考资料