系列导航
本系列从 Pod 网络连通入手,逐步展开到 K8s 网络全景。
① 概念 → ② Flannel → ③ Calico → ④ 流量路径 → ⑤ Cilium → ⑥ 对比 → ⑦ 排障 ‖ ⑧ 开发 → ⑨ 多网卡 → ⑩ AI 演进
| 顺序 | 文章 | 定位 |
|---|---|---|
| ① | 本篇 - 概念与入门 | 基础——主机网络、Docker 网络、CNI 标准、方案分类树 |
| ② | Flannel 详解 | CNI 实现——Overlay(UDP/VXLAN)+ Underlay(HostGW)与抓包 |
| ③ | Calico 详解 | CNI 实现——Overlay(VXLAN/IPIP)+ Underlay(BGP)+ NetworkPolicy |
| ④ | 流量路径全解析 | 全貌——Pod/Service/Ingress/Egress,揭示 CNI 的边界 |
| ⑤ | Cilium 详解(超CNI) | 超 CNI——eBPF 数据平面(Service + NetworkPolicy)+ CNI 连通(VXLAN/BGP) |
| ⑥ | 插件对比与选型 | 选型——5 插件横向对比 + 决策树 |
| ⑦ | 排障思路与常用命令 | 运维——工具链 + 场景排查 + 性能 |
| — | 核心路径 ↑ | 扩展展望 ↓ |
| ⑧ | CNI 插件开发指南 | 扩展——基于 CNI 规范开发自定义插件 |
| ⑨ | 多网卡方案详解 | 进阶——Multus + SR-IOV/ipvlan 多网口实战 |
| ⑩ | AI 时代网络演进 | 展望——GPU 网络、eBPF 加速、未来方向 |
重要
本文回答一个问题:K8s 为什么需要 CNI——因为容器跨主机时网络不通。
先定目标:K8s 要的是一个「端到端 IP 可达」的网络——任意 Pod 之间、Pod 与 Node 之间都能直接用对方的 IP 通信,IP 全程不变、无需 NAT。这是 K8s 网络模型四条假设定下的目标,也是后面 Flannel、Calico、Cilium 所有方案共同服务的对象。
再看现状:Docker 单机网络(namespace + veth + bridge)只能让同主机的容器互通,跨主机的容器够不到这个目标——网络不通。
于是定手段:CNI 为达成这个目标,在 Docker 单机网络基础上补两件事——① IPAM:给 Pod 分一个端到端一致的 IP;② 路由转发:让包真正送达目的 Pod。先定目标、再定手段,是理解 CNI 的钥匙。
文章脉络:先讲 Linux 主机网络和 Docker 单机网络,再定义 K8s 的网络目标,对照 Docker 找出跨主机差距,最后给出 CNI 的补齐手段和方案分类树。
1. 主机网络基础
理解 K8s 网络之前,需要先搞清楚一台 Linux 主机是怎么收发包的。无论物理机、虚拟机还是容器,都遵循同一套协议栈逻辑。
1.1 路由表:内核如何决定下一跳
为网卡配置 IP 地址和子网掩码后,内核自动生成两类路由:
| 路由类型 | 生成条件 | 目标网段 | 网关 | 含义 |
|---|---|---|---|---|
| 直连路由 | 网卡配置了 IP/掩码 | 该接口所在子网 | 0.0.0.0 | 同子网直接发送,不需要网关 |
| 默认路由 | 配置了默认网关 | 0.0.0.0/0 | 指定的网关IP | 不在子网的目标IP,交给网关 |
发包时内核查路由表,按最长前缀匹配决定下一跳:
同子网通信 → 匹配直连路由 → 下一跳 = 目标IP 本身 跨子网通信 → 匹配默认路由 → 下一跳 = 网关IP
1.2 ARP:IP 如何找到 MAC
路由决策只确定了「下一跳IP」,但二层封帧需要的是「下一跳MAC」。ARP 协议负责这个映射:
| 场景 | 下一跳IP | ARP 请求目标 |
|---|---|---|
| 同子网 | 目标主机IP | 直接找目标主机的 MAC |
| 跨子网 | 网关IP | 找网关的 MAC |
查 ARP 缓存(ip neigh 或 arp -n):
查 ARP 缓存 ├─ 命中 → 直接封装二层帧(目标MAC = 下一跳MAC,目标IP 不变) └─ 未命中 → 广播 ARP 请求 → 收到应答后学习 MAC → 封装帧发送
同子网:下一跳就是
目标IP,ARP 查目标MAC。
跨子网:下一跳就是网关,ARP 查网关MAC。目标IP 从源到目的地保持不变,逐跳改写的是二层MAC。
1.3 二层交换与跨网段转发
帧离开源主机后,经过的设备分两类:
| 设备类型 | 工作层 | 行为 |
|---|---|---|
| 交换机 / Linux 网桥 | 二层 | 只看目标MAC,查 CAM 表转发端口,不解封 IP 头 |
| 路由器 | 三层 | 剥离二层头,查目标IP,查路由表决定下一跳,重新封装二层头 |
二层设备的转发规则:
| 目标MAC 类型 | 行为 |
|---|---|
| 广播/多播 | 泛洪到所有端口 |
| 单播且 MAC 在 CAM 表中 | 从对应端口转发 |
| 单播但 CAM 未命中 | 泛洪(未知单播) |
跨网段转发时,数据包经过多个路由器,目标IP 自始至终不变,每跳的源/目标MAC 都在变:
源主机 → 路由器1 → 路由器2 → ... → 目标主机 目标IP = 10.0.2.55(始终不变) 目标MAC = 路由器1.MAC → 路由器2.MAC → ... → 目标主机.MAC(每跳变化)
1.4 主机网络小结
发包流程: 1. 查路由表 → 确定下一跳IP(同网段 = 目标IP / 跨网段 = 网关IP) 2. 查 ARP 缓存 → 确定下一跳MAC(同网段 = 目标MAC / 跨网段 = 网关MAC) 3. 封装二层帧(目标MAC = 下一跳MAC,目标IP 不变)→ 从网卡发出 4. 二层设备按 MAC 转发 → 到达下一跳 5. 若需跨网段,路由器重复步骤 1-4
网卡、网桥 和 路由。2. Docker 单机容器网络
Docker 没有发明新协议,完全基于 Linux 内核已有的网络能力:
Docker容器网络 = Linux NetworkNamespace + veth pair + 网桥 + 自动生成的标准路由配置 + 宿主机NAT规则
2.1 网络栈与 network namespace
网络栈是进程发起和响应网络请求的基本环境,包括:网卡、回环设备、路由表、iptables 规则。
Linux 通过 network namespace 实现网络栈隔离。每个容器拥有独立的 namespace,里面有自己的网卡、路由表、iptables 规则,与宿主机和其他容器互不干扰。
2.2 Veth Pair:连接不同命名空间的网线
Veth Pair 是一对虚拟网卡,从一端写入的数据包会直接出现在另一端,即使两端在不同的 namespace 中。因此常被用来连接不同 NS:
容器 NS 宿主机 NS ┌──────────┐ ┌──────────┐ │ eth0 │◄──veth pair──►│ veth001 │ └──────────┘ └──────────┘
2.3 网桥 docker0
docker0 是一个 Linux 网桥,根据目标MAC 将数据包转发到自身不同的端口上。每个容器启动时,其 veth 对端会插入 docker0。
关键点: veth 网卡一旦插入网桥,就从独立网卡降级为网桥的一个端口,不再调用协议栈处理数据包,转发决策完全由网桥接管。
| |
2.4 容器内的路由表
容器启动后,eth0 配置 IP(如 172.17.0.2/16),内核自动生成两条路由,与 §1 主机网络的路由规则一致:
目标网络 网关 掩码 Flags 网卡 default 172.17.0.1 0.0.0.0 UG eth0 ← 默认路由 172.17.0.0 0.0.0.0 255.255.0.0 U eth0 ← 直连路由
两条路由对应两种访问场景,docker0 扮演的角色不同:
| 路由 | 匹配场景 | docker0 的角色 |
|---|---|---|
直连路由(172.17.0.0/16) | 访问同主机其他容器 | 二层网桥:查 CAM 表,转发到目标容器的 veth 端口 |
默认路由(default) | 访问宿主机网段或外网 | 三层接口:目标MAC = docker0.MAC,上交宿主机协议栈 |
2.5 直连路由:同主机其他容器
直连路由对应「访问同主机其他容器」。以容器 A(172.17.0.2)ping 容器 B(172.17.0.3)为例:
容器A 容器B eth0 ──vethA── docker0 ──vethB── eth0 172.17.0.2 172.17.0.3
完整流程:
1. 容器A 查路由表 目标 172.17.0.3 ∈ 172.17.0.0/16 → 匹配直连路由 下一跳 = 172.17.0.3(目标本身),走 eth0 2. 容器A 查 ARP 缓存:172.17.0.3 → 未命中 → 从 eth0 发送 ARP 广播:"谁是 172.17.0.3?" 3. ARP 广播经 vethA 到达 docker0 网桥 → ARP 是广播帧 → docker0 泛洪到所有端口(vethA, vethB, ...) 4. 容器B 的 eth0(通过 vethB)收到 ARP 广播 → 目标IP == 自身IP → 回复 ARP 应答(MAC_B) 5. 容器A 获得容器B 的 MAC → 封装二层帧 目标MAC = MAC_B,目标IP = 172.17.0.3 6. 数据包经 eth0 → vethA → docker0 → docker0 查 CAM 表:MAC_B → 端口 vethB → 转发到 vethB 7. 数据包经 vethB → eth0 进入容器B → 容器B 协议栈处理 → 回复 pong
整个过程依赖:容器路由规则 → VethPair → 网桥 CAM 转发 → VethPair → 目标容器。
2.6 默认路由:宿主机网段或外网
默认路由对应「访问宿主机网段或外网」。以容器内 ping baidu.com 为例,目标不在 172.17.0.0/16 网段,匹配默认路由:
1. 容器查路由表 → 匹配 默认路由 → 下一跳 = 172.17.0.1(docker0 的 IP) 2. ARP 解析 → 获取 docker0 的 MAC 3. 数据包目标MAC = docker0.MAC → docker0 判定目标是自身 → 上交宿主机协议栈 4. 宿主机 iptables NAT(SNAT)→ 源IP 替换为宿主机IP 5. 从宿主机 eth0 发出 → 走宿主机路由 → 出外网
2.7 docker0 在两种场景下的作用与区别
回顾 §2.5、§2.6 两个场景,docker0 扮演了完全不同的角色:
| 角色 | 触发条件 | 功能 | 特性 |
|---|---|---|---|
| 二层网桥 | 目标MAC ≠ docker0 自身MAC | 查 CAM 表,按 MAC 转发到对应的 veth端口 | 与网桥docker0绑定的veth网卡会自动降级成端口,失去报文解析、转发的能力 |
| 三层接口 | 目标MAC = docker0 自身MAC | 上交宿主机协议栈(路由、iptables) | docker0需要具备IP和MAC |
报文分岔逻辑:
数据包到达 docker0
├─ 目标MAC == docker0 自身MAC
│ → 上交宿主机 IP 层 → 走路由、iptables、协议栈处理
│
├─ 目标MAC == 某端口MAC
│ → 查 CAM 表 → 转发到对应端口(不进宿主机 IP 层)
│
└─ 目标MAC == 广播/多播/CAM 未命中的单播
→ 泛洪到所有端口这是 Linux bridge 的通用行为,任何配了 IP 的 bridge 都如此,非 docker0 特有。
2.8 容器网络与 Linux 协议栈的关系
所有转发决策均遵循内核固有的路由/ARP/二层交换逻辑。容器内外协议栈行为完全一致,差别只在于容器使用的是虚拟设备(veth、网桥)而非物理设备。
3. K8s 网络模型假设
K8s 不对具体网络实现做限制,只强制约定 4 条网络假设:
| 序号 | 假设 | 说明 |
|---|---|---|
| 1 | Pod 间直接通信 | 任意两个 Pod 可以直接用对方 Pod IP 通信,不需要 NAT |
| 2 | 节点与 Pod 间直接通信 | 节点和 Pod 可以直接通信,不需要 NAT |
| 3 | Pod 所见 IP = 外部所见 IP | Pod 内部感知到的 IP 地址,与其他 Pod 或节点看到的 IP 完全一致 |
| 4 | Service 抽象 | 集群内可通过 Service IP 访问 Pod,Service 做负载均衡和 DNS 解析 |
这 4 条假设的核心是:
这避免了传统虚拟化网络中 “网络地址转换 NAT” 带来的复杂性,为服务发现、负载均衡、可观测性提供了统一基础。
4. 跨主机容器通信问题:Docker bridge 为什么不够
四条假设定了目标——端到端 IP 可达。对照这个目标,Docker 单机网络差在哪?
4.1 单机能解决什么
- 同主机,容器间通信:
容器.路由表 + VethPair + docker0网桥(veth降级为端口 + CAM根据mac查端口)
- 容器 访问 其他宿主机或外网:
容器.路由表(匹配默认路由,下一跳 = docker0 IP) → VethPair → docker0 三层设备(目标MAC = docker0.MAC,上交宿主机协议栈) → 宿主机.路由表(根据目标IP 决定从哪个网卡出) → 宿主机对应网卡发出
4.2 跨主机容器通信为什么失败
当容器 A(节点1,172.17.0.2)要访问容器 C(节点2,172.17.0.5)时:
节点1 节点2 容器A (172.17.0.2) 容器C (172.17.0.5) │ eth0 │ eth0 │ vethA │ vethC docker0 (172.17.0.1) docker0 (172.17.0.1) │ eth0 │ eth0 └────────────────────── 物理网络 ─────────┘
失败归结为两个根本原因:
① IPAM:IP 分配不统一,无法感知跨主机的 IP 划分
Docker 默认每台宿主机都用同一个子网(172.17.0.0/16),容器 IP 各管各的:
- 两个节点可能给容器分配相同 IP(如都是
172.17.0.5),冲突无法感知 - 容器 A 查路由表,
172.17.0.5落在本地直连网段,误以为直连可达,其实在另一台机器上
② 连通性(路由转发):路由链路没打通,单机 docker0 送不到
即使知道 172.17.0.5 在节点2,节点1 也没有通往它的路由:
- 容器 A 发 ARP 广播找
172.17.0.5的 MAC,广播只在节点1 的 docker0 内泛洪,到不了节点2 - 两个 docker0 各自维护独立 CAM 表,互不感知
两个原因正好对应 CNI 要补的两件事——IPAM 和连通性。
4.3 跨主机需要什么能力
要打通跨主机容器通信,必须补齐三个能力,分别落在 §4.2 的两个维度上:
| 维度 | 能力 | 说明 |
|---|---|---|
| IPAM | 全局 IPAM | 每个节点的容器子网不能冲突,需要统一的 IP 分配 |
| 连通性(路由转发) | 跨节点路由信息 | 节点1 需要知道「172.17.0.5 在节点2」 |
| 连通性(路由转发) | 数据包送达机制 | 跨节点时通过封装(Overlay)或直接路由 |
这就是 CNI 要做的事情——在 Docker 单机网络的基础上,补齐跨主机能力。
5. CNI 标准与调用链
5.1 CNI 要解决的两件事
对照 §4.2 暴露的两个维度,CNI 要解决的就是这两件事:
- IPAM(IP 地址管理)——给 Pod 分什么 IP、怎么避免冲突
- 连通性(路由转发)——包怎么从源 Pod 送到目的 Pod
① IPAM:思路几乎只有一种
中心化的全局 IP 管理——由统一组件按节点划分子网、给 Pod 分配 IP,从根上避免 §4.2 里「各管各的」导致的冲突。实现这个思路的插件不止一种。
② 连通性:实现分两大类
区别在「要不要封装」——Overlay 用隧道封装、Underlay 直接维护路由。
落地到具体实现:
两件事的具体实现
├── IPAM(3 类实现方式)
│ ├── host-local(静态子网分配)→ Flannel、Kube-router 等
│ ├── calico-ipam(动态分配)→ Calico
│ └── dhcp(从 DHCP 服务器获取)→ 少见
│
└── 连通性(2 类实现方式)
├── Overlay — 构造特殊通路
│ ├── UDP(用户态封装,性能差,仅演示)→ Flannel-UDP
│ ├── VXLAN(内核态 L2-in-UDP,生产通用)→ Flannel-VXLAN、Calico-VXLAN、Cilium-VXLAN
│ └── IPIP(内核态 IP-in-IP,纯三层封装)→ Calico-IPIP
└── Underlay — 维护节点路由
├── HostGW(静态路由,要求二层互通)→ Flannel-HostGW
└── BGP(动态路由,三层互通)→ Calico-BGP、Cilium(native routing)注:Cilium 是「超 CNI」——节点内数据平面用 eBPF(替换掉 iptables + bridge),跨节点传输仍选 VXLAN(Overlay)或 BGP(Underlay),故横跨两类;IPAM 也是自己的一套(cluster-pool / 云厂商集成),详见系列第⑤篇。
连通性两类实现(Overlay / Underlay)的原理和选型见 §6;各插件的配置、路由表、抓包验证,留给 Flannel 详解 和 Calico 详解 展开。
5.2 什么是 CNI
CNI (Container Network Interface) 是 CNCF 定义的容器网络接口标准。它规定了:
- Kubelet 调用网络插件的接口格式
- 插件必须实现的生命周期操作:ADD、DEL、CHECK、VERSION
- 插件返回结果的数据结构
CNI 解决的是 “Pod 网络怎么配置” 的标准化问题,让不同网络方案可以按同样的接口接入 K8s,而不需要修改 K8s 核心代码。
CNI 不管的事: Service 抽象(
kube-proxy负责)、Ingress/Egress(Ingress Controller负责)、网络策略(NetworkPolicy实现负责)、可观测性。Cilium 这类"超 CNI"方案把这些全部整合进一个内核级数据平面。
5.3 K8s 中的 CNI 调用链
用户创建 Pod
↓
API Server 写入 Etcd
↓
Scheduler 调度到某节点
↓
Kubelet 监听到本节点 Pod 创建
↓
读取 /etc/cni/net.d/ 目录下的 CNI 配置文件
↓
执行配置中指定的 CNI 插件二进制文件(在 /opt/cni/bin/)
↓
CNI 插件进入 Pod 网络命名空间
↓
创建 veth 网卡对 → 分配 IP → 配置路由 → 设置 iptables
↓
返回 Pod IP 等信息给 Kubelet
↓
Pod 启动关键配置位置:
- CNI 配置文件:
/etc/cni/net.d/*.conf- CNI 二进制文件:
/opt/cni/bin/- IPAM 存储:etcd(Flannel)或 CRD(Calico)
6. 跨节点通信的两条路
§5.1 给了分类全貌。其中 IPAM 各家方案差异不大,真正拉开差距、也是 K8s 网络方案核心的,是连通性的选择和实现。
连通性分 Overlay / Underlay 两大类,正是这两类实现方式,引出了 Flannel、Calico、Cilium 等具体方案。
6.1 Overlay:构造特殊通路
Overlay 的核心理念:用隧道封装在物理网络之上构建叠加网络。 底层网络不知道 Pod IP,只看到封装后的宿主机间流量。属于"构造特殊通路"的方式。
层次由内层报文决定,不看底层隧道:
| 方案 | 报文结构 | 层次 | 特点 | 场景 |
|---|---|---|---|---|
| UDP | HostIP|UDP|内层IP|Data | L3 | 用户态封装,28B 开销,性能最差 | 仅演示 |
| VXLAN | HostIP|UDP|VXLAN|内层MAC|内层IP|Data | L2 | 内核态 VTEP 封装,50B 开销,生产通用 | 公有云、虚拟化环境 |
| IPIP | HostIP|内层IP|Data | L3 | 内核态封装,20B 开销,更轻量但只支持 IP 单播 | 三层互通 |
层次由 内层报文 决定——内层含 MAC 头就是 L2(VXLAN),只有 IP 就是 L3(UDP、IPIP)。外层封装上,UDP/VXLAN 走 UDP(L4),IPIP 走 IP 协议号 4(L3)。
6.2 Underlay:维护节点路由
路由模式的核心理念:不做封装,直接维护宿主机路由表。 跨节点流量按标准 IP 路由转发。
| 方案 | 路由方式 | 要求 | 典型实现 |
|---|---|---|---|
| HostGW | 静态路由,下一跳 = 目标节点 IP | 所有节点必须在同一个 L2 广播域 | Flannel-HostGW |
| BGP | 动态路由协议,每个节点宣告自己的 Pod 子网 | 机房间允许 BGP 协议 | Calico-BGP |
6.3 分类决策
两类方案的选择本质是 通用性 vs 性能 的权衡:
| 维度 | Overlay(构造通路) | Underlay(维护路由) |
|---|---|---|
| 通用性 | 高,适配任何底层网络 | 低,要求 L2 互通或支持 BGP |
| 性能 | 有 10-20% 封装开销 | 接近裸机网络 |
| 典型场景 | 公有云、虚拟化环境 | 自建 IDC 机房 |
具体各方案的包流向、抓包分析、配置示例,详见 Flannel 详解 和 Calico 详解。多插件横向对比和选型决策,见 插件对比与选型。