Kubernetes 基础体系 · 第 30/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes CNI 网络:Pod IP、路由、Overlay、Underlay 和插件选型

Kubernetes 的网络问题,通常不是“Pod 为什么没有 IP”这么简单。一个 Pod 能否访问另一个 Pod,至少涉及以下对象和路径:

Pod
 └─ network namespace
     ├─ eth0:Pod 内的虚拟网卡
     └─ default route:默认路由
          │
          └─ veth pair
               │
节点网络命名空间
 ├─ bridge / host device / eBPF datapath
 ├─ Pod CIDR 路由
 ├─ Overlay 隧道或 Underlay 路由
 └─ 节点物理网卡
      │
      └─ 其他节点、Service、外部网络

其中:

  • CNI 负责把网络接口、地址和路由接入容器网络;
  • Pod IP 是 Pod 在集群网络中的地址;
  • 路由 决定数据包下一跳;
  • Overlay 在现有网络之上再封装一层虚拟网络;
  • Underlay 是承载这套网络的真实底层网络;
  • ServiceNetworkPolicy 分别处理服务发现/转发与流量控制;
  • CRI 创建并管理容器,但通常由容器运行时调用 CNI;
  • CSI 负责存储,不参与 Pod 网络连通性。

要理解 CNI,首先要把“容器有网卡”与“集群中 Pod 可以互通”区分开。前者是网络命名空间内部的接口配置,后者还需要跨节点路由、封装、转发、策略和可能的 NAT。


一、Kubernetes 网络模型要求了什么

Kubernetes 网络模型可以抽象为以下几个要求:

  1. 每个 Pod 获得一个独立的 IP 地址;
  2. 同一节点上的 Pod 可以直接通信;
  3. 不同节点上的 Pod 可以直接通信;
  4. Pod 可以与节点通信;
  5. Pod 访问 Pod 时,通常不应依赖额外的地址转换;
  6. 容器看到的是自己的网络命名空间,而不是节点的网络命名空间。

“直接通信”并不表示数据包一定物理上直连,也不表示一定没有隧道。它表示从 Kubernetes 网络模型看,Pod 使用对端 Pod IP 通信,不需要应用感知中间的 NAT 或代理。

例如,Pod 10.244.1.12 访问 Pod 10.244.2.8,应用只需要连接:

10.244.2.8:8080

应用不需要知道:

  • 对端 Pod 在哪台节点;
  • 节点之间是否使用 VXLAN;
  • 是否通过 BGP 学习路由;
  • 底层是否经过云厂商 VPC 路由;
  • 中间是否经过 eBPF datapath。

这些由节点上的网络实现完成。

不过,Kubernetes 网络模型是行为模型,不是唯一实现。一个网络插件可以使用:

  • Linux bridge;
  • Linux routing;
  • VXLAN、Geneve 等 Overlay;
  • BGP、静态路由或云路由表;
  • iptables、IPVS 或 eBPF;
  • 额外的 NAT、代理或网关。

因此,“Pod IP 能互通”并不能反推出“集群一定用了 Overlay”,同样也不能反推出“集群一定用了 BGP”。


二、CNI 的责任边界:它到底做什么

1. CNI 不是一个长期运行的网络控制器

CNI 通常指一组容器网络接口规范和插件程序。它描述了容器运行时如何请求网络插件完成网络配置。

典型调用流程如下:

sequenceDiagram
    participant K as kubelet
    participant R as CRI runtime
    participant C as CNI plugin
    participant I as IPAM
    participant L as Linux kernel

    K->>R: 创建 Pod sandbox
    R->>C: CNI ADD
    C->>I: 请求分配 Pod IP
    I-->>C: 返回 IP、网关、路由
    C->>L: 创建 network namespace/veth
    C->>L: 配置 eth0、路由、规则
    C-->>R: 返回接口、IP、路由
    R-->>K: Pod sandbox 网络就绪
    K->>R: 启动容器

在常见实现中:

  1. kubelet 通过 CRI 请求容器运行时创建 Pod sandbox;
  2. 容器运行时为 Pod 创建网络命名空间;
  3. 容器运行时调用 CNI ADD
  4. CNI 插件创建网卡、分配地址、设置路由;
  5. CNI 将结果返回给运行时;
  6. 运行时启动 Pod 中的容器。

具体调用时机和插件组合由容器运行时实现决定,不能把“kubelet 直接执行 CNI”当成所有版本和运行时的唯一事实。对 Kubernetes 使用者而言,重要的是:Pod sandbox 的网络由 CRI 运行时和 CNI 协作建立

2. CNI ADDDEL 的状态变化

可以把一个 Pod sandbox 的网络生命周期简化为:

不存在
  │ ADD
  ▼
IPAM 分配地址
  │
  ▼
创建 veth / 主机端接口
  │
  ▼
配置 Pod eth0、默认路由和节点侧转发
  │
  ▼
网络就绪
  │ DEL
  ▼
删除接口、规则、路由并回收 IP
  │
  ▼
不存在

ADD 通常需要完成:

  • 创建或接入 Pod 的网络命名空间;
  • 创建 veth pair 或其他数据通道;
  • 给 Pod 接口分配 IP;
  • 配置默认路由;
  • 配置节点侧桥、路由、隧道或 eBPF 数据路径;
  • 将 Pod 纳入必要的 NetworkPolicy 或服务规则。

DEL 通常需要反向清理:

  • 删除接口;
  • 删除临时路由和规则;
  • 从 IPAM 中回收地址;
  • 清理隧道端点、端点映射或策略状态。

实际插件可能把部分工作交给常驻控制器完成。例如,CNI ADD 只创建本地端点,而路由分发、策略编译和节点间隧道维护由 DaemonSet 或控制器异步完成。因此,“CNI ADD 返回成功”通常表示本地网络配置完成,不一定表示整个集群的数据平面已经收敛。

3. CNI 主插件、IPAM 插件和链式插件

一个网络配置通常包含多个职责:

主网络插件
 ├─ 创建接口
 ├─ 连接节点数据平面
 └─ 返回基础网络信息

IPAM 插件
 ├─ 分配 Pod IP
 ├─ 分配网关
 └─ 维护地址池状态

链式插件
 ├─ bandwidth
 ├─ port-mapping
 ├─ tuning
 └─ firewall

不要把 CNI 和 IPAM 混为一谈。一个插件可以同时实现两者,也可以调用独立的 IPAM 插件。

CNI 网络配置常见于节点上的配置目录,例如:

ls -l /etc/cni/net.d/
ls -l /opt/cni/bin/

可能看到 .conflist.conf 以及插件二进制。不同发行版、安装方式和安全策略可能使用不同目录,不能仅凭某个目录为空就断定集群没有 CNI。


三、Pod IP 从哪里来:地址、网段和唯一性

1. Pod IP 的三个层次

Pod IP 通常需要同时满足三个条件:

  1. 地址格式合法:例如 IPv4 10.244.1.12
  2. 在某个 Pod 地址池内:例如节点的 Pod CIDR 为 10.244.1.0/24
  3. 在集群可达范围内唯一:不能与其他活动 Pod 冲突。

常见的地址分配层次是:

Cluster Pod CIDR: 10.244.0.0/16
    ├─ Node A Pod CIDR: 10.244.1.0/24
    ├─ Node B Pod CIDR: 10.244.2.0/24
    └─ Node C Pod CIDR: 10.244.3.0/24

Pod CIDR 是分配给节点用于承载 Pod 地址的网段。它不一定存在于所有集群的同一种控制面对象中,也不一定由 Kubernetes 控制器直接管理;有些云网络或 CNI 使用自己的地址池和控制器。

2. 一个地址分配算例

假设地址池为:

10.244.1.0/29

IPv4 /29 一共有:

23229=82^{32-29} = 8

个地址:

10.244.1.0
10.244.1.1
10.244.1.2
10.244.1.3
10.244.1.4
10.244.1.5
10.244.1.6
10.244.1.7

若采用传统子网语义,.0 是网络地址,.7 是广播地址;但 Pod IPAM 是否把所有地址都按传统主机地址规则处理,取决于具体实现和网络模型。因此不能仅凭 CIDR 算出“可创建 Pod 数量”,还要考虑:

  • CNI 的保留地址;
  • 网关地址;
  • 节点预留地址;
  • 双栈是否分别消耗 IPv4 和 IPv6 地址;
  • 是否按节点分配小网段;
  • 是否启用地址回收延迟。

在工程上,地址池容量至少需要满足:

PtotalPrunning+Psurge+Psystem+PfragmentationP_{\text{total}} \geq P_{\text{running}} + P_{\text{surge}} + P_{\text{system}} + P_{\text{fragmentation}}

其中:

  • PrunningP_{\text{running}}:稳定运行的 Pod 数量;
  • PsurgeP_{\text{surge}}:滚动发布、Job、临时副本带来的峰值;
  • PsystemP_{\text{system}}:DaemonSet、DNS、网络插件自身的 Pod;
  • PfragmentationP_{\text{fragmentation}}:地址池按节点或块分配造成的碎片余量。

如果节点先拿到 /24,但实际只运行少量 Pod,地址仍可能被节点级地址块占用。这是“集群总地址足够,但某个节点仍然无法创建 Pod”的常见原因。

3. Pod IP 不是稳定身份

Pod IP 的生命周期通常与 Pod sandbox 或 Pod 生命周期相关,而不是与 Deployment 的逻辑副本相关。

例如:

Deployment web
  Pod web-abc:10.244.1.12
  Pod web-def:10.244.2.9

如果 web-abc 被删除后重新创建,新 Pod 可能获得:

10.244.3.15

因此:

  • 不应把 Pod IP 写入长期配置;
  • 不应把 Pod IP 当成服务发现名称;
  • 应使用 Service、DNS 或其他控制面发现机制;
  • 调试时可以直接使用 Pod IP,但生产应用不应依赖它稳定不变。

四、Pod 内部网络:network namespace、veth 和默认路由

1. Pod 为什么能共享一个 IP

一个 Pod 通常对应一个 network namespace。Pod 中的多个容器共享这个网络命名空间,因此共享:

  • 同一个 Pod IP;
  • 同一个 eth0
  • 同一个端口空间;
  • 同一套路由表;
  • 同一组网络接口和网络命名空间级别的配置。

因此,下面两个容器不能同时监听同一个 Pod IP 上的 8080 端口:

容器 A:0.0.0.0:8080
容器 B:0.0.0.0:8080

这不是 CNI 冲突,而是它们本来就在同一个网络命名空间中。

2. veth pair 如何连接两个网络命名空间

Linux 中常见的连接方式是 veth pair:

Pod network namespace                 Node network namespace

eth0  <============================>  vethXXXX
10.244.1.12                           接入 bridge 或节点数据平面

veth 的两端是一对虚拟以太网接口。一个端点移动到 Pod 的网络命名空间,另一个端点留在节点网络命名空间。向 Pod 内 eth0 发送的数据包,会从 veth 的另一端进入节点网络命名空间。

在 Pod 内执行:

ip addr
ip route

典型结果可能类似:

default via 10.244.1.1 dev eth0
10.244.1.12/24 dev eth0 scope link

这表示:

  • Pod IP 是 10.244.1.12
  • Pod 认为网关是 10.244.1.1
  • 不在本地 /24 的目标默认发送给该网关。

不同 CNI 可能使用点对点 /32、不同网关地址、额外策略路由或 eBPF,不应要求所有插件都出现完全相同的输出。

3. 数据包离开 Pod 后发生什么

当 Pod 10.244.1.12 访问 10.244.2.8:8080 时,简化路径如下:

Pod eth0
  │ 目标:10.244.2.8
  ▼
veth
  ▼
节点数据平面
  │ 查询路由/端点映射
  ├─ 本地 Pod:直接交付
  └─ 远端 Pod:发送到远端节点
          │
          ├─ Overlay:封装后经节点网络发送
          └─ Underlay:按 Pod 路由直接转发

本地 Pod 和远端 Pod 的最大区别不是 Pod 内部配置,而是节点如何处理“远端 Pod 地址”。


五、路由:数据包如何找到目标 Pod

1. 路由的基本形式

对目标地址 DD,节点根据最长前缀匹配选择路由:

R(D)=argmaxrroutes,Dprefix(r)prefixLength(r)R(D) = \arg\max_{r \in \text{routes},\, D \in \text{prefix}(r)} \text{prefixLength}(r)

例如,节点上有:

10.244.1.0/24 dev cni0
10.244.2.0/24 via 192.168.10.12 dev eth0
0.0.0.0/0 via 192.168.10.1 dev eth0

访问 10.244.2.8 时:

  • 10.244.2.8 匹配 10.244.2.0/24
  • 该前缀比默认路由更具体;
  • 下一跳是 192.168.10.12
  • 数据包发送到节点 B。

访问 8.8.8.8 时:

  • 不匹配两个 Pod 网段;
  • 使用默认路由;
  • 发送给 192.168.10.1

可以在节点上检查:

ip route
ip route get 10.244.2.8

ip route get 比只看路由表更有价值,因为它显示内核针对某个具体目标选择的接口、下一跳和源地址。

2. 本地路由与远端路由

假设:

节点 A:192.168.10.11,Pod CIDR 10.244.1.0/24
节点 B:192.168.10.12,Pod CIDR 10.244.2.0/24

Underlay 直接承载 Pod 路由时,节点 A 可能有:

10.244.1.0/24 dev cni0
10.244.2.0/24 via 192.168.10.12 dev eth0

节点 B 可能有:

10.244.2.0/24 dev cni0
10.244.1.0/24 via 192.168.10.11 dev eth0

这样,Pod 10.244.1.12 发往 10.244.2.8 的原始 IP 包可以通过节点间网络传递,不需要额外的 Overlay 头。

路由来源可以是:

  • 静态配置;
  • CNI 控制器下发;
  • BGP 邻居交换;
  • 云厂商路由表;
  • 云原生路由 API;
  • eBPF 或其他数据平面映射。

3. 路由存在不等于链路可用

即使 ip route get 找到了正确下一跳,通信仍可能失败,原因包括:

  • 下一跳节点不接受目标 Pod CIDR;
  • 节点转发被内核参数关闭;
  • 云安全组丢弃了节点间流量;
  • NetworkPolicy 丢弃了数据包;
  • 返回路径缺失;
  • MTU 不匹配;
  • 目标 Pod 已被删除,但路由或端点缓存尚未收敛;
  • 目标端口没有监听。

因此诊断必须把路径拆成多个层次:

Pod 内路由
→ 节点本地转发
→ 节点间路由或隧道
→ 目标节点本地交付
→ NetworkPolicy
→ 目标进程监听
→ 返回路径

六、Overlay:在 Underlay 之上封装 Pod 网络

1. Overlay 的定义

Overlay 是建立在底层网络之上的逻辑网络。Underlay 只需要负责承载 Overlay 的外层数据包,Pod 地址可以不属于底层网络原本的路由空间。

假设:

Pod A:10.244.1.12
Pod B:10.244.2.8

节点 A:192.168.10.11
节点 B:192.168.10.12

Overlay 发送过程可以表示为:

内层 IP:
源 10.244.1.12
目标 10.244.2.8
      │
      ▼ 封装
外层 IP:
源 192.168.10.11
目标 192.168.10.12
      │
      ▼
Underlay 网络传输
      │
      ▼ 解封装
内层 IP:
源 10.244.1.12
目标 10.244.2.8

VXLAN 是常见的一种封装方式。它通常使用 UDP 承载,逻辑上以 VNI 区分 Overlay 网络。具体封装字段、端口和数据平面行为取决于实现,不应把所有 CNI 都等同于 VXLAN。

2. Overlay 的路由逻辑

Overlay 仍然需要先知道“目标 Pod 位于哪个远端节点”。例如节点 A 维护:

10.244.2.0/24 dev vxlan.calico

当目标是 10.244.2.8 时,节点 A 通过 Overlay 设备处理,再把数据包封装到节点 B 的 VTEP 地址 192.168.10.12

可以把它拆成两次查找:

第一次:Pod 目标路由
10.244.2.8 → Overlay 设备

第二次:Underlay 端点路由
192.168.10.12 → 节点 B 的物理网卡和下一跳

这也是 Overlay 故障常见的两个层次:

  • 内层 Pod 路由或端点映射错误;
  • 外层节点 IP 路由、UDP 放行或隧道状态错误。

3. Overlay 的代价

Overlay 的主要代价不是“虚拟”二字,而是额外的状态和封装:

MTU 变小

原始链路 MTU 为 MM,封装头开销为 HH,则 Overlay 内层可用 MTU 近似为:

Moverlay=MHM_{\text{overlay}} = M - H

例如底层接口 MTU 为 1500,隧道增加约几十字节头部时,Pod 侧 MTU 需要小于 1500。实际开销与封装类型、是否加 VLAN、是否加加密头有关,不能固定写成一个适用于所有插件的数字。

如果 Pod 仍发送接近 1500 字节的包,可能出现:

  • 分片;
  • PMTUD 依赖 ICMP,但 ICMP 被过滤;
  • TCP 建连成功但传输大响应卡住;
  • HTTPS、镜像拉取或大文件传输间歇失败;
  • 小包 ping 正常,大包 ping 失败。

可以使用不分片探测:

ping -M do -s 1400 <目标 Pod IP>

这里的 1400 是待测试值,不是通用正确值。应逐步调整并结合实际路径 MTU 验证。

性能路径更复杂

Overlay 可能增加:

  • 封装和解封装;
  • 校验和处理;
  • 隧道端点查找;
  • 更复杂的抓包位置;
  • 额外的 CPU 消耗。

现代内核和网卡可能提供卸载能力,因此不能仅凭“使用 Overlay”推导固定性能损失。生产环境应使用目标内核、实例类型、流量模式和加密设置进行压测。

Underlay 仍然是硬依赖

Overlay 不是绕过底层网络,而是把底层网络要求从“能路由 Pod IP”变为“能路由节点 IP 并允许封装流量”。

例如 VXLAN 常见依赖包括:

  • 节点之间可达;
  • 相应 UDP 流量被安全组和防火墙允许;
  • 节点 MTU 足够;
  • 隧道端点发现信息正确;
  • 节点 IP 不发生未被控制器感知的变化。

七、Underlay:让底层网络直接承载 Pod 网络

1. Underlay 的定义

Underlay 网络是实际承载流量的底层网络。使用 Underlay 数据平面时,Pod IP 往往直接参与底层路由,或者被映射到云网络、物理网络可以识别的地址空间。

一种典型形式是:

Pod CIDR:10.244.0.0/16
节点 A 发布:10.244.1.0/24
节点 B 发布:10.244.2.0/24

底层网络设备知道如何把这些 Pod 网段送到相应节点。常见实现方式包括 BGP 或云厂商路由表。

2. BGP 在这里解决什么问题

BGP 是路由交换协议,不是 CNI,也不是加密协议。CNI 可以通过 BGP 将节点拥有的 Pod CIDR 发布给相邻路由器或其他节点。

示意:

节点 A ── BGP ── ToR/路由器
  发布:10.244.1.0/24

节点 B ── BGP ── ToR/路由器
  发布:10.244.2.0/24

当节点 A 的路由器收到目标为 10.244.2.8 的包时,最长前缀匹配到 10.244.2.0/24,然后转发到节点 B。

BGP 方案的优点是:

  • 不需要为每个远端节点增加 Overlay 封装;
  • 路径更接近普通三层网络;
  • 可以利用成熟的路由观测和故障诊断工具。

代价是:

  • 底层网络必须允许并正确处理这些路由;
  • 云环境中的 BGP、VPC 路由和安全策略未必开放;
  • 路由规模和收敛时间需要评估;
  • 跨网段、跨可用区、跨区域的行为依赖具体基础设施。

3. Underlay 不等于“没有 NAT”

Underlay 只描述承载方式,不自动决定 NAT 是否存在。例如:

  • Pod 到 Pod 可以不做 NAT;
  • Pod 到外部互联网可能需要 SNAT;
  • Pod 访问云服务可能经过云网络的源地址转换;
  • Service 外部访问可能经过 NodePort 或 LoadBalancer;
  • 出站网关可能集中执行 NAT。

所以不要把以下两句话当作等价关系:

使用 Underlay ⇒ 没有 NAT
使用 Overlay ⇒ 一定有 NAT

正确的判断方式是分别观察:

  1. Pod 到 Pod;
  2. Pod 到 Service;
  3. Pod 到节点;
  4. Pod 到集群外部;
  5. 外部到 Service;

每条路径的地址转换和转发规则可能不同。


八、Overlay 与 Underlay 的完整对比

维度 Overlay Underlay
Pod 地址是否需要底层直接认识 通常不需要 通常需要
节点间是否封装 通常是 通常不是
对底层网络要求 主要要求节点 IP 可达并允许封装 要求底层承载 Pod 路由或地址
MTU 需要扣除封装开销 通常更接近物理链路 MTU
部署适配性 对已有网络侵入较小 依赖底层网络能力
路径观测 需要同时看内层和外层 路由路径通常更直接
常见风险 MTU、隧道端点、封装协议被阻断 路由规模、云路由限制、收敛
是否天然更快 不能直接推导 不能直接推导
是否天然更安全 不能直接推导 不能直接推导

真正的选型问题不是“Overlay 好还是 Underlay 好”,而是:

现有底层网络能承载什么?
需要什么地址模型?
需要什么策略和可观测性?
节点规模与路由规模是多少?
是否需要跨云、跨区域或加密?
团队能维护哪一种故障模型?

九、Pod 网络与 Service 网络不是一回事

CNI 主要解决 Pod 网络接入,但 Kubernetes 应用通常通过 Service 通信。

假设:

Pod A:10.244.1.12
Pod B:10.244.2.8
Service ClusterIP:10.96.0.10

应用访问:

http://api.default.svc.cluster.local:8080

通常经历:

DNS 解析
  ▼
Service ClusterIP
  ▼
kube-proxy 或 eBPF Service datapath
  ▼
某个后端 Pod IP

Service 的 ClusterIP 通常是虚拟地址,不一定对应一个实际网卡。实现可能使用:

  • iptables;
  • IPVS;
  • eBPF;
  • 云厂商或其他代理机制。

因此需要区分:

CNI:Pod 接口、Pod IP、Pod 间基础可达性
Service datapath:ClusterIP、端口转发、后端选择
DNS:服务名称到 ClusterIP 的解析
Ingress/Gateway:HTTP/TLS 等应用层入口

一个常见误判是:Pod IP 互通,所以 Service 一定正常。实际上 Service 还可能单独出现:

  • EndpointSlice 没有后端;
  • Service selector 匹配不到 Pod;
  • 端口名或 targetPort 错误;
  • kube-proxy/eBPF 规则未收敛;
  • externalTrafficPolicy 导致源地址或节点行为不同;
  • NetworkPolicy 允许了 Pod IP,但没有正确考虑访问路径。

可以用以下命令拆分验证:

kubectl get svc api -n default -o wide
kubectl get endpointslice -n default \
  -l kubernetes.io/service-name=api
kubectl run netcheck --rm -it --restart=Never \
  --image=curlimages/curl -- sh

进入临时 Pod 后:

nslookup api.default.svc.cluster.local
curl -v http://api.default.svc.cluster.local:8080/
curl -v http://10.96.0.10:8080/

前置条件是集群允许创建临时 Pod,且镜像可以拉取。这里分别验证了 DNS、ClusterIP 和应用端口,不应只用一个 curl 结果判断整个网络栈。


十、NetworkPolicy 对 CNI 的影响

1. NetworkPolicy 不是所有 CNI 都自动实现

Kubernetes 定义了 NetworkPolicy API,但 API 对象存在不等于流量一定被限制。必须使用支持并启用 NetworkPolicy 执行的数据平面。

有些 CNI 只提供基础连通性;有些 CNI 同时实现 NetworkPolicy;有些环境使用独立策略组件。因此选型时必须确认:

  • 是否支持 Kubernetes NetworkPolicy;
  • 支持哪些字段和语义;
  • 是否支持 IPv4、IPv6、双栈;
  • 是否支持命名端口;
  • 是否支持主机网络、节点流量或额外扩展策略;
  • 策略变更的收敛行为和可观测性。

2. 规则是“允许集合”的累加

NetworkPolicy 的核心语义不是“某条策略拒绝某个请求”,而是对选中的 Pod 形成允许集合。

对于某个 Pod pp,定义:

  • I(p)I(p):所有适用于该 Pod 的 Ingress 策略允许集合;
  • E(p)E(p):所有适用于该 Pod 的 Egress 策略允许集合。

如果至少有一条 Ingress 策略选中 pp,则该 Pod 的入站流量进入隔离状态,只允许:

IngressAllowed(p)=iI(p)Rules(i)\text{IngressAllowed}(p) = \bigcup_{i \in I(p)} \text{Rules}(i)

Egress 同理:

EgressAllowed(p)=eE(p)Rules(e)\text{EgressAllowed}(p) = \bigcup_{e \in E(p)} \text{Rules}(e)

因此,多条 NetworkPolicy 一般是“允许规则相加”,不是按书写顺序从上到下匹配。默认拒绝通常通过创建一条选择目标 Pod、但没有允许来源或端口的策略实现。

3. 一个可运行的默认拒绝和放行示例

下面的命名空间和标签假设如下:

命名空间:app
后端 Pod:app=api
客户端 Pod:app=web

先创建默认拒绝:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Egress

应用:

kubectl apply -f default-deny.yaml

这里的 podSelector: {} 选择命名空间 app 中的所有 Pod。第一条使所有这些 Pod 的入站流量默认不再开放,第二条使其出站流量默认不再开放。

然后允许 web 访问 api:8080,并允许 api 访问集群 DNS:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-dns
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

关键语义是:

namespaceSelector:
  matchLabels:
    kubernetes.io/metadata.name: kube-system
podSelector:
  matchLabels:
    k8s-app: kube-dns

同一个 from 项中的 namespaceSelectorpodSelector 表示逻辑 AND:来源必须同时位于 kube-system 命名空间且具有 k8s-app=kube-dns 标签。

如果把它们写成两个列表项,则通常表示 OR:

from:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: kube-system
  - podSelector:
      matchLabels:
        k8s-app: kube-dns

这会放宽为“来自 kube-system 的 Pod,或任意命名空间中带有 k8s-app=kube-dns 的 Pod”。这是 NetworkPolicy 中非常容易造成误放行的结构差异。

验证对象是否存在:

kubectl get networkpolicy -n app
kubectl describe networkpolicy -n app allow-web-to-api

验证实际效果时,应从真实来源 Pod 发起请求,并分别检查:

  • 允许的 web → api:8080
  • 不允许的其他端口;
  • 不允许的其他来源;
  • api 是否还能解析 DNS;
  • 业务是否还需要访问 API、数据库或外部服务。

NetworkPolicy 只描述网络层/传输层的选择和放行模型,不能替代应用认证、TLS、身份授权或 API 网关策略。

4. NetworkPolicy 的边界

常见误解包括:

  • 以为策略按顺序执行;
  • 以为配置了 Ingress 就自动限制 Egress;
  • 以为允许一个 Service 名称就等于允许其所有后端和访问路径;
  • 以为策略一定能阻止节点上的 root 进程;
  • 以为所有 CNI 对同一个字段的扩展行为相同;
  • 以为只要 kubectl get networkpolicy 能看到对象,策略就已经生效。

此外,访问 Service 时,策略匹配的来源和目标可能受 kube-proxy、SNAT、外部流量策略以及 CNI 实现影响。遇到策略问题,应同时检查:

kubectl get networkpolicy -A
kubectl get pods -o wide -A
kubectl get svc,endpointslice -A

再结合插件提供的流量日志、策略状态或节点抓包确认实际源地址和目标地址。


十一、CRI、CNI 与 CSI 的责任边界

这三个缩写经常被并列讨论,但它们处理的是不同资源。

接口 主要对象 主要职责
CRI 容器运行时和 Pod sandbox 创建、启动、停止、删除容器和 sandbox
CNI 网络接口和网络地址 为 sandbox 接入网络、分配 IP、配置路由和策略
CSI 块存储、文件系统、卷 创建、挂载、格式化、扩容和卸载存储卷

一个 Pod 创建过程可以概括为:

kubelet
 ├─ 通过 CRI 创建 Pod sandbox
 │    └─ 运行时调用 CNI:网卡、IP、路由
 ├─ 通过 CSI 挂载声明的存储卷
 └─ 通过 CRI 启动业务容器

因此:

  • Pod 卡在 ContainerCreating,可能是 CNI、CSI、镜像、挂载或运行时问题;
  • Pod 没有 IP,优先查 CNI 和 IPAM;
  • Pod 有 IP 但无法访问数据库,优先查路由、策略、DNS、Service 或外部网络;
  • PVC 挂载失败不是 CNI 故障,除非存储系统本身依赖某条特定网络路径;
  • CNI 不负责创建 PVC,CSI 也不负责配置 Pod eth0

这个边界在诊断中很重要。否则容易在网络插件日志中寻找一个实际由存储插件返回的挂载错误。


十二、常见 CNI 类型与插件选型

1. 先按数据平面能力分类

选型前先回答以下问题,而不是先比较插件名称:

是否必须支持 NetworkPolicy?
是否需要双栈?
是否允许 Overlay?
是否需要 BGP 或云路由集成?
是否需要加密?
是否需要高性能 Service、负载均衡或可观测性?
是否需要多网络接口?
是否运行在公有云、裸机、边缘或混合网络?
团队是否能维护内核、路由和控制器?

2. 常见实现的定位

Flannel

Flannel 的重点是提供较简单的 Pod 网络连通性,常见部署使用 Overlay。它适合网络需求相对基础、希望降低初始复杂度的场景。

需要特别确认的是:Flannel 本身的 NetworkPolicy 能力并不是选择它时可以默认假定的能力,策略通常需要额外组件或其他方案实现。

Calico

Calico 可以提供路由型网络、Overlay、NetworkPolicy,以及在部分环境中的 BGP 集成。它的能力范围较广,但也意味着:

  • 路由模式和封装模式要明确;
  • BGP 对底层网络有要求;
  • 策略、IPAM、双栈和加密能力需要按具体版本确认;
  • 云厂商托管环境可能限制底层路由协议。

Cilium

Cilium 基于 eBPF 提供网络、Service、NetworkPolicy 和可观测能力,并可在不同模式下使用 Overlay 或原生路由。它对内核版本、eBPF 能力、节点操作系统和 kube-proxy 替代模式有更明确的环境要求。

不能仅凭“eBPF 性能高”做结论,实际效果取决于:

  • 内核和网卡特性;
  • 是否启用 kube-proxy replacement;
  • Service、策略和加密功能组合;
  • 包过滤和可观测配置;
  • 业务流量模式。

云厂商原生网络插件

公有云中的原生网络插件常把 Pod 接入云厂商 VPC,可能直接使用 ENI、辅助 IP 或云路由能力。它们通常在云内地址可达性、负载均衡和安全组集成方面更自然,但会受制于:

  • 每个节点可附加的网卡或 IP 数量;
  • 子网地址容量;
  • 跨可用区费用和路由;
  • 云厂商 API 配额;
  • 安全组和 NetworkPolicy 的双重语义;
  • 与其他云资源的地址重叠。

Multus

Multus 通常用于为 Pod 连接多个网络,例如一个默认集群网络加一个 SR-IOV、macvlan 或专用网络。它更像网络附件编排层,而不是单独替代所有基础网络能力。

使用多网络时必须明确:

  • 哪个接口是默认路由出口;
  • 哪个接口承载集群 Service;
  • 额外接口的 IPAM 由谁负责;
  • NetworkPolicy 是否覆盖额外网络;
  • Pod 删除时所有接口和地址是否都能清理;
  • 应用是否正确选择源地址和接口。

3. 选型时不要只看吞吐

至少应对以下指标做真实验证:

  • Pod 创建成功率和地址回收速度;
  • 节点扩缩容时的路由收敛;
  • 大包和长连接行为;
  • Pod 到 Pod、Pod 到 Service、Pod 到外部的延迟;
  • NetworkPolicy 生效和撤销时间;
  • 节点重启、网络插件重启、控制面短暂不可用时的表现;
  • 地址池耗尽和 IP 碎片化;
  • 跨节点、跨可用区和跨区域的成本;
  • 升级时 CNI 控制器和节点 DaemonSet 的兼容性;
  • 故障时是否能获得足够的流量和策略观测。

“插件支持某能力”还不够。要确认该能力在目标插件版本、内核、云环境和部署模式下真正可用。


十三、从现象到根因的诊断方法

1. 先确认 Pod 和节点的基本状态

kubectl get pods -A -o wide
kubectl get nodes -o wide
kubectl describe pod <pod> -n <namespace>

重点看:

  • Pod 是否有 podIP
  • Pod 是否处于 Running
  • 是否卡在 sandbox 创建;
  • 节点是否 Ready
  • Pod 被调度到哪台节点;
  • 事件中是否出现 CNI、IPAM 或 sandbox 错误。

如果 Pod 没有 IP,优先调查:

CNI 配置是否存在
→ CNI 二进制是否可执行
→ IPAM 地址池是否耗尽
→ 节点是否有足够地址
→ CNI DaemonSet 是否正常
→ 运行时是否能调用插件

2. 确认实际地址与路由

在 Pod 内:

kubectl exec -n <namespace> <pod> -- ip addr
kubectl exec -n <namespace> <pod> -- ip route

并非所有业务镜像都包含 ippingcurlnslookup。可以创建临时诊断 Pod:

kubectl run netshoot --rm -it --restart=Never \
  --image=nicolaka/netshoot -- bash

该镜像是否允许使用、能否拉取取决于集群策略和镜像仓库。诊断镜像本身不应长期留在生产命名空间。

检查:

ip addr
ip route
ip route get <目标 Pod IP>
ping <目标 Pod IP>
curl -v http://<目标 Pod IP>:<端口>/

ping 失败不必然说明 TCP 不通,因为 ICMP 可能被策略或防火墙阻断;反过来,ping 成功也不能证明目标端口有服务。

3. 区分本地故障、跨节点故障和 Service 故障

设计测试矩阵:

测试 目的
同节点 Pod → Pod IP 验证本地 veth、bridge、策略和端口
跨节点 Pod → Pod IP 验证节点间路由或 Overlay
Pod → Service ClusterIP 验证 Service datapath
Pod → Service DNS 额外验证 DNS
Pod → 外部 IP 验证出口路由、SNAT 和防火墙
外部 → LoadBalancer/NodePort 验证入口、节点策略和回程路径

如果同节点正常、跨节点失败,优先看:

  • 远端 Pod CIDR 路由;
  • Overlay 隧道;
  • 节点间安全组;
  • MTU;
  • 节点转发;
  • 跨节点 NetworkPolicy。

如果 Pod IP 正常、Service 失败,优先看:

  • Service selector;
  • EndpointSlice;
  • 端口映射;
  • kube-proxy 或 eBPF Service 状态;
  • Service 访问路径上的 SNAT 与策略。

4. 检查节点内核和接口

在节点上:

ip link
ip addr
ip route
sysctl net.ipv4.ip_forward

若使用 Overlay,还应检查对应隧道接口和邻居状态:

ip -d link
ip neigh

如果系统允许抓包,可以分别在 Pod 侧、节点侧和物理网卡侧观察:

tcpdump -ni any host <Pod IP>
tcpdump -ni eth0 host <节点 IP>

抓包位置很重要:

  • Pod 内能看到包发出,节点物理网卡看不到:本地数据平面或策略问题;
  • 物理网卡看到外层包,远端看不到:Underlay、防火墙或安全组问题;
  • 远端收到外层包但没有内层包:隧道解封装或端点状态问题;
  • 目标 Pod 收到请求但客户端失败:优先查回程路由、返回策略和应用响应。

生产环境抓包可能暴露业务数据,也可能增加节点负载,应限定接口、地址和时间范围。


十四、典型故障路径与反例

1. 地址池耗尽

现象:

Pod 长时间 Pending 或 ContainerCreating
事件中出现 IP allocation failed

错误推导是:

节点有 CPU 和内存
→ 所以 Pod 应该能启动

实际还需要:

节点有 CPU 和内存
+ 地址池有可分配 IP
+ CNI 能创建接口
+ IPAM 状态没有泄漏

恢复前应先确认是否只是短暂回收延迟。盲目删除 IPAM 状态可能造成重复分配,引发更严重的地址冲突。应优先按具体插件提供的检查和回收流程处理,并确认没有仍在使用的地址。

2. 只有大包失败

反例:

ping -c 3 <目标> 成功
curl 小响应成功
上传大文件失败

这不能证明网络完整正常。原因可能是:

内层 MTU+封装头>Underlay MTU\text{内层 MTU} + \text{封装头} > \text{Underlay MTU}

小包未触发限制,大包触发分片或被丢弃。应测试不同大小的 DF 包,并检查:

  • Pod 接口 MTU;
  • 节点物理接口 MTU;
  • 隧道配置;
  • ICMP Packet Too Big 是否被放行;
  • TCP MSS 调整是否正确。

3. 节点能访问 Pod,Pod 却不能访问节点

Kubernetes 网络模型通常要求节点与 Pod 可通信,但具体实现可能包含:

  • 节点侧防火墙;
  • 主机网络策略;
  • 云安全组;
  • rp_filter;
  • 路由回程问题;
  • 主机服务只监听 127.0.0.1

因此不能把“节点能 curl Pod”当成“双向网络已验证”。必须从 Pod 发起反向测试,并检查服务监听地址与返回路径。

4. 删除 Pod 后旧 IP 仍然出现在观测中

Pod 删除涉及多个异步状态:

API 对象删除
→ kubelet 停止容器
→ runtime 删除 sandbox
→ CNI DEL
→ IPAM 回收
→ 节点路由/端点状态收敛
→ 监控和连接跟踪老化

短时间内看到旧连接、旧端点或旧路由,不一定表示地址被重新错误分配。真正危险的情况是:

  • IPAM 认为地址已释放,但旧接口仍存在;
  • 新 Pod 获得旧 IP,而旧连接状态未清理;
  • 远端节点仍保留过期端点;
  • 控制器状态与内核数据平面不一致。

这类问题需要同时看 Kubernetes 对象、CNI 状态、节点接口、路由和连接跟踪,不能只看 kubectl get pods -o wide


十五、生产环境中的关键取舍

地址规划

Pod CIDR、Service CIDR、节点网段和外部网络不能随意重叠。例如:

节点网段:10.0.0.0/16
Pod CIDR:10.244.0.0/16
Service CIDR:10.96.0.0/12

如果 Pod CIDR 与某个需要访问的数据库网段重叠,路由最长匹配可能把数据包送到错误接口。地址规划一旦进入生产,迁移成本通常远高于部署前重新规划。

双栈与 IPv6

双栈不是简单地“再配置一个 CIDR”。还需要同时验证:

  • Pod 是否获得 IPv4 和 IPv6;
  • Service 是否有对应地址族;
  • CNI、kube-proxy/eBPF 和 NetworkPolicy 是否都支持;
  • 节点和 Underlay 是否转发 IPv6;
  • 出站 NAT、DNS 和外部负载均衡是否支持;
  • 应用是否正确处理 IPv6 优先连接和回退。

不同插件和云厂商的双栈支持范围存在版本差异,必须以目标版本文档和实测为准。

加密

Overlay、Underlay 和加密是三个独立维度:

Overlay:是否封装
Underlay:由谁承载
Encryption:链路上的数据是否加密

可以有:

  • Overlay 但不加密;
  • Underlay 路由但使用 IPsec/WireGuard 等加密;
  • Overlay 同时加密;
  • 只在特定节点间或跨集群链路加密。

加密会增加 CPU、MTU 和故障排查复杂度。需要验证密钥轮换、节点重启、连接迁移和性能,而不能仅以“配置已启用”作为完成标准。

可观测性

至少要能回答:

这个 Pod 的 IP 是什么?
它在哪个节点?
目标 Pod 在哪个节点?
节点如何路由目标 IP?
是否经过隧道?
是否发生 NAT?
哪条 NetworkPolicy 允许或拒绝?
Service 最终选择了哪个 Endpoint?
包在哪一跳消失?

如果只能看到应用超时,而看不到策略命中、端点映射、隧道状态或路由变化,故障恢复会严重依赖猜测。


十六、建立一套可复用的判断框架

遇到 CNI 网络故障时,可以按以下逻辑逐层缩小范围:

1. Pod sandbox 是否创建成功?
   ├─ 否:查 CRI、CNI、IPAM、节点插件
   └─ 是

2. Pod 是否有 IP、接口和默认路由?
   ├─ 否:查 CNI ADD、本地 namespace、IPAM
   └─ 是

3. 同节点 Pod IP 是否可达?
   ├─ 否:查 veth、bridge、节点策略、端口
   └─ 是

4. 跨节点 Pod IP 是否可达?
   ├─ 否:查 Pod CIDR 路由、Overlay、Underlay、MTU
   └─ 是

5. Service ClusterIP 是否可达?
   ├─ 否:查 EndpointSlice、Service datapath、端口映射
   └─ 是

6. Service DNS 是否正常?
   ├─ 否:查 CoreDNS、DNS NetworkPolicy、search domain
   └─ 是

7. 外部访问或出站是否正常?
   ├─ 否:查 SNAT、云路由、安全组、网关和回程路径
   └─ 是

8. 只有特定来源或端口失败?
   └─ 查 NetworkPolicy 和实际源地址

这个顺序的价值在于避免把所有问题都归因于“CNI”。CNI 负责的是一段关键路径,但 Pod 网络还包含 Linux 内核、节点路由、Service 转发、DNS、策略、云网络和应用监听。


结语

Pod IP 是端点地址,路由是找到端点的规则,Overlay 是对 Pod 流量进行封装的承载方式,Underlay 是实际传输这些数据的底层网络。CNI 把 Pod 接入这套网络,但它不等于 Service、DNS、NetworkPolicy、CRI 或 CSI。

理解一条 Pod 到 Pod 的连接,至少要能还原:

Pod network namespace
→ veth 或其他接入方式
→ 节点本地数据平面
→ Pod CIDR 路由或 Overlay 端点
→ 节点间 Underlay
→ 目标节点解封装或本地转发
→ 目标 Pod
→ 返回路径

插件选型也应从这条数据流出发:底层网络能否承载、地址池如何规划、是否需要策略和加密、故障如何观测、升级和扩缩容如何收敛。只有把这些机制和边界都验证清楚,才能判断一个 CNI 是否适合目标集群,而不是只根据插件名称或单次连通性测试做决定。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。