VXLAN 覆盖网络与 OVS 流表
本文延续前一篇《网络协议栈与路由机制》的脉络,从物理网络延伸到虚拟化场景下的覆盖网络。阅读本文需要具备基本的 IP 路由和以太网概念。
核心概念
后续章节会频繁引用以下术语。
| 术语 | 定义 |
|---|---|
| 覆盖网络(Overlay Network) | 在已有物理网络之上,通过封装技术构建的虚拟网络 |
| Underlay | 承载覆盖网络流量的物理网络基础设施 |
| 广播域(Broadcast Domain) | 一个二层广播帧(目标 MAC 为 FF:FF:FF:FF:FF:FF)能到达的所有设备的集合,三层转发边界(如路由器)是其边界 |
| VLAN(Virtual Local Area Network) | 通过 VLAN ID 将交换端口划入不同广播域的技术,ID 为 12 位,上限 4096 个 |
| VNI(VXLAN Network Identifier) | VXLAN 的网络标识,24 位,用于区分不同的虚拟网络 |
| OVS(Open vSwitch) | Linux 上的软件交换机,支持 OpenFlow 流表 |
| 流表(Flow Table) | OVS 中”匹配条件→动作”的规则列表,控制包的转发与策略 |
| SDN 控制器 | 集中管理网络设备的控制程序,负责向交换机下发流表 |
为什么需要覆盖网络
虚拟化环境中,一台物理机上可能运行数十甚至数百个虚拟机(VM)或容器。这些租户的 IP 地址由虚拟化平台分配,对底层物理网络完全透明。物理交换机不感知这些虚拟 IP 的存在,自然无法直接转发。
VLAN 可以在同一台交换机上划分不同的广播域,但其 ID 只有 12 位,上限 4096 个,且受限于二层网络无法跨越路由器。VXLAN(Virtual eXtensible LAN,RFC 7348)用封装技术解决了这两个限制:
- VNI 为 24 位,支持约 1677 万个虚拟网络
- 通过在 UDP 包中封装二层帧,可以跨越三层网络传输
VXLAN 封装原理
数据包结构
一个虚拟机发出的原始以太网帧与普通帧无异。VXLAN 在其外层依次封装四个报文头:
Outer Ethernet Header (物理 MAC)
Outer IP Header (物理 IP)
Outer UDP Header (目标端口 4789)
VXLAN Header (8 bytes, 含 VNI)
Original Frame (原始以太网帧,一字不改)
展开来看:
┌──────────────────────────────────────────────────────────┐
│ Outer Ethernet: SrcHost MAC → DstHost MAC │
│ Outer IP: SrcHost IP → DstHost IP │
│ Outer UDP: Random Port → Dst Port 4789 │
│ VXLAN Header: Flags=0x08, VNI=0x0000C8, Reserved=0 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Original Ethernet: VM-A MAC → VM-B MAC │ │
│ │ Original IP: VM-A IP → VM-B IP │ │
│ │ Payload │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
外层报文头使用物理网络的地址,交换机和路由器按常规方式转发。到达目标物理机后,内核识别 UDP 端口 4789,剥离外层,将原始帧交给本地虚拟交换机处理。
VNI 的作用
VNI 的定义见上文「核心概念」表格,本节展开其两个核心职责:
- 接收端解复用:远端物理机解封装后,根据 VNI 决定把原始帧交给哪个虚拟网桥处理。在端到端流程的示例中,接收端 OVS 用
VNI=200 + 目标 IP两个维度定位目标端口。 - 二层广播域隔离:同一个 VNI 下的虚拟机属于同一个广播域,不同 VNI 的流量在隧道中完全隔离,即使共享同一套物理网络。
VNI 的本质是二层广播域的标识,而不是租户标识。一个租户往往对应多个 VNI,原因如下:
- 租户内部的网络拓扑需要分段(如 DMZ、应用层、数据层),各段之间要求二层隔离
- 合规要求某些子网之间不能直接二层通信
- 一个租户跨越多个 VNI 后,跨 VNI 通信需要经过三层网关(在广播域之间做 IP 路由转发的设备),可以在网关上叠加路由策略和访问控制,比纯二层更安全
不同平台的映射粒度各不相同。OpenStack Neutron 默认一个网络(network)对应一个 VNI;部分 Kubernetes CNI 插件(如 Calico VXLAN 模式)也按 IP 池(IPPool)粒度分配不同的 VNI。
封装和解封装的执行者
整个过程在内核态完成,没有用户态进程参与:
| 步骤 | 执行者 |
|---|---|
| 虚拟机发出原始帧 | 虚拟机的内核网络栈 |
| 虚拟交换机查表决策 | OVS 内核模块(openvswitch.ko) |
| 封装 VXLAN 外层 | OVS 内核模块(自行管理隧道时)或 Linux 内核 VXLAN 模块(vxlan.ko) |
| 物理网卡发送 | 网卡驱动 |
| 解封装 VXLAN 外层 | OVS 内核模块或 Linux 内核 VXLAN 模块 |
| 虚拟交换机投递到目标虚拟机 | OVS 内核模块 |
OVS:软件交换机
基本结构
OVS 在 Linux 上实现了一台可编程的虚拟交换机。虚拟机或容器通过虚拟网络设备连接到 OVS 的网桥(bridge)上——KVM 虚拟化场景通常使用 TAP 设备,容器场景通常使用 veth pair:
OVS Bridge (br0)
|-- vnet0 <---> VM-1
|-- vnet1 <---> VM-2
|-- vnet2 <---> VM-3
|-- ...
|-- vxlan0 <---> VXLAN tunnel to Host-A
|-- vxlan1 <---> VXLAN tunnel to Host-B
+-- ...
虚拟机发出的包到达 OVS 后,OVS 查询流表决定如何处理。
流表的结构
每条流表规则包含两个部分:匹配条件和动作。
匹配条件: 目标 IP = 10.10.1.5, TCP 端口 = 80
动作: 封装 VNI=200, 从 vxlan0 发出
匹配条件可以基于包头的几乎任何字段(MAC、IP、端口、VNI 等),动作包括转发到指定端口、丢弃、修改包头字段、或者提交到下一个流表继续处理。
流表的流水线
OVS 将流表组织为多级流水线,包按序经过每个 table 处理,类似后端开发中的中间件链。不同 SDN 控制器的 table 编号各不相同,下面展示一种常见的编排方式:
packet
|
table 0: classify (which port, tag metadata)
|
table 20: filter (IP? ARP? multicast?)
|
table 45: security group / policy check
| allow -> resubmit to next
| deny -> drop
|
table 60: forwarding (which port for dst logical IP)
|
output to target port
resubmit(,45) 相当于中间件中的 next(),将包交给下一级处理——各 stage 职责分离,新增功能只需插入新的 table,不影响已有规则。任何一级返回 drop 就终止流水线。
动态映射表
核心问题
虚拟机或容器会在物理节点之间迁移。当 VM-A 从 Host-1 迁移到 Host-2 时,所有节点都需要更新”VM-A 的逻辑 IP 现在在 Host-2 上”这一映射关系。
两类控制模式
集中式:一个 SDN 控制器掌握全局信息,主动向每个节点下发流表。
┌──────────────┐
│ SDN Controller│
└──┬───┬───┬───┘
│ │ │
┌───┘ │ └───┐
▼ ▼ ▼
Host-1 Host-2 Host-3
flow-A flow-B flow-C
分布式:每个节点自行维护本地映射,通过协议互相通告。
Host-1 ←── BGP/gossip ──→ Host-2 ←── BGP/gossip ──→ Host-3
两种模式同步的内容相同:映射关系(逻辑 IP 在哪台物理机上)和网络策略(谁和谁能通信)。
不同实现中的名字
“动态映射表”是一个通用概念,在不同实现中名称不同。Kubernetes 生态中的 CNI(Container Network Interface)插件负责容器网络的映射与策略,Calico 使用 Linux 路由表加 iptables 或 eBPF(一种内核可编程技术,可用于高性能的网络包处理)实现,Flannel 使用 VXLAN 封装配合 Linux 转发表(FDB),Cilium 基于 eBPF 实现。公有云 VPC 的映射则由云厂商的 SDN 控制面管理。
| 环境 | 动态映射表 | 管理者 |
|---|---|---|
| OVS + SDN 控制器 | OVS 流表 | SDN 控制器 |
| Calico (K8s CNI) | Linux 路由表 + iptables/eBPF | calico-node (BIRD) |
| Flannel VXLAN | Linux FDB + 路由表 | flanneld |
| Cilium | eBPF map | cilium-agent |
| 公有云 VPC | 交换机上的流表 | 云厂商 SDN 控制面 |
映射与策略
映射是最简策略
“逻辑 IP 到物理位置的映射”是转发决策的最低要求 — 告诉交换机包往哪发。在此基础上,可以叠加更精细的访问策略:
| 层级 | 功能 | 类比 |
|---|---|---|
| 映射 | 怎么到 | DNS:告诉你服务器在哪 |
| 映射 + 安全组 | 能不能到 | DNS + 防火墙:能不能访问看规则 |
| 映射 + 安全组 + QoS | 到了之后跑多快 | DNS + 防火墙 + 限速 |
策略在流表中的实现
访问策略不是独立于流表的机制,而是流表中的特定规则。如「核心概念」中所定义,每条规则包含匹配条件和动作;转发规则的动作是 output,策略规则的动作是 drop,它们共享同一个流表匹配引擎:
规则1: 目标=10.10.1.5, TCP 80 → output: vxlan0 ← 允许并转发
规则2: 目标=10.10.1.5, TCP 22 → drop ← 拒绝
规则3: 目标=10.10.1.5 → output: vnet3 ← 映射到本地 VM
规则4: 其他 → drop ← 默认拒绝
在 OVS/OpenFlow 中,规则按优先级从高到低匹配,先命中哪条就执行哪条。精细规则(指定端口)优先级高于粗粒度规则(仅指定 IP),因此规则 1 命中 TCP 80 时允许通过,规则 2 命中 TCP 22 时拒绝,而规则 3 作为兜底的映射规则不会被上述端口命中。
在多级流表流水线中,策略检查和映射查找通常位于不同的 table:策略检查在前(table 45),决定”该不该发”;映射查找在后(table 60),决定”往哪发”。二者各司其职,共同完成从逻辑地址到物理位置的完整转发决策。
完整端到端流程
同 VNI 通信(二层转发)
追踪一个虚拟机发包到同一 VNI 内、另一台物理机上的虚拟机的完整路径:
[VM-A 发包] VNI=100, IP=10.10.1.3
目标 10.10.1.5 与本机同网段 → 直接 ARP 查询 VM-B 的 MAC
原始帧: 源 MAC=VM-A, 目标 MAC=VM-B, 源 IP=10.10.1.3, 目标 IP=10.10.1.5
[本地 OVS]
查流表 → 目标 10.10.1.5 不在本机 → 远端物理机 172.16.0.2
封装: 外层 IP=172.16.0.1→172.16.0.2, UDP 4789, VNI=100
[物理网络]
交换机/路由器只看外层地址,正常转发
[远端物理机]
内核识别 UDP 4789 → 剥离外层 → 得到原始帧 + VNI=100
OVS 查流表: VNI=100, 目标 10.10.1.5 → 本地 vnet5
[VM-B 收包]
收到原始帧,完全不知道中间经历了封装和解封装
同 VNI 内的通信是纯二层行为:OVS 根据 MAC 地址做转发决策,虚拟机看到的始终是一个平坦的二层网络。
跨 VNI 通信(三层路由)
当 VM-A(VNI=100, 10.10.1.3)要访问 VM-C(VNI=200, 10.10.2.5)时,二者不在同一个广播域,必须经过三层网关:
[VM-A 发包] VNI=100, IP=10.10.1.3
目标 10.10.2.5 不在本机网段 10.10.1.0/24
→ 查路由表: 默认网关 10.10.1.1
→ ARP 查询网关 MAC
原始帧: 源 MAC=VM-A, 目标 MAC=Gateway, 源 IP=10.10.1.3, 目标 IP=10.10.2.5
[本地 OVS (Host-1)]
查流表: 目标 MAC=Gateway → 送给网关
封装: VNI=100, 外层 IP=172.16.0.1→172.16.0.10 (网关物理 IP)
[三层网关]
解封装 → VNI=100 + 原始 IP 包
查路由表 + VTEP 解析: 10.10.2.0/24 → VNI=200, 下一跳=Host-2 (172.16.0.3)
MAC 重写: 源 MAC=Gateway, 目标 MAC=VM-C
重新封装: VNI=200, 外层 IP=172.16.0.10→172.16.0.3
[物理网络]
交换机/路由器只看外层地址,正常转发
[远端 OVS (Host-2)]
解封装 → VNI=200 + 原始帧
查流表: VNI=200, 目标 MAC=VM-C → 本地 vnet7
[VM-C 收包]
源 MAC=Gateway(网关做了 MAC 重写), 源 IP=10.10.1.3(IP 层不变)
跨 VNI 通信与同 VNI 通信的关键区别:
| 维度 | 同 VNI | 跨 VNI |
|---|---|---|
| 转发依据 | 目标 MAC(OVS 二层) | 目标 IP(三层网关) |
| 决策者 | OVS 二层流表 | 三层网关路由表 |
| MAC 是否重写 | 否(端到端不变) | 是(网关替换源/目标 MAC) |
| 封装 VNI | 始终不变 | 网关从 VNI=100 重封装为 VNI=200 |
| 物理网络感知 | 无(只看外层地址) | 无(只看外层地址) |
可以看到,物理网络在整个过程中只看外层 IP 地址做转发,完全不知道 VNI 和虚拟网络的存在。封装和解封装将物理网络的拓扑与虚拟网络的逻辑完全解耦。