你搭了一个三节点的 K8s 集群,kubeadm init
完成后第一件事是什么?装 CNI 插件。
你搜了一圈,教程里十有八九写的是
kubectl apply -f kube-flannel.yml。
三分钟后集群 Ready,Pod 能互相 ping
通,你觉得网络就这么回事。
直到有一天——
- 你需要 NetworkPolicy 隔离租户流量,发现 Flannel 根本不支持。
- 你用
tcpdump在宿主机上抓包,发现每个跨节点的 ICMP 包外面裹着一层 UDP,MTU 被吃掉了 50 字节。 - 你做性能测试,VXLAN 模式下 iperf3 带宽比裸机低了 15%,延迟高了 30 微秒。
这时候你才意识到:Flannel 做了什么,没做什么,以及为什么它能成为”最简单的 CNI”——因为它只解决了一个问题,并且有意识地不去解决其他问题。
本文的目标:
- 拆清楚 Flannel 的设计哲学与架构。
- 深度分析三种 backend(VXLAN / host-gw / WireGuard)的数据路径。
- 动手实验:分别用 VXLAN 和 host-gw 部署,用
tcpdump抓封装包做对比。 - 讲明白 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
的形式运行在每个节点上。它的核心职责:
- 启动时从 Kubernetes API(或 etcd)获取集群的 PodCIDR
配置(如
10.244.0.0/16)。 - 为本节点申请一个子网(如 Node-1 得到
10.244.0.0/24,Node-2 得到10.244.1.0/24)。 - 将子网信息写入
/run/flannel/subnet.env,供 CNI 插件读取。 - 根据选定的 backend,配置本节点的网络设备(VTEP、路由表、FDB 表等)。
- 持续 watch 其他节点的子网变化,动态更新本节点的路由和转发规则。
查看 flanneld 写入的子网信息:
cat /run/flannel/subnet.envFLANNEL_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 的网络命名空间里会有:
- 一个
eth0(veth pair 的一端) - 默认路由指向
cni0网桥的 IP(如10.244.0.1)
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。核心思想:
- 把一个完整的二层以太网帧封装进 UDP 包里。
- 外层使用节点的物理 IP 做路由(underlay),内层是虚拟网络的帧(overlay)。
封装格式(从外到内):
+----------------------------------------------+
| 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 \
nolearningid 1:VNI 设为 1。dev eth0:指定 underlay 出口设备。dstport 8472:Linux 内核 VXLAN 使用 8472 而非 IANA 标准的 4789。nolearning:禁用 MAC 地址自动学习——Flannel 自己管理 FDB,不靠数据面学习。
第二步:配置 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.20,flannel.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.1aa: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.110.244.1.0 lladdr aa:bb:cc:02:02:02 PERMANENT
10.244.2.0 lladdr aa:bb:cc:03:03:03 PERMANENT
一个跨节点包的完整旅程
Pod-A(10.244.0.2,在 Node-1)要发包给
Pod-B(10.244.1.3,在 Node-2)。完整路径:
- Pod-A 的
eth0发出 IP 包,目的 IP10.244.1.3。默认路由指向10.244.0.1(cni0网桥)。 cni0网桥收到包,做 L3 路由查找。内核查路由表发现:10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink。- 包被路由到
flannel.1。内核需要知道10.244.1.0的 MAC 地址——查 ARP 缓存,命中 flanneld 预设的静态条目:aa:bb:cc:02:02:02。 - 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)。
- 构造内层以太网帧(src MAC = flannel.1 的 MAC,dst MAC =
- 外层 IP 包通过物理网络发送到 Node-2。
- Node-2 的内核收到 UDP 8472 包,识别为
VXLAN 流量,交给
flannel.1设备处理。 - VXLAN 解封装。剥掉外层头部,还原出内层
IP 包(
10.244.0.2 -> 10.244.1.3)。 - 内核路由查找:
10.244.1.0/24 dev cni0。包被转发到cni0网桥。 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.1interface: 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 wideNAME 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
关键观察:
- 外层是 UDP 包,目标端口 8472。
flags [I]和vni 1是 VXLAN 标识。- 内层可以看到 Pod IP 和 ICMP 协议。
- 每个 ICMP 包在物理网络上的大小 = 64 + 20(内层 IP)+ 14(内层 Eth)+ 8(VXLAN)+ 8(UDP)+ 20(外层 IP)+ 14(外层 Eth)= 148 字节。
验证封装细节:
# 查看两层以太网头(外层物理网卡 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.24410.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
关键观察:
- 没有外层 UDP 封装——物理网卡上直接看到 Pod IP。
- 包的大小 = 64(ICMP payload)+ 20(IP header)+ 14(Ethernet header)= 98 字节,比 VXLAN 模式少了 50 字节。
- 抓包过滤器用的是
icmp而不是udp port 8472——因为包在物理网络上就是原始的 ICMP。
对比总结
| 维度 | 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,有两个选择:
- Flannel + Calico Policy-Only 模式(又称 Canal):用 Flannel 做网络,Calico 只负责 Policy 执行。
- 换成 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。原因很简单:
- 安装快,配置少,不容易出错。
- 资源开销小——flanneld 的内存占用通常低于 30 MiB。
- 不需要 NetworkPolicy 或高级网络功能。
- 文档和社区资源丰富,排错容易。
小规模生产集群
对于节点数 < 50、不需要多租户隔离的小集群:
- Pod 网络可达性是唯一的硬需求。
- 没有合规要求强制 NetworkPolicy。
- 运维团队人手有限,无法投入精力学习 Calico/Cilium 的复杂概念。
- 出了网络问题,调试 Flannel 比调试 Calico BGP 或 Cilium eBPF 简单一个数量级。
边缘计算和 IoT
在资源极度受限的边缘节点上:
- flanneld 的 CPU 和内存开销远低于 Calico 或 Cilium。
- Cilium 要求内核 >= 5.10 且需要 BPF JIT 支持,很多边缘设备不满足。
- K3s 默认使用 Flannel,就是因为它适合边缘场景。
对网络栈有强定制需求
如果你需要在 CNI 之外叠加自己的网络方案(比如用 Multus 做多网卡、用 SR-IOV 做高性能网络),Flannel 作为”底座 CNI”的简单性反而是优势——它不会和你的定制方案产生冲突。
决策流程图
你需要 NetworkPolicy 吗?
|
+-- 是 --> Calico / Cilium(或 Flannel + Calico Policy 的 Canal 方案)
|
+-- 否 --> 你的节点跨 L3 吗?
|
+-- 是 --> 需要加密? -> WireGuard / 不需要 -> VXLAN
+-- 否 --> host-gw
迁移路径
当 Flannel 的天花板成为瓶颈时,常见的升级路线:
- Flannel -> Calico:保持 cluster CIDR 不变,逐节点替换。注意 IPAM 模式差异。
- Flannel -> Cilium:要求内核 >=
5.10,使用
cilium-preflight检查前置条件。 - Canal(折中方案):网络层保持 Flannel,Calico Felix 只负责 NetworkPolicy 的 iptables 规则。最小化变更。
十一、常见问题与调试
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 8472MTU 问题
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:延伸阅读