目录
Please enable Javascript to view the contents

K8s网络-Flannel详解

 ·  ☕ 13 分钟

系列导航

本系列从 Pod 网络连通入手,逐步展开到 K8s 网络全景。

① 概念 → ② Flannel → ③ Calico → ④ 流量路径 → ⑤ Cilium → ⑥ 对比 → ⑦ 排障 ‖ ⑧ 开发 → ⑨ 多网卡 → ⑩ AI 演进

顺序文章定位
概念与入门基础——主机网络、Docker 网络、CNI 标准、方案分类树
本篇 - Flannel 详解CNI 实现——Overlay 封装(UDP/VXLAN/HostGW)与抓包
Calico 详解CNI 实现——三层路由(BGP/IPIP)+ NetworkPolicy
流量路径全解析全貌——Pod/Service/Ingress/Egress,揭示 CNI 的边界
Cilium 详解超越 CNI——eBPF 统一 Pod + Service + L7 + Hubble
插件对比与选型选型——5 插件横向对比 + 决策树
排障思路与常用命令运维——工具链 + 场景排查 + 性能
核心路径 ↑扩展展望 ↓
CNI 插件开发指南扩展——基于 CNI 规范开发自定义插件
多网卡方案详解进阶——Multus + SR-IOV/ipvlan 多网口实战
AI 时代网络演进展望——GPU 网络、eBPF 加速、未来方向

Flannel 由 CoreOS 开发,是 k8s 最成熟的开源 CNI 插件之一,通过一个三层的 IPv4 Overlay 网络运行——跨集群每个节点的大型内部网络,每个节点分配一个子网。

1. Flannel架构

▸ 组件

flanneld —— 每个节点上的核心进程,作为 DaemonSet 部署,负责子网划分、子网租赁管理,使用 Etcd 或 K8S API 存储状态。

cni0 —— Linux 网桥,取代 docker0。每个 Pod 的 veth pair 对端插入 cni0,降级为网桥端口。同节点 Pod 间通信走 CAM 表二层转发。

后端(Backend) —— 负责跨节点报文的具体转发方式。Flannel 提供三种后端:UDP(用户态 Overlay)、VXLAN(内核态 Overlay)、Host-GW(路由模式)。

▸ 原理

每个节点从全局子网池中租赁一个子网段(如 10.244.1.0/24),节点内 Pod 从该段分配 IP。跨节点 Pod 通信时,数据路径为:

Pod eth0 → veth pair → cni0 网桥 → 宿主机路由 → 后端封装/转发 → 对端宿主机 → cni0 → veth pair → 目标 Pod

与 Calico 的关键区别:Flannel 的 veth pair 对端必须插入 cni0 网桥,数据先经过二层 CAM 转发后才能进入宿主机协议栈。这种"网桥+路由"双层结构在跨主机时引入了额外的封装/解封复杂度。

2. Flannel后端模式

Flannel 解决的核心问题:跨节点 Pod 发送 IP 包时,原始 Pod 报文如何到达对端节点? 三种后端给出三种答案:

模式分类通路封装位置性能前提条件
UDPOverlay(L3)flanneld 进程间 UDP 通信 + TUN 设备 flannel0用户态最差(3次状态切换)
VXLANOverlay(L2)内核态 VTEP (flannel.1) + VXLAN 隧道 + FDB 转发内核态良好(~50字节开销)内核支持 VXLAN
Host-GWUnderlay(路由)静态路由,flanneld 直接修改宿主机路由表无封装最高(裸机性能)所有节点 L2 互通

Flannel 的三种后端按数据路径分为两类:Overlay 封装(UDP、VXLAN)和 Underlay 路由(Host-GW)。

2.1 Overlay: UDP 模式

通路:flanneld 进程间 UDP 通信 + TUN 设备 flannel0——各节点 flanneld 组成 Overlay 网桥,用户态封包解包,实现最简单,性能最差。

封装结构:|HostMAC|HostIP|UDP|内层IP|Data|,IP 头(20B)+ UDP 头(8B)= 28B 开销。

2.1.1 组件概念

flannel0:TUN 设备

flannel0 是一个 TUN 设备(三层虚拟网络设备),在内核和应用程序(flanneld 进程)之间传递 IP 包:

方向流向行为
宿主机 → flannel0内核态 → 用户态内核态 IP 包交给创建该设备的应用程序(flanneld 进程)
flanneld → flannel0用户态 → 内核态flanneld 进程写入的 IP 包,出现在宿主机网络栈,按路由表流转

2.1.2 封装流程

五段流程

1
S.容器 → S.docker0 → S.flanneld → D.flanneld → D.docker0 → D.容器

1. S.容器 → S.docker0

默认路由 → veth pair → docker0 三层接口上交宿主机协议栈

1.1 容器内,默认路由,即 docker0 的 IP == 网关 IP,通过容器 eth0 发出,目标 IP = 目的容器 IP,目标 MAC = docker0.MAC
1.2 容器 eth0 → veth-pair → docker0 端口 vethxx
1.3 docker0 发现目标 MAC == 自身 MAC,发往 S.宿主机网络栈。

2. S.docker0 → S.flanneld 进程

宿主机路由匹配 Overlay 网段 → flannel0 TUN(内核→用户)→ flanneld 进程,内核剥 L2 头

2.1 到达 S.宿主机网络栈后,根据路由(flanneld 维护),目标 IP(目的容器 IP) ∈ Overlay 网段,发往 flannel0 设备(TUN);
2.2 根据 flannel0 设备的 TUN 特性,IP 包进入 flanneld 进程(注意,内核此时会剥离原始报文的 L2 头,将 IP 报文发给 flannel0)。

3. S.flanneld → D.flanneld

查 Etcd 获取目标宿主机 IP:8285 → UDP 封装(负载=原始 L3-L7)→ 经宿主机网络发往 D.flanneld

3.1 S.flanneld 进程,根据目的容器 IP 结合 Etcd 中节点-子网对应关系,获得目的宿主机 IP,目的端口 = 8285

外层包头:根据宿主机网络 L2-L4 封包,完全按宿主机网络两个 UDP 进程通信进行封包。

UDP 负载:容器网络原始报文的 L3-L7(因为 TUN 设备直接走三层,内核将报文发送 TUN 设备时,会剥离 L2 头)。

3.2 经宿主机网络,到达目的宿主机 UDP 进程——D.flanneld 进程。(注意:进入目标主机内核后,会重新 ARP 缓存或 ARP 请求,根据目标 IP 取得目标 MAC,再封装二层头。此行为属于步骤 4.3。)

4. D.flanneld → D.docker0

剥离外层得原始报文 → flannel0(用户→内核)→ 宿主机路由 → docker0,封装 L2 头

4.1 D.flanneld 进程收到数据时,D.宿主机网络栈已剥离 L1-L4 协议头,得到原始报文(L3 层报文,目标 IP = 目的容器 IP。跟 S.flanneld 收到的一模一样);
4.2 D.flanneld 进程,将 IP 包发往 D.flannel0,IP 报文进入 D.宿主机网络栈;
4.3 D.宿主机网络栈,根据路由(flanneld 维护),目标 IP(目的容器 IP) ∈ 当前节点子网,出设备 = D.docker0。

docker0 是一个网桥(二层设备),要通过它发送,必须封装以太网帧头。D.宿主机网络栈封装 L2 帧头,通过 ARP 请求或 ARP 缓存获取目的容器 MAC,并封装。

5. D.docker0 → D.容器

docker0 CAM 查端口 → veth pair → 目标容器 eth0

5.1 docker0,根据目的容器 MAC,查 CAM 表,转发至指定端口 veth;
5.2 端口 veth → veth-pair → 进入目的容器 eth0

+---------------------------------------+                                +-------------------------------------+
| S.Host (192.168.1.1)                  |                                | D.Host (192.168.1.2)                |
|                                       |                                |                                     |
|   [S.Container] eth0  10.244.1.2      |                                |   [D.Container] eth0  10.244.2.3    |
|     [1] route: default->docker0.1     |                                |     [17] veth pair -> recv pkt      |
|         ARP:   D.MAC = docker0.MAC    |                                |                                     |
|         (user->kernel, 1st switch)    |                                |                                     |
|     [2] veth pair                     |            [2] [16]            |     [16] veth pair                  |
|     container.eth0 <-> docker0.port   |                                |     docker0 port <-> container eth0 |
|                                       |                                |                                     |
|   [S.docker0]  IP:10.244.1.1          |                                |   [D.docker0]  IP:10.244.2.1        |
|               CAM: port <-> MAC       |                                |               CAM: port <-> MAC     |
|     [3] D.MAC==self -> L3 iface       |            [3]  [15]           |     [15] CAM: MAC -> port veth      |
|     TO: S.Host net stack              |                                |                                     |
|                                       |                                |                                     |
|   [S.Host net stack]                  |                                |   [D.Host net stack]                |
|     [4] route: 10.244.2.0/24          |            [4]  [14]           |     [14] route: 10.244.2.3          |
|         -> flannel0 (flanneld preset) |                                |          in subnet -> docker0       |
|         kernel strips L2 hdr          |                                |       docker0 L2 bridge, add L2     |
|                                       |                                |       ARP get Container MAC         |
|                                       |                                |                                     |
|   [S.flannel0 TUN]  L3 tunnel         |                                |   [D.flannel0 TUN]  L3 tunnel       |
|     [5] kernel->user (2nd switch)     |            [5]  [13]           |     [13] user->kernel               |
|         IP pkt -> flanneld process    |                                |          IP pkt -> D.Host net stack |
|                                       |                                |                                     |
|   [S.flanneld] user space             |                                |   [D.flanneld] user space           |
|     [6] etcd: 10.244.2.3              |            [6]  [12]           |     [12] write orig IP -> flannel0  |
|            -> 192.168.1.2:8285        |                                |          -> kernel net stack        |
|      encap UDP payload=L3-L7          |                                |                                     |
|                                       |                                |                                     |
|   [UDP pkt] [7] user->kernel (3rd)    |            [7]  [11]           |   [D.Host kernel]                   |
|            Src 192.168.1.1:rand       |                                |     [11] strip L2-L4 -> UDP 8285    |
|            Dst 192.168.1.2:8285       |                                |          -> D.flanneld              |
|                                       |                                |       orig IP == S.flanneld pkt     |
|                                       |                                |                                     |
|   [S.Host net stack]                  |                                |   [D.Host eth0]                     |
|     [8] add L2 hdr: ARP next-hop      |            [8]  [10]           |     [10] recv L2 frame              |
|                                       |                                |                                     |
|                                       |            [9]  [10]           |                                     |
|                                       |                                |                                     |
|                                       | routers only see outer headers |                                     |
|   [S.Host eth0]                       |                                |                                     |
|     [9] send L2 frame                 |               [9]              |                                     |
|                                       |                                |                                     |
+---------------------------------------+                                +-------------------------------------+

步骤说明

编号位置规则 / 动作
[1]S.Container eth0路由决策: 查路由表,目标 10.244.2.3 不在本机子网 → 匹配默认路由,下一跳 = docker0 IP(10.244.1.1)。发起 ARP 获取 docker0 MAC,封装 L2 帧(目标 MAC = docker0.MAC,目标 IP = 10.244.2.3)。第一次状态切换:用户态→内核态。
[2]veth pair容器 eth0 → veth pair → docker0 网桥上的端口 vethxx。veth 插入网桥后降级为端口,只收不发,由 docker0 决定转发。
[3]S.docker0目标 MAC 判断: 收到帧 → 目标 MAC = 自身 MAC → docker0 充当三层接口,上交 S.Host 协议栈处理。
[4]S.Host net stack路由决策: 查路由表(flanneld 预置),目标 10.244.2.3 ∈ Overlay 网段 → 出设备 = flannel0内核剥 L2 头,将 IP 包发往 flannel0
[5]S.flannel0 (TUN)TUN 设备特性: 内核态 IP 包 → 交给创建该设备的应用程序(flanneld 进程)。第二次状态切换:内核态→用户态。
[6]S.flanneld查 Etcd: 10.244.2.3 → D.Host 192.168.1.2,D.Port 8285UDP 封装: UDP 负载 = 原始报文的 L3-L7(因为 TUN 设备直接走三层,内核发 TUN 时已剥 L2)。外层按宿主机网络两个 UDP 进程通信封 L3-L4 头(源 IP=192.168.1.1,目标 IP=192.168.1.2,目标端口=8285)。
[7]encapsulated状态切换: flanneld 进程 → S.Host eth0。第三次状态切换:用户态→内核态。
[8]S.Host net stack封 L2 头: 查路由表/ARP,确定下一跳 MAC(同子网=对端宿主机 MAC,跨子网=网关 MAC)→ 封装以太网帧头。
[9]S.Host eth0L2 帧从 eth0 发出,进入物理网络。
[10]Physical Network标准 IP 路由转发。中间设备只看外层包头(源/目标 MAC 和 IP),不知道内层 Pod IP。
[11]Physical Network帧到达 D.Host 192.168.1.2eth0
[12]D.Host kernel拆包: 内核依次剥 L2 头(目标 MAC = 自身)、L3 头(目标 IP = 自身)→ L4 UDP 层 → 目标端口 8285 → 交给 D.flanneld 进程。此时网络栈已剥离 1-4 层协议头,得到的原始报文跟 S.flanneld 收到的一模一样。
[13]D.flanneld将原始 IP 包写入 flannel0 → IP 报文进入 D.Host 网络栈。
[14]D.flannel0状态切换:用户态→内核态。 IP 包按 D.Host 路由表流转。
[15]D.Host net stack路由决策: 目标 10.244.2.3 ∈ 本机 Pod 子网 (10.244.2.0/24) → 出设备 = docker0docker0 是二层网桥,须先封装 L2 头 → 查 ARP 缓存(或发 ARP 请求)获取 D.Container MAC → 封装以太网帧头(源 MAC=docker0.MAC,目标 MAC=D.Container MAC)。
[16]D.docker0CAM 转发: 查 CAM 表,目标 MAC → 对应端口 veth → 从该端口发出。
[17]veth pairveth pair → 进入 D.Container eth0。容器协议栈逐层解包,收到完整数据。

2.1.3 性能分析

为什么性能差?三次内核态↔用户态切换

发送端:
  第一次:容器进程 → 容器 eth0               用户态 → 内核态
          之后 eth0→vethxx→docker0→flannel0 一直在内核态
  第二次:flannel0 → flanneld 进程            内核态 → 用户态  ← TUN 特性
  第三次:flanneld 进程 → 宿主机 eth0         用户态 → 内核态
UDP 模式仅用于演示原理,生产环境绝对不要用。

2.2 Overlay: VXLAN 模式

通路:内核态 VTEP 设备 flannel.1 + VXLAN 隧道 + FDB 转发表,封装/解包全在内核中完成。

封包格式:|HostMAC|HostIP|UDP|VXLAN|内层MAC|内层IP|Data|,总计 ~50B 开销(VTEP 帧头 14 + VXLAN 头 8 + UDP 头 8 + IP 头 20)。

VXLAN (Virtual Extensible LAN) 是 Linux 内核原生支持的 Overlay 协议,采用内核态标准协议,所有封装/解包在内核中完成,实际传输速度比 UDP 模式快许多。flanneld 服务在每个节点监听端口 8472(VXLAN),其他节点用随机端口连接。

VNI (VXLAN Network Identifier): Flannel 默认使用 VNI 1。同一 VNI 内的节点属于同一个 L2 广播域。

2.2.1 组件概念

flannel.1 = VTEP

flannel.1 是一个 VTEP(VXLAN Tunnel End Point)设备,工作在内核态,负责 VXLAN 封装和解封。

与普通网桥(cni0 / docker0)的区别:

特性普通网桥(cni0)VTEP(flannel.1)
转发表CAM 表(端口 ↔ MAC)FDB 表(远端 VTEP MAC ↔ 远端宿主机 IP)
是否记录 IP是,FDB 记录远端宿主机 IP
封装能力无封装,纯二层转发内核态 VXLAN 封装/解封

三层表

flanneld 进程在每个节点预置三类表项,构成完整的数据平面:

内容作用查看命令
路由表远端子网 → flannel.1,网关 = 远端 VTEP IP将跨节点 Pod 流量引向 VTEP 设备ip route
ARP 表远端 VTEP IP → 远端 VTEP MACVTEP 间邻居发现,回答"对端 VTEP 的 MAC 是什么"ip neigh show dev flannel.1
FDB 表远端 VTEP MAC → 远端宿主机物理 IP回答"对端 VTEP 在哪台宿主机上"bridge fdb show dev flannel.1
1
2
3
4
# 示例:查看 FDB 表
bridge fdb show flannel.1 | grep <目标VTEP.MAC>
# <目标VTEP.MAC> dev flannel.1 dst 10.168.0.3 self permanent
# 含义:发往该 VTEP MAC 的二层帧,通过 flannel.1 设备发出,发往物理 IP=10.168.0.3 的节点
关键改进: 封装/解封装全在内核态完成(通过 flannel.1 虚拟网卡),无需像 UDP 模式那样经过用户态 flanneld 进程,性能比 UDP 方案提升约 50%。

2.2.2 封装流程

完整封装流程

以源容器 → 目的容器为例:

步骤1:源容器发出 IP 包
  S.容器 eth0 → veth pair → S.宿主机 cni0 网桥
  → 路由表匹配:D.容器IP ∈ 远端子网

步骤2:路由引向 VTEP
  路由规则(flanneld 预置):
  10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
  → IP 包发往 S.flannel.1,下一跳 = 远端 VTEP IP

步骤3:VTEP 内层二层封装
  flannel.1 查 ARP 表:远端 VTEP IP → 远端 VTEP MAC
  封装内层以太网帧头:S.VTEP.MAC + D.VTEP.MAC

步骤4:VTEP 封 VXLAN 头
  添加 VNI = 1,形成 VXLAN 报文

步骤5:VTEP 封外层 UDP+IP 头
  查 FDB 表:D.VTEP.MAC → D.宿主机.IP
  源 IP = S.宿主机.IP, 目标 IP = D.宿主机.IP
  目标端口 = 8472

步骤6:宿主机二层发出
  查路由表/ARP,封装外层以太网帧(D.宿主机.MAC),从 eth0 发出

步骤7:目的宿主机解封装
  D.宿主机 eth0 → 内核依次解外层 MAC/IP/UDP
  → 识别 VXLAN 端口 8472 → 根据 VNI=1 交给 flannel.1

步骤8:VTEP 剥离内层头
  flannel.1 剥离 VTEP 内层 MAC 头、VXLAN 头,还原原始 IP 包

步骤9:路由到目标容器
  查路由表,目标 IP ∈ 本机 Pod 子网 → 走 cni0
  → ARP 获取容器 MAC → 封装二层头 → cni0 CAM 转发
  → veth pair → D.容器 eth0
VXLAN 协议只定义了数据封装与 FDB 转发机制。Flannel 作为控制平面,负责利用 Etcd 同步信息,并具体化为宿主机上的路由、FDB、ARP 表项。

2.2.3 抓包验证

抓包验证

如下面一条抓包数据所示——源 192.168.1.1 节点的 pod [10.42.1.2] ping 目的 192.168.1.2 节点的 pod [10.42.2.6],抓源主机 enp0s3 设备的 UDP 包:

1
2
3
192.168.1.215.38381 > 192.168.1.216.8472: [bad udp cksum 0x11bc -> 0x6334!] OTV, flags [I] (0x08), overlay 0, instance 1
f2:60:ca:96:7f:1f > 4a:02:36:26:f9:c1, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 34781, offset 0, flags [DF], proto TCP (6), length 60)
10.42.0.0.10384 > 10.42.1.7.443: Flags [S], cksum 0x1589 (incorrect -> 0x6701), seq 1841703998, win 64240, options [mss 1460,sackOK,TS val 3365946307 ecr 0,nop,wscale 7], length 0
  • 192.168.1.1.39338 > 192.168.1.2.8472:外层 UDP 包,源端口 39338,目标端口 8472(VXLAN 协议端口)
  • f2:60:ca:96:7f:1f > 4a:02:36:26:f9:c1:外层以太网帧,源/目标 MAC 为两台宿主机 enp0s3 网卡的 MAC
  • 10.42.1.2.10384 > 10.42.2.6.443:内层 TCP 包,这是 Pod 之间真实的通信——源 IP 10.42.1.2,目标 IP 10.42.2.6

2.3 Underlay: Host-GW 模式

通路:宿主机路由表——flanneld 直接将 Pod 子网路由写入宿主机路由表,无任何封装,性能等于裸机网络。

2.3.1 组件概念

与前两种 Overlay 模式不同,Host-GW 采用主机路由方案——既然跨节点不通是因为节点间没有路由信息,而 Flannel 知道这些信息,就直接把这个信息告诉网络上的节点。

Flannel 通过在各个节点上的 Agent 进程,将容器网络的路由信息刷到主机路由表上,所有主机关联整个容器网络的路由数据。

Host-Gateway 没有像 Overlay 中额外的装包解包操作,完全是普通的网络路由机制,效率与虚拟机直连通信相差无几。

然而,由于 Flannel 只能修改各个主机的路由表,一旦主机隔了其他路由设备(如三层路由器),数据包会在路由设备上被丢弃。因此 Host-Gateway 只能用于二层直接可达的网络。

2.3.2 主要流程

工作原理:
1. 每个节点的 Pod 子网在 Etcd 注册
2. flanneld 在每个节点的主机路由表中增加到其他节点子网的静态路由
3. 路由的下一跳就是目标节点的主机 IP
4. Pod 跨节点流量直接按主机路由转发,无任何封装
性能最高,等于裸机网络,零封装开销。 代价是要求所有节点必须在同一个 L2 广播域(跨 VLAN、跨机房、云环境无法使用)。

2.3.3 路由验证

假设两个节点:

1
2
节点1:宿主机 IP = 192.168.1.1,Pod 子网 = 10.244.1.0/24
节点2:宿主机 IP = 192.168.1.2,Pod 子网 = 10.244.2.0/24

flanneld 在节点1 的路由表中写入:

1
2
3
[root@node1 ~]# ip route
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1   # 本机 Pod 子网(直连)
10.244.2.0/24 via 192.168.1.2 dev eth0                          # 远端 Pod 子网 → 节点2

flanneld 在节点2 的路由表中写入:

1
2
3
[root@node2 ~]# ip route
10.244.2.0/24 dev cni0 proto kernel scope link src 10.244.2.1   # 本机 Pod 子网(直连)
10.244.1.0/24 via 192.168.1.1 dev eth0                          # 远端 Pod 子网 → 节点1

节点1 的 Pod(10.244.1.5)访问节点2 的 Pod(10.244.2.8)时:匹配 10.244.2.0/24 via 192.168.1.2 → 下一跳 = 192.168.1.2 → 查 ARP 获取节点2 的 MAC → 封装 L2 帧从 eth0 发出。无任何隧道封装,数据包 = 原始 Pod IP 包。

3. 流量加密

默认情况下,封装的流量不加密。Flannel 提供两种加密方案:

  • IPSec: 使用 strongSwan 在节点间建立加密 IPSec 隧道,实验性后端
  • WireGuard: 比 strongSwan 更快的替代方案

4. 总结

维度UDPVXLANHost-GW
封装开销28B~50B0
封装位置用户态内核态无封装
性能最差良好最高
部署要求无特殊要求内核支持 VXLAN节点 L2 互通
适用场景仅演示原理公有云、虚拟化环境,生产通用同 L2 广播域的自建机房
控制方式flanneld 进程转发VTEP 内核隧道路由表

VXLAN 是 Flannel 的默认和推荐模式。UDP 模式仅作演示理解原理,Host-GW 在不跨网段的场景下性能最优。

参考链接

Comparing CNI providers – Flannel

Comparing CNI providers – Flannel

分享

Hex
作者
Hex
CloudNative Developer