深入理解 Kubernetes CNI (容器网络接口)
深入理解 Kubernetes CNI (容器网络接口)
在 Kubernetes (K8s) 集群中,网络是连接各个组件和应用的基础设施。为了实现灵活、可插拔的网络解决方案,Kubernetes 采用了容器网络接口(Container Network Interface, CNI)规范。CNI 是 K8s 网络模型得以实现的关键,它定义了一套标准接口,用于容器运行时动态地配置容器的网络。理解 CNI 的工作原理对于掌握 K8s 网络至关重要。
一、Kubernetes 网络模型的核心原则
在深入 CNI 之前,我们首先需要理解 Kubernetes 对集群网络的基本要求。其网络模型旨在提供一个扁平、易于理解和使用的网络环境,核心原则如下:
所有 Pod 能够不通过 NAT 就能相互访问:
- 含义: 集群内任何 Pod 都可以直接使用其他 Pod 的 IP 地址进行通信,无需网络地址转换。这极大地简化了服务发现和应用间通信。
- 实现: 通过 CNI 插件为每个 Pod 分配集群内唯一的 IP 地址,并确保这些 IP 地址在集群范围内是可路由的。底层网络插件(如 Flannel, Calico)利用 Overlay 网络(如 VXLAN)或路由技术(如 BGP)来实现跨节点的 Pod 间通信。
所有节点能够不通过 NAT 就能相互访问:
- 含义: 集群中的物理或虚拟节点之间可以直接通信。这对于 K8s 控制平面组件(如 API Server 与 Kubelet)的正常工作至关重要。
- 实现: 通常要求所有节点处于同一二层网络或通过路由、VPN 等方式确保节点间的 IP 连通性。
容器内看见的 IP 地址和外部组件看到的容器 IP 是一样的:
- 含义: Pod 内的应用看到的自身 IP 地址,与集群中其他 Pod 或节点看到的该 Pod 的 IP 地址是相同的。这避免了地址转换带来的复杂性。
- 实现: CNI 插件在 Pod 的网络命名空间内配置的 IP 地址即为其在集群网络中的真实 IP。
二、CNI 规范与接口
CNI 本身并不是一个具体的网络实现,而是一套规范和接口标准。它由 Cloud Native Computing Foundation (CNCF) 维护。
- 目的: 将容器运行时的网络设置逻辑与具体的网络实现解耦。容器运行时(如 containerd, CRI-O,或 Kubelet 内置的 Docker shim)只需按照 CNI 规范调用相应的插件,即可完成容器的网络配置,而无需关心底层使用的是 Flannel、Calico 还是其他网络方案。
- 接口形式: CNI 插件通常是可执行文件(二进制文件或脚本)。容器运行时通过执行这些文件,并通过环境变量传递参数(如容器 ID、网络命名空间路径、网络配置等),通过标准输入(stdin)传递 JSON 格式的网络配置,并通过标准输出(stdout)获取执行结果(如分配的 IP 地址信息)。
三、CNI 插件类型
CNI 插件生态丰富,通常可以分为几类:
- 主接口插件 (Interface Plugin): 负责创建具体的网络设备。
bridge
: 创建 Linux 网桥,并将容器连接上去。ipvlan
/macvlan
: 使用 L2/L3 层虚拟化技术将容器直接连接到主机的物理接口。ptp
: 创建 Veth Pair 设备对。- 特定网络方案插件:如
calico
,flannel
,weave-net
等插件,它们实现了更复杂的网络功能(如网络策略、路由分发等),但底层可能也会用到上述基础设备类型。
- IPAM 插件 (IP Address Management): 负责 IP 地址的分配和管理。
host-local
: 使用预先配置的地址段在本地节点上为容器分配 IP。dhcp
: 通过向 DHCP 服务器请求来获取 IP 地址。calico-ipam
,whereabouts
: 更高级的 IPAM 插件,支持跨节点协调、IP 池管理等。
- Meta 插件 / Chained 插件: 用于增强或修改现有网络配置,通常在主插件之后链式调用。
portmap
: 基于 iptables 实现主机与容器间的端口映射(类似docker run -p
)。bandwidth
: 使用 Linux TC (Traffic Control) 对容器进行带宽限制。firewall
: 使用 iptables/firewalld 为容器设置防火墙规则。tuning
: 调整网络接口的 sysctl 参数。
四、CNI 工作流程
CNI 插件的调用和执行遵循一套标准流程:
配置加载:
- 容器运行时(在 K8s 中通常是 Kubelet 通过 CRI 接口调用 containerd/CRI-O)查找 CNI 配置文件。默认目录是
/etc/cni/net.d/
。 - 配置文件是 JSON 格式,后缀可以是
.conf
(单个网络配置) 或.conflist
(插件链配置)。 - 运行时按文件名的字母顺序加载第一个配置文件作为默认网络配置。
.conflist
文件允许定义一个plugins
数组,指定按顺序调用的多个 CNI 插件。
- 容器运行时(在 K8s 中通常是 Kubelet 通过 CRI 接口调用 containerd/CRI-O)查找 CNI 配置文件。默认目录是
插件执行 (ADD/DEL 操作):
- 容器创建 (ADD): 当 Pod 启动时,容器运行时为 Pod 创建网络命名空间后,根据加载的配置,调用相应的 CNI 插件(可执行文件,默认在
/opt/cni/bin/
目录下查找)。- 运行时设置必要的环境变量(如
CNI_COMMAND=ADD
,CNI_CONTAINERID
,CNI_NETNS
,CNI_IFNAME
,CNI_PATH
等)。 - 运行时将 JSON 网络配置通过标准输入传递给插件。
- 插件执行网络设置:创建网络接口(如 veth pair)、将其一端移入容器网络命名空间、调用 IPAM 插件分配 IP、配置 IP 地址和路由、设置 DNS 等。
- 如果使用
.conflist
,前一个插件的执行结果(JSON 格式,包含 IP 配置等)会通过标准输入传递给下一个插件(作为prevResult
字段)。 - 插件将执行结果(成功时包含分配的 IP/MAC、DNS 等信息)以 JSON 格式输出到标准输出。
- 运行时设置必要的环境变量(如
- 容器销毁 (DEL): 当 Pod 删除时,运行时调用 CNI 插件执行清理操作。
- 环境变量
CNI_COMMAND=DEL
。 - 插件负责释放 IP 地址、删除网络接口、清理相关规则等。
- 对于插件链 (
.conflist
),DEL
操作必须按照ADD
操作的相反顺序执行。
- 环境变量
- 容器创建 (ADD): 当 Pod 启动时,容器运行时为 Pod 创建网络命名空间后,根据加载的配置,调用相应的 CNI 插件(可执行文件,默认在
关键参数:
--cni-bin-dir
: Kubelet (或容器运行时) 查找 CNI 插件可执行文件的目录。--cni-conf-dir
: Kubelet (或容器运行时) 查找 CNI 网络配置文件的目录。
示例配置 (/etc/cni/net.d/10-mycni.conflist
):
1 |
|
这个配置定义了一个名为 “mycni-network” 的网络,它按顺序执行三个插件:bridge
(负责核心网络连接和 IP 分配), portmap
(处理端口映射), 和 tuning
(调整内核参数)。
五、CNI 插件设计要点 (深入解析)
CNI 规范对容器运行时和插件的行为提出了一些关键要求,以确保互操作性和健壮性:
- 网络命名空间先行: 运行时必须在调用任何 CNI 插件之前,为容器创建好一个新的、隔离的网络命名空间。插件的工作是在这个命名空间内进行的。
- 配置决定插件: 运行时负责读取 CNI 配置文件,确定需要为当前容器调用哪些插件以及它们的执行顺序。
- JSON 配置: 网络配置采用标准化的 JSON 格式,易于读写和解析。
- 插件链顺序执行: 对于
.conflist
,ADD
操作必须按plugins
数组顺序执行,DEL
操作必须按相反顺序执行。前一个插件的ADD
结果作为后一个插件的输入。 - 生命周期管理 (ADD/DEL 对称性与幂等性):
- 一个成功的
ADD
最终应对应一个DEL
。 DEL
操作必须是幂等的。即多次调用DEL
应能正确处理(第一次清理资源,后续调用发现已清理则直接成功返回),以应对运行时或节点故障恢复场景。
- 一个成功的
- 并发控制:
- 运行时不能对同一个容器(由
ContainerID
标识)并行执行 CNI 操作。 - 运行时可以对不同的容器并行执行 CNI 操作。
- 运行时不能对同一个容器(由
- 唯一标识与状态管理:
ContainerID
是容器的唯一标识。- 需要存储状态的插件(如 IPAM)应使用网络名称、
ContainerID
和网络接口名 (IfName
) 组成的复合键来索引状态信息。
- 禁止重复 ADD: 运行时不能对同一个容器、同一个网络、同一个接口连续调用两次
ADD
(在没有DEL
的情况下)。
六、CNI 与主机网络及 Kube-proxy 的集成
CNI 插件不仅要配置 Pod 网络,还必须确保 Pod 能够与 Kubernetes 的服务发现机制(主要是 Service
和 Kube-proxy
)正确集成。
基础要求:
lo
插件- 所有 Pod 网络命名空间都需要一个启动的
lo
(loopback) 接口 (127.0.0.1
)。标准的 CNIlo
插件负责实现这一点。
- 所有 Pod 网络命名空间都需要一个启动的
与 Kube-proxy (iptables/IPVS 模式) 的协同
Kube-proxy
通过在宿主机上设置iptables
或IPVS
规则来实现Service
的负载均衡和转发。- 关键挑战: CNI 插件必须确保从 Pod 发出、目标是
Service
ClusterIP 的流量,以及从外部访问NodePort
或LoadBalancer
并需要转发到 Pod 的流量,能够被宿主机上的Kube-proxy
规则所处理。
场景一:使用 Linux 网桥的 CNI 插件 (如
bridge
插件, Flannel VXLAN)- 问题: 当 Pod 通过 veth pair 连接到宿主机上的 Linux 网桥时,如果源 Pod 和目标 Pod(或 Service 的后端 Pod)位于同一个节点且连接到同一个网桥,它们之间的流量可能仅在二层(网桥层面)转发,绕过宿主机的三层 IP 栈,从而错过
iptables/IPVS
规则的处理。这会导致Service
访问失败。 - 解决方案: 启用内核参数
net.bridge.bridge-nf-call-iptables=1
。- 该参数(位于
/proc/sys/net/bridge/bridge-nf-call-iptables
)强制让通过 Linux 网桥的数据包也经过宿主机的iptables
链(如PREROUTING
,FORWARD
,OUTPUT
)。 - 这样,即使是桥接流量,也能被
Kube-proxy
在nat
表或filter
表中设置的规则匹配和处理。 - 通常也需要设置
net.bridge.bridge-nf-call-ip6tables=1
(IPv6) 和检查br_netfilter
内核模块是否加载。
- 该参数(位于
- 图示:
(此图展示了 veth pair 连接到 cni0 网桥的典型结构)
- 问题: 当 Pod 通过 veth pair 连接到宿主机上的 Linux 网桥时,如果源 Pod 和目标 Pod(或 Service 的后端 Pod)位于同一个节点且连接到同一个网桥,它们之间的流量可能仅在二层(网桥层面)转发,绕过宿主机的三层 IP 栈,从而错过
场景二:不使用 Linux 网桥的 CNI 插件 (如 Calico BGP 模式, OVS-based 插件)
- 机制: 这类插件通常不依赖 Linux 网桥,而是直接在宿主机上配置路由规则,或者使用像 Open vSwitch (OVS) 这样的高级虚拟交换机。
- 路由方式 (如 Calico BGP): CNI 插件为每个 Pod 的 veth 接口配置
/32
路由,并通过 BGP 或直接写入内核路由表,确保宿主机知道如何将流量路由到目标 Pod(无论在本机还是其他节点)。由于流量必须经过宿主机的 IP 路由层,它自然会经过iptables/IPVS
的处理点。关键在于 CNI 插件必须正确配置路由。 - OVS 方式: 基于 OVS 的插件需要配置 OVS 流表,确保 Pod 流量能被正确导向宿主机网络栈(以应用
iptables/IPVS
规则)或直接转发。
- 路由方式 (如 Calico BGP): CNI 插件为每个 Pod 的 veth 接口配置
- 结论: 在这些场景下,通常不需要设置
bridge-nf-call-iptables
,因为流量路径本身就包含了 IP 层处理。重点在于 CNI 插件自身的路由或流表配置是否正确。 - 图示:
(此图可能示意了另一种网络连接方式,例如直接路由或通过其他虚拟交换技术)
- 机制: 这类插件通常不依赖 Linux 网桥,而是直接在宿主机上配置路由规则,或者使用像 Open vSwitch (OVS) 这样的高级虚拟交换机。
七、总结
CNI 是 Kubernetes 网络生态系统的基石。它通过一套简洁的规范,实现了网络配置的标准化和插件化,极大地增强了 Kubernetes 的灵活性和可扩展性。无论是选择 Flannel、Calico、Weave Net 还是其他网络方案,它们都遵循 CNI 规范与 Kubernetes 集成。理解 CNI 的工作原理、插件类型、设计要点以及它如何与 Kube-proxy 等组件协同工作,对于诊断网络问题、选择合适的网络方案以及深入理解 Kubernetes 本身都至关重要。