土法炼钢兴趣小组的算法知识备份

Flannel:最简单的 Overlay 网络,以及它的天花板

目录

你搭了一个三节点的 K8s 集群,kubeadm init 完成后第一件事是什么?装 CNI 插件。 你搜了一圈,教程里十有八九写的是 kubectl apply -f kube-flannel.yml。 三分钟后集群 Ready,Pod 能互相 ping 通,你觉得网络就这么回事。

直到有一天——

这时候你才意识到:Flannel 做了什么,没做什么,以及为什么它能成为”最简单的 CNI”——因为它只解决了一个问题,并且有意识地不去解决其他问题。

本文的目标:

  1. 拆清楚 Flannel 的设计哲学与架构。
  2. 深度分析三种 backend(VXLAN / host-gw / WireGuard)的数据路径。
  3. 动手实验:分别用 VXLAN 和 host-gw 部署,用 tcpdump 抓封装包做对比。
  4. 讲明白 Flannel 的天花板在哪里,什么时候该换掉它。

实验环境:Ubuntu 22.04, kernel 6.5, kind v0.20+ 或两台 VM(同一 L2 子网)。Flannel v0.25.x。


一、Flannel 的设计哲学:只做 L3 Overlay,不碰别的

Flannel 项目诞生于 2014 年,由 CoreOS 团队创建,目标极其明确:

给每个节点分配一个子网,让所有 Pod 的 IP 在集群范围内可达。

就这么一句话。它不做 NetworkPolicy,不做 BGP,不做 eBPF 加速,不做服务网格集成。它只干一件事——L3 Overlay(或在特定条件下 L3 直连路由)。

这种”做减法”的设计哲学带来了独特优势:代码量小(flanneld 核心不到一万行 Go),部署简单(一个 DaemonSet + 一个 ConfigMap,无 CRD),依赖少(直接用 Kubernetes API 作后端存储),学习曲线平缓。

但”简单”是有代价的。我们先看它做了什么,再看它没做什么。


二、Flannel 架构与核心组件

Flannel 的架构可以用三个组件概括:

子网分配:flanneld

flanneld 以 DaemonSet 的形式运行在每个节点上。它的核心职责:

  1. 启动时从 Kubernetes API(或 etcd)获取集群的 PodCIDR 配置(如 10.244.0.0/16)。
  2. 为本节点申请一个子网(如 Node-1 得到 10.244.0.0/24,Node-2 得到 10.244.1.0/24)。
  3. 将子网信息写入 /run/flannel/subnet.env,供 CNI 插件读取。
  4. 根据选定的 backend,配置本节点的网络设备(VTEP、路由表、FDB 表等)。
  5. 持续 watch 其他节点的子网变化,动态更新本节点的路由和转发规则。

查看 flanneld 写入的子网信息:

cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

CNI 插件:flannel-cni

Flannel 的 CNI 插件实际上是一个 delegate 插件——它不直接配置网络,而是读取 /run/flannel/subnet.env 中的信息,然后调用 bridge CNI 插件来完成实际的 Pod 网络配置。

典型的 CNI 配置(/etc/cni/net.d/10-flannel.conflist):

{
  "name": "cbr0",
  "cniVersion": "1.0.0",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

这意味着每个 Pod 的网络命名空间里会有:

cni0 网桥

每个节点上会创建一个 cni0 Linux 网桥,所有本节点 Pod 的 veth pair 都挂在这个网桥上。同节点 Pod 之间的通信直接通过 cni0 二层转发,不经过任何 Overlay。

# 查看 cni0 网桥及其挂载的 veth
brctl show cni0

# 输出示例:
# bridge name    bridge id          STP enabled    interfaces
# cni0           8000.a2b3c4d5e6f7  no             veth1234abcd
#                                                  veth5678efgh
#                                                  vethdeadbeef

关键点:Flannel 只负责跨节点通信。同节点通信走 cni0 网桥,与 Flannel 的 backend 无关。


三、VXLAN Backend 深度拆解

VXLAN 是 Flannel 的默认 backend,也是最常用的模式。要理解它,你需要先理解 VXLAN 本身。

VXLAN 协议回顾

VXLAN(Virtual Extensible LAN)是一个 L2-over-L3 的隧道协议,定义在 RFC 7348。核心思想:

封装格式(从外到内):

+----------------------------------------------+
| Outer Ethernet  (src/dst: 节点物理 MAC)      |
+----------------------------------------------+
| Outer IP  (src: 192.168.1.10  dst: .20)      |
+----------------------------------------------+
| Outer UDP  (src: 随机  dst: 8472)            |
+----------------------------------------------+
| VXLAN Header (8 bytes, VNI=1)                |
+----------------------------------------------+
| Inner Ethernet  (src/dst: Pod MAC)           |
+----------------------------------------------+
| Inner IP  (src: 10.244.0.2  dst: 10.244.1.3)|
+----------------------------------------------+
| Inner Payload (TCP/UDP/ICMP...)              |
+----------------------------------------------+

VXLAN 封装额外开销:50 字节(14 外层以太网 + 20 外层 IP + 8 UDP + 8 VXLAN)。这就是为什么 Flannel VXLAN 模式下 MTU 默认设为 1450 而不是 1500。

flanneld 如何配置 VTEP

当 flanneld 以 VXLAN backend 启动时,它会:

第一步:创建 VXLAN 设备 flannel.1

ip link add flannel.1 type vxlan \
  id 1 \
  dev eth0 \
  dstport 8472 \
  nolearning

第二步:配置 flannel.1 的 IP 地址

ip addr add 10.244.0.0/32 dev flannel.1

注意这里用的是 /32,不是子网掩码。flannel.1 不负责本地子网的路由,它只是一个隧道端点。

第三步:为远端节点添加路由、ARP 和 FDB 条目

这是最关键的部分。假设 Node-2 的子网是 10.244.1.0/24,物理 IP 是 192.168.1.20flannel.1 的 MAC 是 aa:bb:cc:02:02:02

# 路由:到 10.244.1.0/24 的包走 flannel.1,下一跳是 Node-2 的 VTEP IP
ip route add 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink

# ARP:告诉内核 10.244.1.0 的 MAC 地址(静态 ARP,不用广播)
ip neigh add 10.244.1.0 lladdr aa:bb:cc:02:02:02 dev flannel.1 nud permanent

# FDB:告诉 VXLAN 设备这个 MAC 对应的远端 VTEP IP
bridge fdb append aa:bb:cc:02:02:02 dev flannel.1 dst 192.168.1.20

这三条命令构成了 Flannel VXLAN 的控制面三件套。flanneld 通过 watch Kubernetes API 中其他节点的 Node Annotations 和 Lease 信息,动态维护这些条目。

查看当前的 FDB 表:

bridge fdb show dev flannel.1
aa:bb:cc:02:02:02 dst 192.168.1.20 self permanent
aa:bb:cc:03:03:03 dst 192.168.1.30 self permanent

查看 ARP 缓存:

ip neigh show dev flannel.1
10.244.1.0 lladdr aa:bb:cc:02:02:02 PERMANENT
10.244.2.0 lladdr aa:bb:cc:03:03:03 PERMANENT

一个跨节点包的完整旅程

Flannel VXLAN 跨节点数据路径

Pod-A(10.244.0.2,在 Node-1)要发包给 Pod-B(10.244.1.3,在 Node-2)。完整路径:

  1. Pod-A 的 eth0 发出 IP 包,目的 IP 10.244.1.3。默认路由指向 10.244.0.1cni0 网桥)。
  2. cni0 网桥收到包,做 L3 路由查找。内核查路由表发现:10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
  3. 包被路由到 flannel.1。内核需要知道 10.244.1.0 的 MAC 地址——查 ARP 缓存,命中 flanneld 预设的静态条目:aa:bb:cc:02:02:02
  4. VXLAN 封装。内核在 flannel.1(VXLAN 设备)上执行封装:
    • 构造内层以太网帧(src MAC = flannel.1 的 MAC,dst MAC = aa:bb:cc:02:02:02)。
    • 查 FDB 表:aa:bb:cc:02:02:02 -> 192.168.1.20
    • 构造外层 UDP 包(src IP = 192.168.1.10,dst IP = 192.168.1.20,dst port = 8472)。
    • 添加 VXLAN Header(VNI=1)。
  5. 外层 IP 包通过物理网络发送到 Node-2
  6. Node-2 的内核收到 UDP 8472 包,识别为 VXLAN 流量,交给 flannel.1 设备处理。
  7. VXLAN 解封装。剥掉外层头部,还原出内层 IP 包(10.244.0.2 -> 10.244.1.3)。
  8. 内核路由查找10.244.1.0/24 dev cni0。包被转发到 cni0 网桥。
  9. cni0 查 FDB 找到 Pod-B 的 veth 端口,包最终到达 Pod-B 的 eth0

整个过程中,内核态完成所有封装/解封装,flanneld 不在数据路径上——它只负责控制面(同步路由、ARP、FDB)。

封装开销量化

每个跨节点包的额外开销:

层次 字节数 说明
Outer Ethernet 14 物理网卡的以太网头
Outer IP 20 节点物理 IP 头(无 Options)
Outer UDP 8 目标端口 8472
VXLAN Header 8 VNI + Flags
总计 50

这意味着:如果物理网络 MTU 是 1500,则 Pod 内可用 MTU 是 1450。如果物理网络支持 Jumbo Frame(MTU 9000),Pod MTU 可以设到 8950。每个包的封装/解封装都在内核态软中断上下文执行,有额外 CPU 开销。


四、host-gw 模式:更快的代价

VXLAN 的封装开销不算大,但如果你的节点都在同一个二层网络里,为什么还要套一层隧道?

host-gw 的工作原理

host-gw(Host Gateway)模式完全不做隧道封装。它的核心思想极其简单:

把对端节点的物理 IP 作为网关,直接添加路由。

假设集群有两个节点:

节点 物理 IP Pod 子网
Node-1 192.168.1.10 10.244.0.0/24
Node-2 192.168.1.20 10.244.1.0/24

在 host-gw 模式下,flanneld 在 Node-1 上添加的路由是:

ip route add 10.244.1.0/24 via 192.168.1.20 dev eth0

在 Node-2 上添加的路由是:

ip route add 10.244.0.0/24 via 192.168.1.10 dev eth0

就这么简单。没有 flannel.1 设备,没有 VXLAN 封装,没有 FDB 表。Pod 的 IP 包直接通过物理网络路由到目标节点。

为什么要求 L2 直连

host-gw 模式有一个硬性前提:所有节点必须在同一个 L2 广播域(即同一子网)内。

原因在于路由表的 via 语义。当内核看到:

10.244.1.0/24 via 192.168.1.20 dev eth0

它需要把包发给 192.168.1.20。为了做到这一点,内核必须能够通过 ARP 解析出 192.168.1.20 的 MAC 地址,然后在 eth0 上发送一个以太网帧。这要求 192.168.1.20 和本机在同一个 L2 网络中——中间不能有路由器。

如果两个节点跨了 L3(中间有路由器),ARP 请求到不了对端,路由就失败了。

# host-gw 可行的拓扑:
Node-1 (192.168.1.10) ---[L2 Switch]--- Node-2 (192.168.1.20)

# host-gw 不可行的拓扑:
Node-1 (192.168.1.10) ---[Router]--- Node-2 (10.0.2.20)

host-gw 的性能优势

没有封装意味着零额外字节开销(Pod MTU = 物理 MTU 1500)、零封装 CPU 开销、更低的延迟。实测对比(两台物理机,万兆网卡,iperf3 TCP 单流):

指标 VXLAN host-gw 差异
吞吐 ~9.2 Gbps ~9.8 Gbps +6.5%
延迟 (P50) ~45 us ~32 us -29%
CPU (softirq) ~18% ~12% -33%
Pod MTU 1450 1500 +50 bytes

在万兆网络上差异已经可观。在更高速的 25G/100G 网络上,VXLAN 封装的 CPU 开销会成为更显著的瓶颈。

host-gw 的配置

修改 Flannel 的 ConfigMap:

{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "host-gw"
  }
}

查看 flanneld 配置的路由:

ip route show | grep -E '10\.244\.[0-9]+\.0/24'
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 192.168.1.20 dev eth0
10.244.2.0/24 via 192.168.1.30 dev eth0

注意:本节点的子网(10.244.0.0/24)走 cni0,远端节点的子网走物理网卡 eth0 并以对端节点 IP 为网关。


五、WireGuard Backend:加密 Overlay 的性能权衡

从 Flannel v0.23.0 开始,WireGuard 成为正式支持的 backend。它的目标是在不引入 IPsec 复杂性的前提下,提供加密的 Overlay 网络。

WireGuard 的工作原理

WireGuard 是一个内核态 VPN 隧道协议:代码量约 4000 行,使用现代密码学(ChaCha20、Poly1305、Curve25519),每个 peer 只需一对公私钥,Linux 5.6+ 原生支持。

在 Flannel WireGuard backend 下,flanneld 在每个节点上创建 flannel.1 WireGuard 接口,为远端节点配置 peer(公钥 + Endpoint),路由表指向该接口。

wg show flannel.1
interface: flannel.1
  public key: aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7=
  listening port: 51820

peer: cD5eF7gH9iJ1kL3mN5oP7qR9sT1uV3wX5yZ7aB9=
  endpoint: 192.168.1.20:51820
  allowed ips: 10.244.1.0/24
  transfer: 1.23 GiB received, 4.56 GiB sent

封装格式

WireGuard 的封装比 VXLAN 更简洁:

+----------------------------------------------+
| Outer IP  (src: 192.168.1.10  dst: .20)      |
+----------------------------------------------+
| Outer UDP  (src/dst: 51820)                  |
+----------------------------------------------+
| WireGuard Header (32 bytes)                  |
|   Type + Counter + Encrypted Payload         |
+----------------------------------------------+
| (加密的) Inner IP Packet                     |
+----------------------------------------------+

封装开销:约 60 字节(20 IP + 8 UDP + 32 WireGuard Header),比 VXLAN 多 10 字节。Flannel WireGuard 模式默认 MTU 为 1420。

性能权衡

加密不是免费的。实测对比(x86 平台):

指标 VXLAN WireGuard host-gw
吞吐 (iperf3) ~9.2 Gbps ~7.8 Gbps ~9.8 Gbps
延迟 (P50) ~45 us ~55 us ~32 us
CPU (per Gbps) ~2.0% ~3.5% ~1.2%
Pod MTU 1450 1420 1500
数据加密

WireGuard 的吞吐量比 VXLAN 低约 15%,延迟高约 22%。但它提供了线路加密——在不信任底层网络的场景(如跨数据中心、公有云 VPC 之间)这是必要的安全保障。

配置 WireGuard Backend

{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "wireguard"
  }
}

前提条件:内核版本 >= 5.6(或安装 WireGuard 内核模块),wg 工具已安装(apt install wireguard-tools)。


六、三种 Backend 全面对比

在选择 backend 之前,先看一张对比表:

维度 VXLAN host-gw WireGuard
封装协议 VXLAN (UDP 8472) 无(纯路由) WireGuard (UDP 51820)
网络要求 L2 或 L3 均可 必须 L2 直连 L2 或 L3 均可
数据加密
Pod MTU 损失 50 bytes 0 60-80 bytes
CPU 开销 中等(封装/解封装) 最低(纯路由) 最高(加密/解密)
吞吐量 较高 最高 较低
延迟 中等 最低 较高
额外内核设备 flannel.1 (VXLAN) flannel.1 (WireGuard)
适用场景 通用,跨子网 同子网,追求性能 不信任底层网络

决策路径:节点同一 L2 + 不需加密 -> host-gw跨 L3 -> VXLAN需要线路加密 -> WireGuard


七、flanneld 的控制面:它到底 watch 了什么

很多人以为 flanneld 只是”起动时配一下路由就完事了”。实际上它是一个持续运行的控制面守护进程。

Kubernetes API 交互

flanneld 通过 Kubernetes API 完成两件事:

1. 读取节点的 PodCIDR

kubectl get node node-1 -o jsonpath='{.spec.podCIDR}'
10.244.0.0/24

这个值由 kube-controller-manager 的 --cluster-cidr--node-cidr-mask-size 参数决定。

2. 通过 Node Annotations 交换 backend 信息

flanneld 把自己的 backend 信息写入 Node Annotations,并 watch 所有节点的 Annotations 变化:

kubectl get node node-1 -o jsonpath='{.metadata.annotations}' | python3 -m json.tool
{
  "flannel.alpha.coreos.com/backend-data": "{\"VNI\":1,\"VtepMAC\":\"aa:bb:cc:01:01:01\"}",
  "flannel.alpha.coreos.com/backend-type": "vxlan",
  "flannel.alpha.coreos.com/public-ip": "192.168.1.10"
}

当新节点加入、节点 IP 变化或节点被删除时,flanneld 立即更新本节点的路由、ARP、FDB 条目。

故障场景:flanneld 挂了会怎样

flanneld 只负责控制面。进程挂掉后,已有的路由、FDB 条目和跨节点连接不受影响(数据面在内核态运行)。但新节点加入或节点 IP 变化时,路由不会更新——直到 flanneld 恢复。


八、实验:VXLAN vs host-gw 抓包对比

理论讲完了,动手做实验。我们在同一个环境下分别部署 VXLAN 和 host-gw 模式的 Flannel,用 tcpdump 抓包对比。

环境准备

使用两台 VM(或两个 kind 节点),确保它们在同一 L2 子网:

Node-1: 192.168.1.10  PodCIDR: 10.244.0.0/24
Node-2: 192.168.1.20  PodCIDR: 10.244.1.0/24

在两个节点上各运行一个测试 Pod:

# 在 Node-1 上调度 pod-a
kubectl run pod-a --image=nicolaka/netshoot --overrides='{"spec":{"nodeName":"node-1"}}' -- sleep infinity

# 在 Node-2 上调度 pod-b
kubectl run pod-b --image=nicolaka/netshoot --overrides='{"spec":{"nodeName":"node-2"}}' -- sleep infinity

获取 Pod IP:

kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE
pod-a   1/1     Running   0          30s   10.244.0.2   node-1
pod-b   1/1     Running   0          30s   10.244.1.3   node-2

实验一:VXLAN 模式抓包

在 Node-1 的物理网卡上抓包:

# 只抓目标是 Node-2 的 UDP 8472 流量
tcpdump -i eth0 -nn udp port 8472 -c 5

在 Pod-A 中 ping Pod-B:

kubectl exec pod-a -- ping -c 3 10.244.1.3

抓包结果:

14:23:01.123456 IP 192.168.1.10.52731 > 192.168.1.20.8472: VXLAN, flags [I] (0x08), vni 1
IP 10.244.0.2 > 10.244.1.3: ICMP echo request, id 42, seq 1, length 64
14:23:01.123789 IP 192.168.1.20.38492 > 192.168.1.10.8472: VXLAN, flags [I] (0x08), vni 1
IP 10.244.1.3 > 10.244.0.2: ICMP echo reply, id 42, seq 1, length 64

关键观察:

验证封装细节:

# 查看两层以太网头(外层物理网卡 MAC + 内层 VTEP MAC)
tcpdump -i eth0 -nn -e udp port 8472 -c 1

# 验证路由和 FDB
ip route get 10.244.1.3
bridge fdb show dev flannel.1
ip neigh show dev flannel.1

实验二:host-gw 模式抓包

切换 Flannel 到 host-gw 模式后重复实验。

修改 ConfigMap 并重启 Flannel DaemonSet:

kubectl edit configmap kube-flannel-cfg -n kube-flannel
# 将 "Type": "vxlan" 改为 "Type": "host-gw"

kubectl rollout restart daemonset kube-flannel-ds -n kube-flannel

等待所有 flanneld Pod 重启完成后,验证路由:

ip route show | grep 10.244
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 192.168.1.20 dev eth0

注意区别:via 192.168.1.20 dev eth0——直接通过物理网卡路由,不经过 flannel.1

在 Node-1 的物理网卡上抓包:

# 抓目标是 Pod-B 子网的 ICMP 包
tcpdump -i eth0 -nn icmp -c 5

在 Pod-A 中 ping Pod-B:

kubectl exec pod-a -- ping -c 3 10.244.1.3

抓包结果:

14:25:01.456789 IP 10.244.0.2 > 10.244.1.3: ICMP echo request, id 43, seq 1, length 64
14:25:01.457012 IP 10.244.1.3 > 10.244.0.2: ICMP echo reply, id 43, seq 1, length 64

关键观察:

对比总结

维度 VXLAN 抓包 host-gw 抓包
物理网卡上的协议 UDP 8472 ICMP(原始协议)
可见的 IP 地址 外层:节点 IP;需解封才看到 Pod IP 直接看到 Pod IP
单个 ICMP ping 的物理帧大小 148 bytes 98 bytes
tcpdump 过滤器 udp port 8472 直接按 Pod IP 或协议过滤
flannel.1 设备 存在(VXLAN 类型) 不存在
FDB 表 有条目
路由下一跳 flannel.1 eth0 via 对端节点 IP

九、Flannel 的局限与天花板

Flannel 的简单是它的优势,也是它的天花板。以下是它有意不支持的能力:

没有 NetworkPolicy

这是 Flannel 最被诟病的缺失。Kubernetes NetworkPolicy 是集群内流量隔离的标准 API,但 Flannel 完全不实现它。

如果你 kubectl apply 了一个 NetworkPolicy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

这个 Policy 会被 Kubernetes API Server 接受并存储,但 不会有任何效果——因为没有控制器去读取它并配置相应的 iptables/eBPF 规则。

Flannel 官方文档明确说明:

Flannel is focused on networking. For network policy, other projects such as Calico can be used.

如果你需要 NetworkPolicy,有两个选择:

  1. Flannel + Calico Policy-Only 模式(又称 Canal):用 Flannel 做网络,Calico 只负责 Policy 执行。
  2. 换成 Calico 或 Cilium:让一个 CNI 同时负责网络和 Policy。

没有 BGP

Flannel 的路由同步完全依赖 Kubernetes API。不能把 Pod 网段宣告给外部路由器,不能实现 Pod IP 的外部直达。需要这些能力时,应选择 Calico 的 BGP peering 或 Cilium 的 BGP Control Plane。

没有 eBPF 数据面

Flannel 的数据面完全基于传统 Linux 网络栈(iptables、Linux Bridge、VXLAN 内核模块)。不使用 eBPF,因此无法替代 kube-proxy、无法做 conntrack-free Service 转发、无法利用 eBPF 的可编程性做高级流量观测。

没有高级负载均衡

Flannel 不参与 Service 层面的流量转发——完全由 kube-proxy(iptables 或 IPVS 模式)负责。Flannel 只管 Pod-to-Pod 可达性。

有限的可观测性

Flannel 没有像 Cilium Hubble 那样的可观测平台,也没有 per-flow 监控。你能看到的只有 flanneld 日志和标准的 Linux 网络统计(ip -s link show flannel.1)。

单一的 IPAM 策略

Flannel 使用固定的 per-node 子网分配(默认 /24)。不支持按 namespace 分配 IP 池、多 IP 池、或指定 Pod 使用特定 IP 范围。


十、什么场景下 Flannel 仍然是最佳选择

说了这么多局限,Flannel 是不是该淘汰了?不是。在以下场景中,Flannel 仍然是合理甚至最佳的选择:

开发和测试环境

在 minikube、kind、k3s 等轻量级 K8s 环境中,Flannel 是最常见的默认 CNI。原因很简单:

小规模生产集群

对于节点数 < 50、不需要多租户隔离的小集群:

边缘计算和 IoT

在资源极度受限的边缘节点上:

对网络栈有强定制需求

如果你需要在 CNI 之外叠加自己的网络方案(比如用 Multus 做多网卡、用 SR-IOV 做高性能网络),Flannel 作为”底座 CNI”的简单性反而是优势——它不会和你的定制方案产生冲突。

决策流程图

你需要 NetworkPolicy 吗?
  |
  +-- 是 --> Calico / Cilium(或 Flannel + Calico Policy 的 Canal 方案)
  |
  +-- 否 --> 你的节点跨 L3 吗?
               |
               +-- 是 --> 需要加密? -> WireGuard / 不需要 -> VXLAN
               +-- 否 --> host-gw

迁移路径

当 Flannel 的天花板成为瓶颈时,常见的升级路线:


十一、常见问题与调试

Pod 跨节点不通

按以下顺序排查:

# 1. flanneld 是否正常
kubectl get pod -n kube-flannel -o wide

# 2. 路由是否正确
ip route show | grep 10.244

# 3. flannel.1 设备是否 UP(VXLAN 模式)
ip link show flannel.1

# 4. FDB / ARP 是否有远端条目
bridge fdb show dev flannel.1
ip neigh show dev flannel.1

# 5. 物理网络连通性 + 防火墙
ping -c 3 <对端节点 IP>
iptables -L -n | grep 8472

MTU 问题

VXLAN 模式下 MTU 不一致会导致”ping 通但 curl 超时”——大包被丢弃或碎片化。

# 检查各层 MTU 是否一致(都应该是 1450)
kubectl exec pod-a -- ip link show eth0
ip link show cni0
ip link show flannel.1

# 用 Don't Fragment 标志测试边界
kubectl exec pod-a -- ping -c 3 -s 1422 -M do 10.244.1.3  # 1422+28=1450,应成功
kubectl exec pod-a -- ping -c 3 -s 1423 -M do 10.244.1.3  # 1423+28=1451,应失败

十二、Flannel 源码导读

关键源码路径(基于 v0.25.x),backend/vxlan/vxlan.go 中的核心流程:

// RegisterNetwork 是 VXLAN backend 的入口
func (be *VXLANBackend) RegisterNetwork(ctx context.Context, ...) (backend.Network, error) {
    devAttrs := vxlanDeviceAttrs{
        vni:       uint32(cfg.VNI),
        name:      fmt.Sprintf("flannel.%d", cfg.VNI),
        vtepIndex: be.extIface.Iface.Index,
        vtepAddr:  be.extIface.IfaceAddr,
        port:      cfg.Port,
    }
    dev, err := newVXLANDevice(&devAttrs)
    dev.Configure(ipa.IP, flannelnet)
    go be.handleSubnetEvents(ctx, sn)
}
// handleSubnetEvents 处理子网的增删改——"控制面三件套"的代码实现
func (nw *network) handleSubnetEvents(batch []subnet.Event) {
    for _, event := range batch {
        switch event.Type {
        case subnet.EventAdded:
            nw.dev.AddARP(neighbor{IP: sn.IP, MAC: mac})
            nw.dev.AddFDB(neighbor{IP: sn.IP, MAC: mac, VtepIP: vtepIP})
            netlink.RouteReplace(&route)
        case subnet.EventRemoved:
            nw.dev.DelARP(neighbor{...})
            nw.dev.DelFDB(neighbor{...})
            netlink.RouteDel(&route)
        }
    }
}

十三、总结:一张表回顾 Flannel 的全貌

维度 Flannel 的答案
设计哲学 只做 L3 Overlay / 路由,不做 Policy
控制面 flanneld DaemonSet,watch Kubernetes API
数据面 内核态(VXLAN 模块 / 路由表 / WireGuard)
VXLAN 模式 UDP 8472 封装,50 字节额外开销,不要求 L2 直连
host-gw 模式 纯路由,零开销,要求 L2 直连
WireGuard 模式 加密隧道,约 60 字节额外开销,不要求 L2 直连
NetworkPolicy 不支持(需要搭配 Calico 或换 CNI)
BGP 不支持
eBPF 不使用
可观测性 基本的设备统计和日志
最佳场景 开发测试、小集群、边缘计算、K3s
天花板 大规模多租户、安全合规、高级流量管理

Flannel 就像一把螺丝刀——它不是瑞士军刀,但当你需要拧螺丝的时候,它比任何其他工具都顺手。理解它的能力边界,在正确的场景使用它,不要在错误的场景强求它,这才是工程上的正确态度。

下一篇我们进入 Calico:BGP、iptables 与 eBPF 三种数据面的深度对比,看一个”什么都能做”的 CNI 是如何构建的——以及”什么都能做”带来的复杂性代价。


附录 A:快速参考命令

# Flannel 配置与状态
kubectl get configmap kube-flannel-cfg -n kube-flannel -o yaml
kubectl logs -n kube-flannel -l app=flannel -f
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.flannel\.alpha\.coreos\.com/backend-type}{"\n"}{end}'

# VXLAN 设备与转发表
ip -d link show flannel.1
bridge fdb show dev flannel.1
ip neigh show dev flannel.1
ip route show | grep -E '10\.244|flannel'

# 抓包与 MTU 测试
tcpdump -i eth0 -nn udp port 8472
ping -c 3 -s 1422 -M do <Pod-IP>

附录 B:Flannel ConfigMap 模板

{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "vxlan",
    "VNI": 1,
    "Port": 8472,
    "DirectRouting": false
  }
}

DirectRouting 设为 true 时,同一子网的节点之间使用 host-gw(直接路由),跨子网时才用 VXLAN 封装——兼顾性能和跨子网兼容性。

附录 C:延伸阅读


By .