4、CNI(容器网络接口)规范:设计哲学、插件架构与标准操作
聊到容器网络,CNI 是个绕不开的话题。我个人觉得,理解 CNI 的设计哲学,比背几个命令重要得多。说白了,CNI 就是给容器插网线的标准流程。你想想看,Kubernetes 本身不负责网络实现,它只负责调度容器。那容器怎么联网?谁来管?CNI 就是那个「谁」。
4.1 CNI 的设计哲学:简单、可组合、无状态
CNI 的设计哲学,我总结为三个词:简单、可组合、无状态。
- 简单:CNI 规范本身非常轻量。它不定义网络策略、不定义 IPAM 细节,只定义「容器该连哪个网络」这个核心动作。我在项目中见过有人试图在 CNI 里塞进一大堆功能,最后搞得插件又重又难维护。嗯,这其实违背了 CNI 的初衷。
- 可组合:你可以把多个 CNI 插件串起来用。比如先用一个插件分配 IP,再用另一个插件把容器接入 bridge 网络。这种「搭积木」的方式,让网络方案非常灵活。
- 无状态:CNI 插件本身不保存状态。每次调用都是独立的。状态由容器运行时(比如 kubelet)或外部系统管理。这样做的好处是,插件崩溃了也不丢数据,重启就能恢复。
核心思想:CNI 只关心「把容器接入网络」和「把容器从网络断开」这两个动作。至于网络怎么规划、IP 怎么分配,那是插件自己的事。
4.2 插件架构:二进制调用与标准输入输出
CNI 的插件架构,说白了就是一个可执行的二进制文件。容器运行时(比如 kubelet)通过环境变量和标准输入,把参数传给插件。插件执行完后,通过标准输出返回结果。
我刚开始接触时觉得这方式有点「原始」。但后来发现,这种设计恰恰是它的优势——跨语言、跨平台。你完全可以用 Go、Rust、甚至 Python 写一个 CNI 插件,只要它能被调用、能输出 JSON 就行。
一个典型的 CNI 插件目录结构是这样的:
/opt/cni/bin/
├── bridge
├── host-local
├── flannel
├── calico
├── macvlan
└── loopback
每个文件都是一个独立的二进制。kubelet 会根据配置,按顺序调用它们。
个人经验:我曾经在生产环境遇到过一个问题——某个 CNI 插件版本不兼容,导致容器启动后网络不通。排查了半天,最后发现是插件二进制文件被覆盖了。从那以后,我习惯在部署前用 cni --version 检查一下插件版本。
4.3 标准操作:ADD / DEL / CHECK
CNI 定义了三个核心操作:ADD、DEL、CHECK。每个操作都有明确的输入和输出。
4.3.1 ADD:把容器接入网络
ADD 操作是 CNI 最核心的调用。当 kubelet 要启动一个 Pod 时,它会调用 CNI 插件的 ADD 方法。插件需要完成以下工作:
- 创建网络命名空间(如果还没有)
- 把容器网卡(veth pair 的一端)移入容器的网络命名空间
- 分配 IP 地址(通过 IPAM 插件)
- 配置路由和 ARP 表
ADD 操作的输入是通过环境变量传递的:
CNI_COMMAND=ADD
CNI_CONTAINERID=abc123
CNI_NETNS=/var/run/netns/abc123
CNI_IFNAME=eth0
CNI_ARGS=ignore_unknown=1
CNI_PATH=/opt/cni/bin
同时,标准输入会传入一个 JSON 格式的网络配置:
{
"cniVersion": "0.4.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"ipam": {
"type": "host-local",
"subnet": "10.88.0.0/16"
}
}
插件执行成功后,会输出一个 JSON 结果:
{
"cniVersion": "0.4.0",
"interfaces": [
{"name": "eth0", "mac": "0a:58:0a:f4:00:01", "sandbox": "/var/run/netns/abc123"}
],
"ips": [
{"version": "4", "address": "10.88.0.2/16", "gateway": "10.88.0.1"}
],
"dns": {}
}
注意:ADD 操作必须是幂等的。如果同一个容器被重复调用 ADD,插件应该返回相同的网络配置,而不是报错。我曾经遇到过某个插件在重复 ADD 时直接 panic,导致 Pod 一直处于 ContainerCreating 状态。
4.3.2 DEL:把容器从网络断开
DEL 操作是 ADD 的逆操作。当 Pod 被删除时,kubelet 会调用 DEL 来清理网络资源。DEL 操作需要:
- 删除容器网卡
- 释放 IP 地址
- 清理路由和 ARP 条目
- 删除网络命名空间(如果不再需要)
DEL 操作的输入和 ADD 类似,只是 CNI_COMMAND=DEL。插件执行成功后,不需要输出任何内容(空 JSON 即可)。
这里有个坑:DEL 操作必须能处理「资源已经被清理过」的情况。比如,容器已经被强制删除了,网络命名空间已经不存在了。这时候插件不能报错,应该静默退出。我见过有些插件在 DEL 时因为找不到网卡而直接返回错误,导致 kubelet 反复重试,最终把节点搞崩了。
4.3.3 CHECK:验证网络连通性
CHECK 操作是 CNI 0.4.0 版本引入的。它的作用是验证容器的网络配置是否仍然有效。kubelet 会定期调用 CHECK 来确保 Pod 的网络是健康的。
CHECK 操作需要检查:
- 容器网卡是否存在
- IP 地址是否仍然有效
- 路由是否配置正确
- 网关是否可达(可选)
如果 CHECK 失败,kubelet 会认为 Pod 的网络有问题,可能会触发重建。我个人建议,CHECK 操作不要做太重的检查,比如不要发起 ICMP ping。因为网络抖动是正常的,偶尔丢包不代表网络配置有问题。CHECK 应该只检查「配置是否存在」,而不是「网络是否通」。
避坑指南:我曾经在一个大规模集群中遇到过 CHECK 操作导致节点 CPU 飙升的问题。原因是某个 CNI 插件在 CHECK 时遍历了宿主机上所有的网络命名空间,而节点上运行了上千个 Pod。每次 CHECK 都要花好几秒,最终把 kubelet 拖垮了。后来我们给 CHECK 加了一个超时限制,问题才解决。
4.4 总结:CNI 规范的核心要点
| 操作 | 触发时机 | 核心职责 | 注意事项 |
|---|---|---|---|
| ADD | Pod 创建时 | 创建网卡、分配 IP、配置路由 | 必须幂等,重复调用返回相同结果 |
| DEL | Pod 删除时 | 清理网卡、释放 IP、删除路由 | 必须能处理资源已被清理的情况 |
| CHECK | 周期性健康检查 | 验证网络配置是否仍然有效 | 不要做重操作,避免影响性能 |
嗯,CNI 规范其实就这么点东西。但正是这种「小而美」的设计,让容器网络生态如此繁荣。你想想看,从 Flannel 到 Calico,从 Weave 到 Cilium,底层都遵循着同样的 ADD/DEL/CHECK 流程。理解了这些,你再看任何 CNI 插件的源码,都会觉得「哦,原来如此」。