阶段二 · CNI 与数据平面

CNI:规范与主流插件

一句话总结

CNI(容器网络接口)是 Kubernetes 把"配置 Pod 网络"标准化外包出去的接口: 它只规定"插件必须能给 Pod 配好网络, 能回收", 至于怎么配(Overlay 还是路由, 要不要策略)由选哪个插件决定.
理解 CNI, 就理解了为什么 Flannel, Calico, Cilium 能被随意替换.

前置回顾

第 01, 02 篇我们手动搭了 namespace, veth, 网桥, 也讲了跨节点的 Overlay 和路由两条路线, 但一直没回答: 这些东西在 Pod 创建的瞬间是谁自动配好的? Flannel, Calico 到底是什么?

本篇揭开--它们都是 CNI 插件, 而 CNI 正是本系列核心主线"K8s 定义接口, 实现外包给插件"在网络侧的体现.

为什么 K8s 自己不实现网络

先问一个根本问题: Kubernetes 既然这么强大, 为什么不干脆内置一套网络实现, 非要搞个接口外包出去?

因为"Pod 怎么联网"的需求千差万别:

  1. 自建小集群图省事 → 想要 Flannel 这种无脑 Overlay
  2. 大规模性能敏感 → 想要 Calico 的 BGP 路由
  3. 要 L7 安全和可观测 → 想要 Cilium 的 eBPF
  4. 跑在 AWS 上 → 想用 VPC 原生 IP
  5. 还有各种私有云方案...

如果 K8s 内置死一种实现, 就无法适配所有这些场景.

所以 Kubernetes 做了一个克制而聪明的决定: 自己只定义"Pod 网络该满足什么"(第 01 篇的四条规则), 把"具体怎么实现"抽象成一个标准接口, 谁都可以来实现. 这就是 CNI(Container Network Interface).

这是'机制与策略分离'的典范

K8s 定义机制(接口: "必须能给 Pod 配网, 能清理"), 插件提供策略(实现: "我用 VXLAN / 我用 BGP / 我用 eBPF"). 这种分离让 Kubernetes 的核心保持精简稳定, 而网络生态可以百花齐放, 自由竞争与替换.

存储侧的 CSI(第 07 篇)是一模一样的设计--这种对称不是巧合, 而是云原生贯穿始终的设计哲学.

CNI 到底是什么: 一个朴素到惊人的接口

CNI 的规范简单得超乎想象. 它本质上就是约定: 一个 CNI 插件是一个可执行程序, 接收标准化的输入(容器的 namespace, 网络配置), 完成网络配置, 返回结果. 核心就两个操作:

操作 什么时候调用 插件要做什么
ADD Pod 创建时 给 Pod 建网络: 创建/配置网卡(veth), 分配 IP, 配路由--就是第 01-02 篇那些手动操作的自动版
DEL Pod 销毁时 清理网络: 回收 IP, 删除网卡等

CNI 像电源插座的国家标准: 标准只规定"插孔的形状, 电压"(接口), 不规定"电从核电站来还是太阳能来"(实现).
任何符合标准的电器(插件)都能插上用, 想换供电方案, 换个插座背后的东西就行, 电器(K8s)完全无感.

CNI 是怎么被调用的

把第 01-02 篇和 CNI 接通, 看一次 Pod 创建时网络是怎么自动配好的. 这里有个常被讲错的细节: 真正创建网络命名空间, 调用 CNI 的不是 kubelet, 而是容器运行时(containerd / CRI-O):

┌─────────────────┐
│ 调度器把 Pod 分配到某节点 │
└─────────────────┘


┌──────────────────────────────────────┐
│ 该节点的 kubelet 经 CRI 通知容器运行时启动 Pod │
└──────────────────────────────────────┘


┌──────────────────────────────────────────────────┐
│ 容器运行时创建 Pod 沙箱(pause 容器)+ network namespace │
└──────────────────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│ 容器运行时调用 CNI 插件的 ADD, 传入: namespace 路径, 网络配置 │
└──────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────┐
│ CNI 插件干活: 建 veth, 插网桥, 分配 IP, 配路由(跨节点则配 VXLAN 或 BGP) │
└────────────────────────────────────────────────────────────┘


┌─────────────────────────┐
│ 插件返回分配的 IP, Pod 网络就绪 │
└─────────────────────────┘

所以第 01 篇说的"这些 veth, 网桥不用手动配", 真相就是: 容器运行时在起 Pod 沙箱时调用了 CNI 插件, 插件自动完成了手动做的一切. 而 kubelet 只是经 CRI(下文)发起"启动 Pod"的指令, 它和运行时都不懂网络细节--具体怎么联网, 全是 CNI 插件的事. 这就是接口外包的威力: 每一层只管自己那一环, 网络实现被干净地隔离在插件里.

为什么是运行时而不是 kubelet 调 CNI

这正好印证本篇的 CRI/CNI/CSI 三件套: kubelet 不直接干运行时的活, 它通过 CRI 把"创建 Pod 沙箱"交给容器运行时; 沙箱(含 network namespace)由运行时创建, 所以"给这个 namespace 配网络(调 CNI)"自然也由运行时发起.

kubelet 站在更高层只做编排决策, 把运行时, 网络, 存储分别外包给 CRI, CNI, CSI--层层解耦, 各司其职.(早期 dockershim 时代链路略有不同, 现代 K8s 统一为"运行时经 CRI 被调用, 再调 CNI".)

CNI 插件通常以 DaemonSet 形式跑在每个节点

装一个 CNI(如 kubectl apply Calico 的 manifest), 它会以 DaemonSet(每节点一个 Pod, Kubernetes 笔记里的工作负载)的形式部署: 在每个节点上放好 CNI 可执行文件, 跑好需要的守护进程(如 Calico 的 BGP 程序, Flannel 的 flanneld).

节点上的容器运行时就能调用它了. 所以"装 CNI"=在每个节点铺好那个能响应 ADD/DEL 的插件程序.

三大主流插件: 一条光谱

有了 CNI 接口, 市面上出现了几十种插件. 把最主流的三个排在一条"从简单到强大"的光谱上, 就能快速建立选型直觉. 它们恰好对应第 02 篇的技术路线:

Flannel: 最简单, 够用就好

  • 路线: 默认 VXLAN Overlay(第 02 篇路线一)
  • 特点: 配置极简, 无脑能用, 几乎零运维. 专注"把 Pod 网络跑通"这一件事
  • 短板: 不支持 NetworkPolicy(第 05 篇), 即没有网络安全隔离能力; 有封装性能损耗
  • 适合: 学习, 开发, 中小集群, 不需要网络策略的场景

Calico: 性能与策略兼备

  • 路线: 默认 BGP 路由(第 02 篇路线二, 无封装), 也支持 IPIP/VXLAN 兜底
  • 特点: 性能好(无封装), 成熟稳定, 完整支持 NetworkPolicy, 生产环境广泛使用
  • 短板: BGP 配置门槛比 Flannel 高; 策略基于 iptables(大规模时有性能话题, 见第 04 篇)
  • 适合: 生产集群, 性能敏感, 需要网络策略

Cilium: 基于 eBPF 的下一代

  • 路线: 基于 eBPF(第 04 篇专讲), 数据平面在内核里高效执行
  • 特点: 高性能, 可替代 kube-proxy, 支持 L7 层策略(到 HTTP 方法/路径级别), 自带 Hubble 可观测性
  • 短板: 概念新, 门槛最高, 对内核版本有要求
  • 适合: 追求性能与安全可观测, 愿意拥抱新技术的现代集群
维度 Flannel Calico Cilium
跨节点路线 VXLAN 封装 BGP 路由(可封装兜底) eBPF(可路由/封装)
NetworkPolicy ❌ 不支持 ✅ 支持(L3/4) ✅ 支持(到 L7)
性能 一般(封装) 好(无封装) 很好(eBPF)
复杂度 / 门槛 最低 较高
一句话 简单够用 生产主力 前沿强大

选型一句话

学习/小集群/图省事 → Flannel; 生产, 要性能和网络策略 → Calico; 追求极致性能 + L7 安全 + 可观测, 能接受新技术 → Cilium.

正因为有 CNI 这个统一接口, 这三者理论上可以替换(实际换 CNI 需重建 Pod 网络, 有成本, 但应用层无感)--这就是"接口外包"带来的自由. 当下的趋势是 Cilium 势头很猛, 第 04 篇会讲清它凭什么.

快速回顾

  • K8s 不自己实现网络, 因为联网需求千差万别; 它定义 CNI 接口, 把实现外包给可插拔插件--机制/策略分离
  • CNI 接口朴素: 插件是个可执行程序, 核心两操作--ADD(Pod 创建时配网络), DEL(销毁时清理)
  • 调用链: kubelet 起 Pod → 建 namespace → 调 CNI 的 ADD → 插件自动完成第 01-02 篇那些手动操作; CNI 插件通常以 DaemonSet 铺在每个节点
  • 三大插件光谱: Flannel(VXLAN, 简单, 无策略) < Calico(BGP, 生产主力, 支持 L3/4 策略) < Cilium(eBPF, L7 策略+可观测, 前沿)
  • 选型: 图省事 → Flannel; 生产+策略 → Calico; 极致性能+L7+可观测 → Cilium

CNI 和 CRI, CSI 是什么关系? 名字好像.

它们是 Kubernetes 的"三大可插拔接口", 同一种设计哲学的三个方向: CRI(Container Runtime Interface)外包容器运行时(用 containerd 还是 CRI-O), CNI 外包网络(本篇), CSI(Container Storage Interface)外包存储(第 07 篇).

记忆法: R=Runtime 运行, N=Network 网络, S=Storage 存储. K8s 把这三块最容易有分歧, 最需要适配不同环境的能力, 统统做成了标准接口. 看懂一个, 另两个触类旁通.

能同时装多个 CNI 插件吗?

主网络插件(负责给 Pod 配主网卡的那个)通常只能有一个--两个都想管 Pod 的主网络会打架. 但 CNI 支持链式调用(chaining)多网卡方案(如 Multus): Multus 本身作为"元插件", 能让一个 Pod 拥有多块网卡, 分别由不同 CNI 负责(常见于电信, NFV 等需要多网络平面的场景).

常规业务用单个主 CNI 即可, 不必折腾多 CNI.

动手练习

  1. 查看节点 CNI 配置: 在一个跑着的 K8s 节点上, 看看 /etc/cni/net.d/ 目录里的配置文件和 /opt/cni/bin/ 里的插件可执行文件--这就是容器运行时调用 CNI 的依据
  2. 确认 CNI DaemonSet: 用 kubectl get pods -n kube-system 找到 CNI 插件 Pod(如 calico-node, cilium, kube-flannel), 确认它是 DaemonSet, 每个节点一个
  3. 对比不同 CNI 集群: 分别用 kind 创建一个默认 CNI 的集群和一个指定 Calico 的集群, 对比两者跨节点流量(抓包)和路由表有何不同(印证第 02 篇两条路线)
  4. 验证策略能力差异: 在装了 Flannel 的集群上写一个 NetworkPolicy, 观察它不生效(Flannel 不支持); 换到 Calico/Cilium 再试, 确认生效--亲身体会插件能力差异
  5. 实际选型判断: 对照团队的实际情况, 用本篇的选型逻辑判断: 该用(或正在用)哪个 CNI? 理由是什么?