网络模型与 CNI:每个 Pod 一个 IP 背后的实现
Kubernetes 对网络做出了一个看似简单的承诺:每个 Pod 拥有一个集群范围内唯一的可路由 IP,Pod 之间无需 NAT 可直接通信,节点访问 Pod 也无需 NAT。三条规则,没有一条涉及实现细节。这种"定义行为、不定义机制"的风格与 Kubernetes 整体的设计哲学一致,但也意味着没有任何一行核心代码实现了这个承诺——它完全由 CNI 插件负责兑现。
理解 CNI 的切入点不是插件本身,而是 Linux 网络命名空间带来的问题。每个容器拥有独立的网络栈,包含独立的路由表、ARP 缓存、iptables 规则集。新建的命名空间里只有一个回环接口 lo,对外完全不可达——这正是容器隔离的本意。CNI 插件的全部工作就是在这种隔离性上凿出一条通道:创建 veth pair,把一端移入 Pod 命名空间,另一端挂到宿主机网桥,配置 IP 地址和路由,让 Pod 从完全隔离的孤岛变成集群网络里的一个节点。
跨节点通信是第二层复杂性。同节点内的 Pod 通过共享网桥即可互通,但当数据包需要离开宿主机的物理网卡,穿越真实的交换机和路由器,抵达另一台节点,再精准地找到目标 Pod 的网络命名空间,就需要更多机制的介入。不同 CNI 插件的核心差异恰在这里:Flannel 选择 VXLAN overlay,把 Pod 网络帧封装进 UDP;Calico 选择 BGP,把每个节点变成路由器,直接通告 Pod 子网;Cilium 选择 eBPF,在内核数据路径上直接完成转发,绕过 iptables 和 netfilter 的整个处理链路。
数据流全景
1 | |
Kubernetes 网络模型的三条规则
K8s 网络规范不规定任何实现,只规定行为约束,任何 CNI 实现必须满足这三条:
第一条,所有 Pod 之间通信不经过 NAT。Pod A 向 Pod B 发出的数据包,源 IP 和目的 IP 在整个路径上保持不变。这排除了 Docker 默认 bridge 模式里用宿主机 IP + 端口映射访问容器的模型——在那个模型里,容器 IP 在宿主机外不可见,跨主机容器通信需要在宿主机 IP 上做 DNAT/SNAT,源 IP 会变。K8s 明确禁止这种行为,要求 Pod IP 本身是集群范围可路由的一等公民。
第二条,节点到 Pod 的通信不经过 NAT。运维人员从节点上直接 curl <pod-ip> 可以正常工作,Pod 收到请求时看到的来源 IP 就是节点 IP,而不是某个经过 SNAT 变换后的地址。这条规则保证了集群内的 Pod 可以准确识别连接来源,对基于 IP 的访问控制和审计日志至关重要。
第三条,Pod 自己看到的 IP(ip addr 输出的 eth0 地址)与外部看到的 IP 相同。这条规则排除了"容器内部有一套私有 IP,外部通过另一套 IP 访问"的双映射情形,比如某些 NAT 穿透场景里容器不知道自己的公网 IP。K8s 要求 Pod 对自身 IP 的感知与真实路由一致。
三条规则合在一起,定义了一个扁平的三层网络语义。底层可以是 VXLAN overlay、BGP 直路由、eBPF 转发或任何其他技术,只要最终行为符合这三条,就是合法的 CNI 实现。实现者在三层规则之上可以自由选择技术路线,调用方(kubelet、scheduler、kube-proxy)无需关心底层网络实现细节。
CNI 规范:接口而非实现
CNI(Container Network Interface)是 CNCF 托管的一份轻量规范,核心是三个操作:ADD、DEL、CHECK。
ADD 操作:为容器分配网络资源。调用方传入容器 ID、网络命名空间路径(/var/run/netns/XXX)、网络接口名(通常是 eth0)以及 CNI 配置 JSON;插件在命名空间里创建网络接口,分配 IP,配置路由,把分配结果写回 stdout(包含 IP、网关、DNS 配置的结果 JSON)。ADD 的执行是同步的,完成后 Pod 网络即可用。
DEL 操作:释放容器的网络资源。Pod 被删除时调用,清理 veth 设备、IP 分配记录、路由规则等所有 ADD 时写入的内容。DEL 需要幂等——Pod 可能因为各种原因被多次调用 DEL,插件需要能正确处理"资源已不存在"的情形,而不是报错。
CHECK 操作:验证容器网络配置与 ADD 时的预期是否一致,供运行时健康检查使用。如果检查发现网络配置与预期不符(比如 veth 被意外删除),CHECK 返回错误,运行时可以触发修复流程。
调用约定是纯 Unix 进程约定,没有守护进程、没有 socket 通信。kubelet(通过容器运行时接口 CRI)在 /opt/cni/bin/ 里直接 exec 插件二进制,通过环境变量传递容器 ID、命名空间路径等参数,通过 stdin 传递配置 JSON,读取 stdout 的结果 JSON。插件执行时间计入 Pod 启动延迟;如果插件卡住,Pod 启动就会超时并重试。
CNI 配置文件放在 /etc/cni/net.d/,按字典序选第一个文件(或 conflist 文件)。配置支持插件链:一个 conflist 文件里列多个插件,按顺序调用,前一个的输出作为后一个的输入。主插件(ptp/bridge/macvlan)负责创建网络设备和分配 IP,辅助插件(portmap 做端口映射,bandwidth 做流量整形,firewall 做防火墙规则)在其后追加能力。这种链式组合使 CNI 在不修改主插件代码的前提下,可以叠加不同的网络能力。
kubelet 调用 CNI 的时机严格绑定在 Pod 沙箱的生命周期上:沙箱(pause 容器)被 CRI 创建后立刻调用 ADD,沙箱被删除前调用 DEL。pause 容器是一个极简的占位容器,它的唯一作用是持有 Pod 的网络命名空间,让其他业务容器可以加入同一个命名空间共享网络栈。CNI 看到的网络命名空间始终是 pause 容器的命名空间,而不是业务容器的命名空间。
Linux 网络原语:veth pair 与网桥
所有主流 CNI 插件都建立在两个 Linux 内核原语之上,理解这两个原语是理解整个 CNI 体系的基础。
veth pair 是 Linux 内核提供的虚拟网络设备对,行为像一段管道:数据从一端进,必从另一端出,双向传输。veth pair 天然成对存在,创建时就是两端。创建命令是 ip link add veth0 type veth peer name veth1,之后两个设备分别代表管道的两端。CNI 插件在 ADD 时创建一个 veth pair,把 eth0 这一端用 setns 系统调用移入 Pod 的网络命名空间,另一端(通常命名为 veth 加随机十六进制,如 veth3a7b9c)留在宿主机的默认网络命名空间。
从 Pod 内看,eth0 是一块独立的网卡,分配了 IP 地址,有自己的路由表。从宿主机看,veth3a7b9c 是一个普通的网络接口,Pod 发出的所有流量都从这里流出,进入宿主机的网络栈。两者之间的对应关系可以通过 /sys/class/net/eth0/iflink 里记录的接口序号来追踪——Pod 内的 eth0 会记录宿主机端的接口序号,反向也可以查。
Linux bridge 是内核实现的 L2 软件交换机,行为与物理以太网交换机相同:维护一张 MAC 地址 → 端口的转发表(FDB),根据目标 MAC 地址转发帧。CNI 插件在宿主机上创建一个名为 cni0 的网桥,把所有 Pod 的 veth 宿主机端都 attach 到这个网桥上。当 Pod A 向 Pod B 发送数据帧,帧从 Pod A 的 eth0 出来,经 veth 进入宿主机,到达 cni0 网桥;网桥查 FDB 找到 Pod B 对应的 veth 端口,把帧转发出去,进入 Pod B 的 veth 另一端,最终到达 Pod B 的 eth0。整个过程在 L2 完成,不经过 IP 路由层,延迟极低。
cni0 网桥本身被赋予一个 IP 地址(如 10.244.1.1/24),作为该节点上所有 Pod 的默认网关。Pod 内的路由表只有一条默认路由指向这个 IP,所有离开 Pod 的流量先到达网桥,由网桥决策:如果目标 MAC 是某个已知 Pod 的 MAC,在 L2 直接转发;如果目标不在本地,帧被发往网桥 IP 的三层路由处理,宿主机路由表决定下一跳。
网桥在内核里的数据路径非常短:帧进入 veth 后直接到达 bridge 核心代码,完成 FDB 查找和转发,无需经过完整的 netfilter 链路(虽然 iptables 的 br_netfilter 模块可以让桥接流量也经过 iptables 规则,这在 kube-proxy 需要拦截 Pod 间流量时很重要)。
IPAM:IP 地址管理
IP 地址管理(IPAM,IP Address Management)是 CNI 规范里的一个重要子概念。IPAM 负责回答"这个 Pod 应该分配哪个 IP"的问题,以及记录哪些 IP 已被分配、哪些可用。IPAM 本身也是一个可插拔的子插件,在 CNI 配置的 ipam 字段里指定。
host-local 是最简单的 IPAM 实现,在节点本地文件系统(通常是 /var/lib/cni/networks/)里记录已分配的 IP。每次 ADD 时,读取已分配列表,找到下一个可用 IP,分配给 Pod,写入记录文件(以 IP 地址为文件名,内容是容器 ID)。DEL 时删除对应的文件,释放 IP。host-local 只在单节点范围内工作,不同节点之间相互独立——只要各节点的 PodCIDR 不重叠,就不会冲突。Flannel 使用 host-local IPAM,因为 Flannel 已经为每个节点分配了不重叠的 /24 子网,host-local 只需在这个 /24 范围内分配即可。
calico-ipam 是 Calico 的 IPAM 实现,通过 Calico 的 IPAM 控制器在集群级别管理 IP 分配,支持跨节点的 IP 池(IPPool)管理、IP 地址块(block)的动态分配和回收,以及基于 label 的 IP 策略(某些 namespace 的 Pod 从特定 IP 池分配)。calico-ipam 通过 Kubernetes API 或 etcd 存储 IP 分配状态,确保跨节点的全局唯一性。一个 Calico IPPool 可以跨越整个集群,IPAM 控制器按需把 IP 块分配给各节点,节点需要时再从自己的块里分配给 Pod。这种两层分配模式(全局池 → 节点块 → Pod IP)避免了大规模集群下的分配冲突,同时减少了控制面的通信开销。
whereabouts 是一个独立的 IPAM 插件,支持跨节点的 IP 范围管理,用 etcd 或 Kubernetes API 做分布式状态存储,适合不使用特定 CNI 但需要跨节点 IP 唯一性保证的场景(如 SR-IOV 网卡直通)。whereabouts 的设计目标是做一个"厂商中立"的集群级 IPAM,可以与任何 CNI 主插件配合,只负责 IP 分配这一件事。
IPAM 分配失败是 Pod 启动失败的常见原因之一。当节点的 PodCIDR 耗尽(默认 /24 = 254 个可用 IP,减去网关等保留地址,约 252 个),后续 Pod ADD 调用会返回错误,Pod 停留在 ContainerCreating 状态。诊断方式:检查 kubectl describe pod 的 Events,看到 failed to allocate for range 0: no IP addresses available in range set: ... 即为 IPAM 耗尽。解决方式取决于 CNI 实现:Flannel 需要调整 PodCIDR 范围(kubeadm 的 --pod-network-cidr 参数),Calico 可以在线添加新的 IPPool。
Flannel VXLAN 模式深入
Flannel 是最早被广泛采用的 CNI 插件之一,设计目标是在任意 IP 网络上运行,不对底层网络做额外要求。只要节点间 UDP 8472 端口可通,Flannel VXLAN 就能工作。这种兼容性是 Flannel 最大的优势,也是它在早期 Kubernetes 采用率极高的原因。
Flannel 的守护进程 flanneld 启动时从 kube-apiserver 读取节点对象,为每个节点分配一个 Pod 子网(由集群的 --pod-cidr 总范围切分,如每个节点分配一个 /24 子网),把分配结果写回节点的 annotation(flannel.alpha.coreos.com/subnet)。flanneld 持续 watch 节点对象,感知新节点加入和旧节点退出。当新节点加入时,flanneld 更新本节点内核的转发规则,让本节点能访问新节点的 Pod 子网;当旧节点退出时,清理对应的规则。
VXLAN 的工作原理是把 L2 以太帧封装在 UDP/IP 报文里,通过 UDP 在 IP 网络上传输。封装格式:外层以太网头(14B)+ 外层 IP 头(20B)+ UDP 头(8B)+ VXLAN 头(8B)+ 原始 L2 帧。其中 VXLAN 头里的 VNI(VXLAN Network Identifier)用于区分不同的虚拟网络,Flannel 通常使用固定的 VNI 1。
每个节点上有一个 VTEP(VXLAN Tunnel Endpoint)设备,通常命名为 flannel.1。当宿主机路由表把目标 Pod 子网的流量引向 flannel.1,该设备需要知道"目标 VTEP 在哪个节点上"才能封装外层 UDP 目标 IP。flanneld 通过 Netlink 接口把这个映射关系写入内核的两张表:
第一张是 FDB 表(Forwarding Database)。FDB 里记录"目标 VTEP 的 MAC → 远端节点的 IP(隧道终点)"。当 flannel.1 需要封装一个发往远端 VTEP 的帧,先查 ARP 缓存得到目标 Pod 对应的 VTEP MAC,再查 FDB 得到该 MAC 对应的远端节点 IP,最后封装 UDP 包发出。
第二张是 ARP 缓存(邻居表)。ARP 缓存里记录"目标 Pod IP → 目标 VTEP 的 MAC"。这是一个静态的、由 flanneld 维护的 ARP 表项,而不是通过 ARP 广播动态学习的。flanneld 在得知某个 Pod 子网归属于某个节点后,在 ARP 表里预先写入该子网所有可能的 Pod IP → 目标 VTEP MAC 的映射(或者在收到第一次查询时写入)。
内核的 VXLAN 模块查这两张表完成封装,全程不进用户态。flanneld 不处理任何数据包,它只负责维护这两张内核表的状态。这与 kube-proxy 的职责模式完全对称:控制面守护进程维护内核规则,内核负责实际的数据面处理。
VXLAN 封装带来额外开销:每个数据包增加约 50 字节头部,实际有效 MTU 从 1500 降低约到 1450。CNI 配置里的 mtu 参数需要与此匹配,否则大包会在封装后超过物理 MTU,被分片(fragmentation),引发性能下降。Flannel 在配置里默认设置 "mtu": 1450,kubelet 会把这个值传给 Pod 内的 eth0。现代 Linux 内核(5.x+)对 VXLAN 有 hardware offload 支持,部分网卡可以在硬件层面完成 VXLAN 封装/解封装,大幅减少 CPU 开销。
Flannel 本身不实现 NetworkPolicy。需要网络策略能力时,需要叠加 Calico 的网络策略组件(Canal 方案,即 Flannel 负责网络连通 + Calico 的 Felix 负责策略执行),或者直接迁移到支持 NetworkPolicy 的 CNI(Calico/Cilium/Weave)。
Flannel 还有一种 host-gw 后端模式,完全不做封装:flanneld 在宿主机路由表里写入"目标 Pod 子网 via 目标节点 IP"的静态路由,数据包不经任何 VTEP,直接三层转发到对端节点。host-gw 性能接近裸金属,但要求所有节点在同一个 L2 域(能直接 ARP 互通),无法跨子网工作。
Calico BGP 模式深入
Calico 的核心设计思路是:不需要封装,直接用 IP 路由。每个节点运行 BIRD BGP 路由守护进程,通过 BGP 协议向其他节点(或上游路由器)通告本节点负责的 Pod 子网路由。
BGP(Border Gateway Protocol)本是互联网骨干路由协议,Calico 把它用于数据中心内部的 Pod 网络路由通告。默认情况下,所有节点之间建立 BGP 全互联(full mesh)——每对节点互相通告路由,节点数量 N 时连接数为 N*(N-1)/2。节点数量超过 50-100 时,全互联的连接数和路由通告量会成为瓶颈,通常需要在几个节点上部署路由反射器(Route Reflector,RR),让其他节点只与 RR 建立 BGP 会话,由 RR 集中转发路由通告。Calico 提供了 calico-node 的 RR 模式,也支持与外部路由器(如 ToR 交换机)建立 BGP peering,把 Pod 路由通告到数据中心的骨干网络。
数据包的跨节点路径在 Calico BGP 模式下:宿主机路由表有由 BIRD 注入的条目(标记 proto bird),如 10.244.2.0/24 via 192.168.10.2 dev eth0,直接把目标 Pod 子网的流量路由到对应节点的物理 IP;目标节点收到包,本地路由表有 10.244.2.3 dev cali-xxxxxxxx(Calico 用 cali- 前缀命名 veth),把包交给对应 Pod。整个路径里没有任何封装设备,每一跳都是标准的三层 IP 转发。
Calico 对底层网络有要求:节点之间必须能做任意 IP 路由。在同一个 L2 域(同一个 VLAN/交换机)里的节点可以直接路由;跨 L3 边界时,底层路由器需要学习到 Calico 的 Pod 子网路由(通过 BGP peering 与边界路由器建立会话)。在公有云 VPC 里,通常不允许自定义路由协议,节点也可能跨可用区,这时 Calico 会自动检测并回退到 IPIP 或 VXLAN 封装模式。IPIP 封装比 VXLAN 开销小(只增加 20 字节 IP 头),在需要封装的场景下是 Calico 的默认选择。
Calico 原生支持 NetworkPolicy,每个节点上的 Felix 守护进程 watch Kubernetes NetworkPolicy 和 Calico 自定义的 NetworkPolicy 资源,把策略转译为 iptables(或 eBPF)规则链写入内核。Felix 的同步是增量的:策略变更只更新对应的 iptables 链,不需要全量重写所有规则,在策略规则多时效率明显优于 kube-proxy 的全量同步。Calico 的 NetworkPolicy 还扩展了 Kubernetes 原生 NetworkPolicy 的表达能力:支持 FQDN(基于域名的出口策略)、支持基于 ServiceAccount 的策略、支持全局策略(GlobalNetworkPolicy,跨命名空间生效)。
Calico 在宿主机上还会为每个 Pod 创建一个 proxy_arp 接口(将对应 veth 的 proxy_arp 设为 1),使得宿主机代理 Pod 的 ARP 请求。Pod 内只有一条默认路由指向网关地址(通常是 169.254.1.1,一个 link-local 地址),这个地址实际上被 veth 宿主机端的 proxy_arp 响应,从而把 Pod 的所有出口流量都引导到宿主机路由表。这种设计不需要网桥设备(cni0),减少了一层 L2 转发。
Cilium eBPF 模式深入
Cilium 代表了最新一代的 CNI 实现路径。它不依赖 iptables,而是把网络处理逻辑写成 eBPF 程序,加载到内核的 TC(Traffic Control)钩子和 XDP(eXpress Data Path)钩子上,在内核数据路径里直接完成路由、NAT、连接追踪、负载均衡等操作,完全绕过了 netfilter/iptables 的处理链路。
eBPF(extended Berkeley Packet Filter)的核心机制是:程序被编译成字节码,经过内核的 Verifier 做安全验证(确保不存在无限循环、越界访问等危险操作),再由 JIT(Just-In-Time)编译器转为原生机器指令执行。eBPF 程序加载到内核后,运行时性能接近内核模块,但不需要修改内核代码,且可以动态加载和更新——Cilium 可以在不重启节点的情况下升级网络策略和转发逻辑。
eBPF Map 是 eBPF 程序与用户态之间共享的数据结构,存储在内核内存里,支持多种类型(哈希表、数组、LRU 哈希等)。Cilium 用 eBPF Map 存储路由信息(目标 Pod IP → 目标 veth 接口)、Service 到 Pod 的映射、网络策略规则、连接追踪状态等,所有数据结构都在内核内存里,数据路径处理时无需用户态到内核态的切换。
Cilium 的转发路径具体分为两条:
TC 钩子(Traffic Control hook)挂在网络设备的 ingress/egress 路径上,每个 veth 接口的 tc ingress 和 tc egress 都可以挂 eBPF 程序。数据包进入 veth 时,tc ingress 程序做策略检查(NetworkPolicy)、路由查找(目标是本节点 Pod 还是远端);数据包离开 veth 时,tc egress 程序做 SNAT/DNAT 处理。对于同节点 Pod 间通信,Cilium 可以通过 redirect 指令在两个 veth 之间直接转发,完全绕过 IP 路由层,延迟极低。
XDP 钩子(eXpress Data Path)挂在网卡驱动层,是最早的数据包处理点,比 TC 还要早。Cilium 把 XDP 用于 NodePort 和 LoadBalancer 的快速路径:外部包进入网卡后,XDP 程序直接做 DNAT,把目标 IP 改写为 Pod IP,再发给本地 Pod,全程不经过内核网络栈的其余部分,延迟极低。
与 iptables 相比,Cilium 的优势体现在三个维度:查找效率(eBPF Map 哈希表 O(1) vs iptables 链遍历 O(n));同步效率(eBPF Map 支持原子更新 vs iptables 全量重写);协议感知能力(eBPF 程序可以解析 HTTP/gRPC/Kafka 应用层协议,实现 L7 策略,iptables 只能处理 L3/L4 字段)。
Cilium 还提供 Hubble,一个利用 eBPF 收集连接事件的可观测性层,提供 Pod 间流量的实时拓扑、请求延迟分布、错误率,不需要在应用里注入任何 sidecar。Hubble Relay 把各节点的 Hubble 数据汇聚成集群级别的视图,Hubble UI 提供交互式的服务依赖图。在需要服务网格可观测性但不想引入 Istio 复杂度的场景里,Cilium + Hubble 是一个有竞争力的替代方案。
Cilium 对内核版本要求较高(建议 Linux 5.4+,部分特性需要 5.10+ 或 5.15+),且 eBPF 的调试工具(bpftool、bpftrace)与传统网络工具(tcpdump、iptables -L)不同,团队需要额外的知识投入。bpftool prog list 可以列出所有加载的 eBPF 程序,bpftool map dump 可以查看 eBPF Map 的内容,cilium bpf ct list global 可以查看 Cilium 的连接追踪表。
可运行实验
以下实验需要一个 kind 集群(kind create cluster)和节点访问权限(通过 docker exec kind-control-plane)。
1 | |
实验结果映射
ip link show type veth 列出的每个接口对应一个运行中的 Pod。数量应与 kubectl get pods -A --field-selector=status.phase=Running | wc -l 的 Pod 数量基本一致(包含系统 Pod)。
ip link show master cni0 列出挂载到网桥的所有接口。同一节点上所有 Pod 的 veth 宿主机端都出现在这里,每个接口对应一个 Pod。网桥充当节点上 Pod 间的"局域网交换机"。
/sys/class/net/eth0/iflink 里读到的数字是 veth pair 宿主机端的接口序号(ifindex)。在宿主机上 ip link show | grep "^<ifindex>:" 可以找到对应的接口名,确认 Pod 的 eth0 与宿主机上的哪个 veth 是一对。这是在不依赖任何额外工具的情况下追踪 veth pair 对应关系的方法。
ip route 在宿主机上的输出里,10.244.X.0/24 dev cni0 是本节点 Pod 子网的路由(流量留在本地,走网桥),10.244.Y.0/24 via X.X.X.X dev flannel.1(Flannel 模式)或 10.244.Y.0/24 via X.X.X.X dev eth0(Calico BGP 模式)是其他节点 Pod 子网的路由。这两类路由的区分就是 CNI 插件跨节点转发机制的直接体现——前者不离开本节点,后者需要跨物理网络。
tcpdump 在 cni0 上抓到的 ICMP 包,源 IP 和目标 IP 与 Pod IP 完全一致,没有任何 SNAT/DNAT 的痕迹,直接验证了 K8s 网络模型的"无 NAT"承诺。如果在 eth0 上同时抓包(Flannel VXLAN 模式),能看到 VXLAN 封装的 UDP 报文——外层是节点 IP,内层才是 Pod IP,对比两个接口的抓包结果可以直观感受封装的工作方式。
/var/lib/cni/networks/ 里的文件是 host-local IPAM 的分配记录,每个文件名是已分配的 IP,内容是占用该 IP 的容器 ID。删除一个 Pod 后,对应的文件被删除,IP 被释放。如果 kubelet 异常终止(节点断电、OOM kill),host-local 的文件可能残留,导致 IP 泄漏(该 IP 无法再分配,直到手动清理或重启 kubelet 触发 CNI DEL)。
模式提炼
CNI 是"网络命名空间的注册中心"。Pod 被创建时,kubelet 触发 CNI ADD,插件把 Pod 的网络命名空间注册进集群网络拓扑;Pod 被删除时,kubelet 触发 CNI DEL,插件注销这个命名空间。整个过程是同步的、事务性的:ADD 失败则 Pod 创建失败,DEL 失败则留有资源泄漏。
这种插件化设计把网络实现从 Kubernetes 核心完全解耦,让不同环境使用最适合自己的网络方案,而对上层的 Pod 调度和 Service 访问保持透明。调度器决定 Pod 放在哪个节点时,只需要知道节点剩余容量和 PodCIDR,不需要了解该节点用的是 VXLAN 还是 BGP。
三个主流 CNI 的取舍:兼容性(Flannel 最好,任意 IP 网络均可工作)、性能上限(Calico BGP 无封装在 IDC 可控网络里最好,Cilium eBPF 在高并发短连接下优势最显著)、能力广度(Cilium 最广,L7 策略、完整可观测性、kube-proxy 替代、服务网格能力一体化)。三个维度彼此制衡,没有在所有场景下同时占优的选项。
工程迁移表
| 传统技术 / Docker 概念 | Kubernetes CNI 对应物 | 差异说明 |
|---|---|---|
| Docker 默认 bridge(172.17.0.0/16,单节点,有 SNAT) | Pod 网络(集群范围 IP,无 NAT) | K8s Pod IP 跨节点可路由,Docker 默认不可路由 |
| Docker overlay(Swarm 内置 VXLAN) | Flannel VXLAN | 概念相同,Flannel 由 kube-apiserver 协调,不依赖 Swarm |
Linux bridge docker0 |
cni0 网桥 |
角色相同,CNI 创建,名称可配置 |
| OVS(Open vSwitch) | 部分 CNI 插件(如 OVN-Kubernetes) | OVS 提供可编程流表,用于更复杂的 SDN 场景 |
| 手写 iptables 规则 | NetworkPolicy 对象(由 CNI 实现) | CNI 把声明式策略转为内核规则,无需手动维护 |
| IPAM(DHCP 或静态分配) | CNI IPAM 子插件(host-local / calico-ipam) | 集群级 IP 分配,防止跨节点 IP 冲突 |
| traceroute 路径诊断 | cilium monitor / hubble observe |
CNI 层面的流量可见性,比 traceroute 更精准 |
| VMware vSwitch 端口组 | cni0 网桥 + veth pair | 虚拟化层做的隔离转移到内核命名空间 |
常见误解
误解一:Kubernetes 安装好就有网络,不需要额外配置。
这个误解在初学者中极为常见。kubeadm 初始化的裸集群,如果不手动安装 CNI 插件,CoreDNS Pod 会停在 Pending 状态,任何新建的 Pod 都无法获得 IP,网络完全不通。kind 之所以能直接用,是因为 kind 默认安装了 kindnet(一个轻量 CNI)。生产集群使用 kubeadm 时,kubeadm init 完成后必须紧接着安装 CNI——这步骤在官方文档里写得很清楚,但常被教程省略,导致初学者困惑。
误解二:Pod IP 重启后不变,可以在应用配置里硬编码 Pod IP。
Pod IP 与 Pod 对象的生命周期绑定,不与容器进程绑定。Pod 被删除再重建(Deployment 滚动更新、节点故障导致 Pod 迁移、kubectl delete pod 手动重建),新 Pod 会从 IPAM 重新分配地址,通常与原来不同。StatefulSet 保证 Pod 名称稳定(web-0、web-1),但同样不保证 IP 稳定。需要稳定地址的场景应通过 Service ClusterIP 或 Headless Service 的 DNS 名访问,不要直接依赖 Pod IP。
误解三:Calico 一定比 Flannel 快,Cilium 一定比 Calico 快。
性能对比高度依赖场景和基础设施环境。在节点间有直接三层路由的环境(IDC 可控网络),Calico BGP 无封装确实在吞吐量上有优势(约 5-15%),延迟也更低;但在公有云 VPC 里,Calico 也要走 IPIP/VXLAN,差距大幅缩小。现代内核(5.x+)上的 VXLAN 有硬件 offload 支持,Flannel 的封装开销远低于早期版本。Cilium 在高并发短连接场景(每秒大量新建 TCP 连接)下,因为 eBPF socket 级负载均衡减少了 conntrack 开销,延迟优势最显著;但在长连接、大吞吐场景下,三者差距可能在测量误差范围内。选型时,性能往往不是第一决策因素——运维团队的熟悉程度、网络策略需求、可观测性要求、底层网络约束,通常更具决定性。
练习
练习一:在 kind 多节点集群里,找到两个分别在不同节点上的 Pod,在宿主机的 cni0 接口和物理 eth0 接口上同时 tcpdump,然后让两个 Pod 互相 ping。观察 cni0 上抓到的是 Pod IP 的 ICMP 包,eth0 上抓到的是封装后的 VXLAN UDP 包(Flannel 模式)或原始 ICMP 包但外层是节点 IP(Calico BGP 模式)。对比两个接口的输出,理解封装层次。
练习二:用 ip netns exec 手动模拟 CNI ADD 的过程:创建一个网络命名空间,创建一个 veth pair,把一端移入命名空间并命名为 eth0,把另一端 attach 到 cni0 网桥,在命名空间内配置 IP 地址和默认路由,然后从宿主机 ping 这个命名空间的 IP。整个过程用不超过 10 条 ip 命令完成,理解 CNI 的本质是一组 ip 命令的自动化。
练习三:阅读 kindnet 的源码(GitHub 上约 500 行 Go 代码),找到它调用 CNI ADD 的位置,理解 kubelet → CRI → CNI 的调用链。对比 Flannel 的 flanneld 源码,找到它通过 Netlink 写入 FDB 表的代码(netlink.NeighSet),理解守护进程如何维护内核的转发表而不自己处理数据包。
练习四:在使用 Calico 的集群里,用 ip route show proto bird 查看 BIRD 注入的路由,找到某个远端节点 Pod 子网的路由条目,用 traceroute <pod-ip> 验证数据包只经过节点 IP 中转,没有任何 VTEP 中间设备。再对比 Flannel 集群的 traceroute 输出,理解有封装(VTEP 出现在路径里)和无封装(只有节点 IP)模式的实际路径差异。
NetworkPolicy 与流量隔离
CNI 插件负责连通性,NetworkPolicy 负责隔离性,两者是互补而非对立的关系。Kubernetes 的 NetworkPolicy 对象描述"哪些 Pod 之间允许通信",CNI 插件负责把这个声明转化为内核规则执行。
NetworkPolicy 的默认行为是"全通":集群里没有任何 NetworkPolicy 时,所有 Pod 之间、Pod 与 Service 之间、Pod 与外部之间都可以任意通信。一旦某个 Pod 被至少一条 NetworkPolicy 的 podSelector 选中,该 Pod 就进入受管控状态——所有不被任何 NetworkPolicy 显式允许的流量都被拒绝(白名单模式)。
一条 NetworkPolicy 的结构包含三部分:podSelector(选中被保护的 Pod 集合)、policyTypes(声明控制入方向 Ingress、出方向 Egress 还是两者)、以及 ingress/egress 规则列表(列出允许的来源或目标)。ingress 规则里可以用 from 字段组合三种选择器:namespaceSelector(允许来自特定命名空间的 Pod)、podSelector(允许来自满足标签条件的 Pod)、ipBlock(允许来自特定 CIDR 的流量)。三种选择器可以组合:同一条 from 里的多个字段是 AND 关系,from 列表里的多条规则是 OR 关系。
1 | |
NetworkPolicy 的执行完全由 CNI 插件负责,Kubernetes 核心代码不执行任何策略。Flannel 不实现 NetworkPolicy——部署了 Flannel 的集群里,NetworkPolicy 对象可以创建,但没有任何效果,所有流量仍然畅通。Calico(Felix 组件)和 Cilium 都实现了 NetworkPolicy,并且扩展了原生规范的能力。
Calico 的扩展:GlobalNetworkPolicy(集群范围,不限命名空间)、按 ServiceAccount 匹配的策略(比按 Pod 标签更安全,SA 与应用身份绑定更紧密)、基于 FQDN 的出口策略(允许 Pod 访问 *.s3.amazonaws.com,拒绝其他外部域名)。Felix 把 NetworkPolicy 翻译为 iptables 链,策略规则多时采用 ipset 优化:不是逐条 IP 匹配,而是把允许的 IP 集合放进 ipset,iptables 规则只需匹配 ipset 名,O(1) 查找。
Cilium 的 L7 NetworkPolicy:eBPF 程序可以解析 HTTP,允许按 HTTP 方法(GET/POST)、URL path、Header 内容做策略,甚至可以解析 gRPC 的 service/method。这是传统 iptables 根本无法实现的能力——iptables 只能看到 TCP 的源/目标端口,无法看到 HTTP 层的内容。Cilium 的 L7 策略让零信任安全在 Kubernetes 内部可以精确到 API 粒度,而不仅仅是 IP 粒度。
NetworkPolicy 的常见陷阱:一是忘记放行 DNS,导致 Pod 无法解析服务名;二是 policyTypes 里声明了 Egress 但 egress 规则列表为空,等价于"拒绝所有出站流量",会把 Pod 的一切对外通信切断;三是 namespaceSelector 和 podSelector 写在同一个 from 条目里(AND)与分开写两个条目(OR)行为完全不同。
CNI 选型决策框架
三种主流 CNI 的选型应从以下几个维度评估:
底层网络约束是第一优先级。如果节点部署在公有云 VPC 里,且 VPC 不支持自定义路由协议(大多数 VPC 不支持 BGP),Calico 纯 BGP 模式无法工作,必须配置 IPIP 或 VXLAN overlay。如果底层是可控的 IDC 网络,节点间三层可达,Calico BGP 无封装模式性能最优。如果是边缘节点或混合云,网络异构性高,Flannel VXLAN 的兼容性最好。
NetworkPolicy 需求是第二优先级。如果业务需要 L7 策略(按 HTTP 路径隔离)、FQDN 出口控制(限制 Pod 能访问的外部域名)、或 mTLS 等服务网格能力,Cilium 是唯一选择。如果只需要标准 NetworkPolicy(L3/L4),Calico 和 Cilium 都能满足,Flannel 需要搭配 Calico。如果完全不需要网络策略,Flannel 是最简单的选项。
运维团队能力是第三优先级。Flannel 最简单,故障排查用 tcpdump + iptables -L 等传统工具。Calico 中等复杂度,需要了解 BIRD BGP 守护进程和 Felix 的调试方式(calicoctl node status、calicoctl get node -o yaml)。Cilium 复杂度最高,需要学习 eBPF 调试工具(bpftool、cilium monitor、hubble observe),但换来的是最强的可观测性。
集群规模是第四优先级。小于 50 节点时,三者差距可以忽略。50-200 节点时,Calico BGP 的全互联连接数开始增多,需要规划 Route Reflector。超过 200 节点时,Cilium 的 eBPF 数据路径优势开始显现,iptables 规则数量也开始成为 kube-proxy 的瓶颈,此时 Cilium kube-proxy 替代是自然选择。超过 1000 节点的超大集群,Cilium 几乎是唯一经过充分验证的选项(Google GKE 的 Dataplane V2 底层就是 Cilium)。
CNI 迁移的风险与节奏:从 Flannel 迁移到 Calico 或 Cilium,需要替换所有节点上的 CNI 配置,通常需要节点滚动重启或 drain/cordon 操作。迁移期间,新旧 CNI 不能共存于同一节点(配置文件冲突),但不同节点可以短暂处于不同 CNI 状态(跨节点通信在切换期间可能不稳定)。生产环境迁移时,推荐在低流量时段,按批次替换节点,每批次迁移后验证网络连通性再继续。如果可以,使用蓝绿节点池(新建配置 Calico/Cilium 的节点池,逐步把 Pod 驱逐到新节点池,旧节点池缩容至零)是最安全的迁移策略,切换期间两套节点池并存,失败时可以快速回滚。
可观测性对比:Flannel 和 Calico 在传统工具(tcpdump、iptables -L、ip route)的调试体验上几乎相同,学习成本低。Cilium 额外引入了 cilium monitor(实时查看 eBPF 事件流:哪个 Pod 发包、命中哪条策略、是否被丢弃)和 Hubble(基于 eBPF 的网络可观测性平台,提供服务依赖拓扑、请求延迟分位数、L7 请求统计)。Hubble 能在不修改应用代码的前提下,提供接近服务网格的可观测性深度,这是传统 CNI 无法比拟的。
CNI 故障排查思路
网络问题是 Kubernetes 故障中最难定位的一类,因为数据路径跨越多个抽象层(应用、Service、kube-proxy 规则、CNI 插件、内核网络栈、物理网络),每一层都可能是问题所在。建立分层排查的思路比记忆具体命令更重要。
第一步,确认 Pod 自身网络配置正确。进入 Pod 内部(kubectl exec -it <pod> -- sh),检查 ip addr show eth0(确认有 IP 地址且地址在预期的 PodCIDR 范围内)、ip route(确认有默认路由指向网关,通常是 cni0 的 IP)、cat /etc/resolv.conf(确认 nameserver 指向 CoreDNS 的 ClusterIP)。这三项都正确,说明 CNI ADD 成功执行,Pod 的基础网络配置无误。
第二步,测试同节点 Pod 间通信。从 Pod A ping 同一节点的 Pod B(kubectl exec podA -- ping <podB-ip>),如果不通,问题在节点内的 L2 转发(网桥 FDB 表、veth pair 状态)。检查 ip link show master cni0 确认 Pod 的 veth 宿主机端已挂载到网桥,bridge fdb show dev cni0 查看 FDB 表,arp -n 查看 ARP 缓存。
第三步,测试跨节点 Pod 间通信。从 Pod A ping 另一个节点上的 Pod B,如果同节点通了但跨节点不通,问题在跨节点转发机制。Flannel 模式下检查 flannel.1 VTEP 是否正常(ip link show flannel.1)、FDB 表是否有远端节点的条目(bridge fdb show dev flannel.1)、flanneld 是否正常运行(kubectl get pod -n kube-flannel)。Calico 模式下检查 BIRD 是否建立了 BGP 会话(calicoctl node status)、路由表是否有对端节点的 Pod 子网路由(ip route show proto bird)。
第四步,测试 DNS 解析。kubectl exec <pod> -- nslookup kubernetes,如果 DNS 不通,先确认 CoreDNS Pod 是否正常运行(kubectl get pod -n kube-system -l k8s-app=kube-dns),再从测试 Pod ping CoreDNS 的 ClusterIP,然后用 dig @<coreDNS-clusterIP> kubernetes.default.svc.cluster.local 直接测试 DNS 查询,逐步缩小故障范围。
第五步,测试 Service 访问。kubectl exec <pod> -- curl http://<service-clusterIP>:<port>,如果 Pod 直通 OK 但 Service 不通,问题在 kube-proxy 规则。检查 iptables -t nat -L KUBE-SERVICES -n | grep <clusterIP> 确认规则存在,kubectl get endpoints <svc-name> 确认有就绪端点,kubectl logs -n kube-system <kube-proxy-pod> 查看同步错误。
tcpdump 是贯穿所有步骤的利器。在 cni0 上抓包可以看到节点内的所有 Pod 流量;在 flannel.1 或 eth0 上抓包可以看到跨节点的封装流量;在 Pod 内的 eth0 上抓包可以看到 Pod 视角的原始流量。对比不同接口的抓包结果,可以精确定位数据包在哪一层消失。
常见的 CNI 故障现象与原因对照:Pod 创建后 IP 地址是 <none> 或 1.1.1.1(CNI ADD 调用返回错误,检查 kubelet 日志和 /var/log/pods/ 里的 CNI 日志);Pod IP 分配了但同节点 ping 不通(veth pair 未挂载到 cni0,或 cni0 本身 down,ip link show cni0 验证);跨节点 ping 不通但同节点正常(路由或 VXLAN FDB 表缺失,Flannel 模式下优先检查 flanneld Pod 是否崩溃);DNS 解析超时(CoreDNS Pod 本身不正常,或 ndots:5 触发了不必要的 search 域展开导致查询时序问题,kubectl logs -n kube-system -l k8s-app=kube-dns 查看错误);跨命名空间通信被拒绝(NetworkPolicy 默认拒绝规则生效,kubectl get networkpolicy -A 排查)。
CNI 升级与版本管理也是生产运维中容易忽视的点。CNI 插件的版本与 Kubernetes 版本有兼容矩阵,升级 Kubernetes 时需要同步检查 CNI 插件的兼容性(如 Calico 3.x 对应 K8s 1.24+,Cilium 1.14+ 对应 K8s 1.26+)。CNI 配置文件(/etc/cni/net.d/)在节点上直接生效,修改后对新建 Pod 立即生效,对已运行 Pod 不影响(已建立的 veth 对和路由不会重建)。升级 Calico 或 Cilium 时,推荐先在非生产节点验证,再滚动更新 DaemonSet,每个节点的升级过程中该节点上已有 Pod 的网络不受影响,但新建 Pod 可能短暂出现 CNI 不可用的窗口期(通常小于 30 秒)。
IP 地址规划是集群网络长期健康的基础。Pod CIDR 规划不足是扩容时最头疼的问题之一——一旦节点数增多,每个节点的 PodCIDR 子网用完,新 Pod 就无法获得 IP。Kubernetes 通过 --cluster-cidr 给整个集群分配 Pod IP 段,--node-cidr-mask-size 决定每个节点分到的子网大小(默认 /24,即每个节点最多 254 个 Pod IP)。如果节点数超过了集群 CIDR 能容纳的 /24 子网数量(例如集群 CIDR 是 /16,最多支持 256 个节点),就需要把 node-cidr-mask-size 调大(例如 /25,每节点 128 个 Pod IP),或者扩展集群 CIDR 范围。修改已有集群的 CIDR 是高风险操作,通常需要重建集群——因此 IP 地址规划应在集群初始化时就按照预期最大规模的 2-3 倍预留空间。
多网卡节点(节点有多个网络接口)场景下,CNI 需要明确知道使用哪块网卡作为 Pod 流量的出口网卡。Flannel 通过 --iface 参数指定,Calico 通过 IP_AUTODETECTION_METHOD 环境变量(支持 first-found、can-reach=<IP>、interface=eth0 等模式)指定,Cilium 通过 --direct-routing-device 指定。指定错误的出口网卡会导致跨节点流量从错误的接口发出,是多网卡环境里最常见的 CNI 配置问题。
Kubernetes 1.28 引入的 NetworkPolicy v2 功能扩展(仍在 alpha 阶段)将进一步完善网络策略模型,计划加入 AdminNetworkPolicy(集群管理员级别的全局策略,优先于命名空间级别的 NetworkPolicy)和 BaselineAdminNetworkPolicy(默认允许/拒绝策略,在没有命名空间级策略时生效)。这两个对象的引入是为了解决多租户场景下,平台管理员无法用标准 NetworkPolicy 强制实施全局安全基线的问题——目前 Calico 的 GlobalNetworkPolicy 和 Cilium 的 ClusterwideCiliumNetworkPolicy 各有私有实现,AdminNetworkPolicy 是 SIG Network 推进的标准化尝试。
eBPF 对 CNI 生态的影响远不止 Cilium 一家。Calico 从 3.18 版本开始引入 eBPF 数据路径(与 Felix 传统 iptables 路径并列,可切换),在保留 Calico BGP 控制平面和 NetworkPolicy 生态的同时,获得 eBPF 的性能优势。这意味着 CNI 的竞争格局正在从"谁用 eBPF"转向"eBPF 用得有多深"——Cilium 的 eBPF 覆盖了从 L3/L4 转发到 L7 策略的完整路径,而 Calico eBPF 目前主要替换数据路径的转发部分,控制平面仍沿用 Felix。
WireGuard 加密是 CNI 领域近年来采用率上升最快的特性之一。Calico 从 3.13 版本支持 WireGuard 节点间加密(calico-node 自动管理 WireGuard 密钥对,无需手动配置),Flannel 的 WireGuard 后端从 0.14 版本提供,Cilium 同样支持 WireGuard 作为跨节点加密选项。WireGuard 相比传统 IPsec 的优势是配置极简(内核原生支持,无需 IKE 守护进程)、性能更高(基于现代密码学算法 ChaCha20-Poly1305)、密钥自动轮换。在合规要求传输层加密的场景(金融、医疗),WireGuard CNI 加密是比服务网格 mTLS 更轻量的选择,因为它在 L3 层透明加密,不需要修改应用或注入 sidecar。
不同 CNI 在大规模场景下的控制平面压力也值得关注。Flannel 的控制平面极简,flanneld 只需要从 etcd 或 kube-apiserver 读取节点子网信息,控制平面负载极低,即使 1000+ 节点也不会成为瓶颈。Calico 的 Felix 需要监听所有节点的 WorkloadEndpoint、NetworkPolicy 变化,在策略复杂、Pod 数量多的集群里,Felix 的 CPU 占用可能显著上升;Calico Typha 组件(介于 kube-apiserver 和 Felix 之间的缓存代理)是 100+ 节点时的推荐配置,能大幅减少 Felix 对 apiserver 的直接 watch 连接数。Cilium 的 Operator 和 Agent 在大规模集群里同样需要关注内存用量,eBPF Map 的大小有内核限制,需要根据集群 Pod 密度调整 --bpf-ct-global-any-max 等参数。
CNI 插件的健康状态监控应当纳入集群监控体系。Calico 提供 Prometheus 指标端点(calico-node 的 9091 端口),暴露 Felix 的策略同步延迟、数据路径错误计数、BGP 会话状态等指标。Cilium 的指标更为丰富,cilium-agent 在 9962 端口暴露数百个 Prometheus 指标,涵盖 eBPF Map 使用率、策略执行延迟、DNS 代理请求计数、Hubble 流量统计等。Flannel 指标相对简单,主要关注 flanneld 进程存活和 VXLAN 封包计数。将 CNI 指标接入 Prometheus + Grafana 告警体系,是生产集群网络可靠性保障的基础建设。
系列导航
- 00 核心概念导读
- 01 API Server 与声明式 API
- 02 etcd 与持久化
- 03 控制器模式与 Informer 机制
- 04 Pod 生命周期
- 05 调度器深入
- 06 kubelet 与容器运行时
- 07 网络模型与 CNI
- 08 Service 与 kube-proxy
- 09 Ingress 与 Gateway API
- 10 存储体系
- 11 配置与密钥管理
- 12 RBAC 与安全模型
- 13 资源管理与自动伸缩
- 14 CRD 与 Operator 模式
- 15 Helm 与应用打包
- 16 可观测性
- 17 生产集群运维
参考资料
- CNI Specification — CNI 接口规范原文,ADD/DEL/CHECK 三个操作的完整定义和 JSON 格式约定
- Kubernetes Networking Model — K8s 官方文档对网络模型三条规则的权威表述
- Flannel Backends — Flannel 各后端模式(VXLAN/host-gw/IPIP/WireGuard)的技术说明和适用场景
- Calico Architecture — Calico 组件架构,包含 Felix 策略引擎、BIRD BGP 守护进程、IPAM 的详细说明
- Cilium eBPF Datapath — Cilium 基于 eBPF 的数据路径实现,XDP/TC hook 的工作位置和 eBPF Map 的使用方式
- Linux VXLAN — Vincent Bernat 的 VXLAN 深入分析,包含 FDB 表手动操作和内核 VXLAN 模块工作原理
- Linux namespaces(7) — Linux 命名空间手册,网络命名空间(netns)是 CNI 运作的基础隔离原语
- Calico Typha — Typha 组件说明,大规模集群下减少 Felix 与 apiserver 直连的缓存代理架构
