本文档基于 Kubernetes mini-cni 项目的源代码注释编写,深入解析 CNI(Container Network Interface)插件的工作机制、PodSandbox 的生命周期管理以及容器网络的底层实现细节。
RunPodSandbox 负责创建一个 Pod 的沙箱,也就是 Pause 容器加上 Network Namespace 的组合体。这是 Pod 网络初始化的绝对起点。
配置中需要填写的关键信息包括:
这些信息会被打包成一个标准的 PodSandboxConfig 对象,传递给后续的 CRI 接口。
m.runtimeService 就是 Containerd/Docker 的 CRI 客户端。通过这个客户端,Kubelet 可以与底层的容器运行时进行标准化的通信。
这一步之后,Containerd 会去调用 CNI 插件!
这是一个至关重要的转折:RunPodSandbox 本身并不直接配置网络,而是通过创建 Sandbox 触发 Containerd 调用 CNI 插件来完成网络配置。
CNI 插件的调用逻辑位于 github.com/containernetworking/cni/pkg/invoke/raw_exec.go。这是一个独立的库,被所有符合 CNI 规范的容器运行时所使用。
直接调用二进制文件的路径。例如 /opt/cni/bin/mini-cni。这个路径通常在 Kubelet 启动时通过 --cni-bin-dir 参数指定。
把 JSON 配置通过 Stdin(标准输入)塞进去。这就是为什么我们在 main.go 里要用 json.Unmarshal(os.Stdin) 来接收配置的原因。
这个 JSON 配置包含了:
CNI 规范定义了一组标准的环境变量,必须在调用前设置好:
CNI_COMMAND:操作类型,如 “ADD”、”DEL”、”CHECK”CNI_CONTAINERID:容器的唯一标识符CNI_NETNS:网络命名空间的路径CNI_IFNAME:接口名称(通常是 eth0)CNI_PATH:CNI 插件目录这些环境变量是 CNI 插件获取上下文信息的官方渠道。
执行完毕后,CNI 插件需要通过 stdout 返回一个 Result JSON。这就是 Kubelet(通过 Containerd)收到的那个 Result JSON,包含了分配的 IP 地址、网关、路由等信息。
在插件入口处新增 Panic 捕获,防止程序崩溃了没有任何日志。这对于调试生产环境的网络问题至关重要。
修复动作 1:启动 Loopback (lo) 接口。如果不启动这个,很多网络操作会莫名其妙失败。
lo 接口是每个网络命名空间的必备基础设施,它提供了本地回环通信的能力(127.0.0.1)。缺少 lo 接口会导致:
因此,在任何网络配置开始前,必须先启用 lo 接口。
在创建新的 veth pair 之前,先进行暴力清理,清除可能残留的旧接口。这是一种防御性编程的实践,可以避免因接口重名导致的冲突。
Veth Pair 是一对虚拟以太网设备,它们的特点是:从一个设备进入的数据包会从另一个设备出来,就像一根虚拟的网线连接了两个网口。
在 CNI 场景中:
这样就建立了容器到宿主机网络的桥梁。
为容器的 eth0 接口分配 IP 地址。这个 IP 通常是 Pod CIDR 范围内的一个空闲地址。
修复动作 2:显式确保 eth0 是 UP 的。虽然 SetupVeth 函数可能会做这件事,但再做一次是为了保险起见。
网络接口必须处于 UP 状态才能收发数据包。在某些边缘情况下(如内核版本差异、竞态条件),仅靠 SetupVeth 可能不足以保证接口激活,因此需要显式地再调用一次 netlink.LinkSetUp。
配置一条默认路由,将所有非本地流量指向网关。这是容器能够访问外部网络的前提。
这里曾经是容易报错的地方,常见的问题包括:
将 veth 的另一端插入到宿主机的网桥(如 cbr0 或 br-cni)上。这样做的目的是:
最后一步是构造并返回 Result JSON,告知调用者(Containerd/Kubelet)网络配置的结果。标准的 Result 包含:
CNI 插件的名称需要被传递和使用,因此在代码结构中定义为外部可见的常量或变量,而不是隐藏在内部作用域中。
这种设计的好处是:
通过分析 mini-cni 项目的源码注释,我们可以看到 Kubernetes 容器网络实现的几个核心原则:
Pod 网络的初始化不是一蹴而就的,而是分为清晰的两个阶段:
这种分工的优势在于解耦:Kubelet 只需要关心 Pod 的生命周期,而不需要了解具体的网络技术细节;网络配置的复杂性被封装在了 CNI 插件中。
CNI 规范定义了严格的输入输出契约:
正是这种标准化,使得 Kubernetes 可以无缝对接数十种不同的 CNI 实现(Flannel、Calico、Cilium、Weave 等),而无需修改一行核心代码。
从 Panic 捕获到双重确认接口 UP 状态,再到暴力清理残留资源,这些看似冗余的代码实际上都是生产环境血泪教训的结晶。在网络这种容易出现竞态条件和边缘场景的领域,防御性编程不是可选项,而是必选项。
即使是像 CNI 插件这样短命的进程(生命周期可能只有几毫秒),也需要考虑日志记录和错误追踪。这也是为什么要在最开始就设置 Panic 捕获的原因——宁可牺牲一点点性能,也要保证出现问题时有迹可循。
理解 CNI 插件的工作原理,对于排查 Kubernetes 网络问题、开发自定义网络插件以及优化大规模集群的网络性能都具有重要的指导意义。
为了验证CNI插件的实际工作效果,我们需要在一个干净的Linux环境中进行以下实验。实验目标是手动调用CNI插件,观察网络命名空间的创建和配置过程。
确保系统中已安装以下组件:
1 | # 检查CNI插件目录 |
这个实验演示如何在没有Kubernetes的情况下,手动模拟PodSandbox的创建过程。
1 | # 创建一个新的网络命名空间 |
预期输出应该显示除了lo接口外没有其他网络接口,这证明我们有了一个干净的网络环境。
创建CNI插件的输入配置文件/tmp/cni-config.json:
1 | { |
CNI插件依赖一组标准环境变量来获取上下文信息:
1 | export CNI_COMMAND=ADD |
注意:CNI_NETNS必须是绝对路径。如果使用ip netns创建的命名空间,需要先绑定挂载:
1 | # 创建绑定挂载点 |
1 | # 调用CNI插件并捕获输出 |
预期的Result JSON输出应该包含:
1 | { |
1 | # 检查命名空间内的接口 |
这个实验演示容器与宿主机之间的网络连接是如何建立的。
1 | # 列出所有网络接口 |
1 | # 安装网桥工具(如果未安装) |
输出示例:
1 | bridge name bridge id STP enabled interfaces |
这证明了多个容器的veth对端都已正确插入到同一个网桥中,形成了二层交换网络。
CNI插件不仅负责创建网络,还需要正确处理删除操作。
1 | export CNI_COMMAND=DEL |
注意:DELETE命令通常不产生输出(成功的删除是静默的)。
1 | # 检查命名空间是否还存在 |
这个实验尽可能接近Kubernetes中Pod网络的实际创建流程。
1 | # 使用crictl或docker启动pause容器 |
其中sandbox-config.json包含:
1 | { |
1 | # 获取pause容器的PID |
1 | export CNI_COMMAND=ADD |
1 | # 在容器内执行网络命令 |
症状:调用CNI插件时报operation not permitted
原因:缺少CAP_NET_ADMIN能力
解决方案:
1 | # 使用sudo或以root身份运行 |
症状:failed to find bridge cbr0
原因:宿主机上没有创建指定的网桥
解决方案:
1 | # 手动创建网桥 |
症状:failed to allocate IP range
原因:IPAM配置的子网太小,可用IP已分配完
解决方案:
1 | # 检查已分配的IP |
通过这些实验,我们可以直观地观察到:
这些动手实验不仅能加深对理论知识的理解,还能培养在实际生产环境中诊断和解决网络问题的能力。建议读者在自己的测试环境中完整复现以上实验,并尝试修改参数观察不同配置的效果。