8. 同一个子网内的通信:ARP 代答与封包
我们来看一个简单的场景:两台 VM 在同一个 VPC、同一个子网里。VM-A 要 ping VM-B。
在传统物理网络里,这不过是一次 ARP 广播加一个 ICMP 包的事,几毫秒就能完成,简单到没人会多想。但在 VPC 里,这两台 VM 可能分别运行在不同机架的不同物理机上,中间隔着整个 VxLAN Overlay 网络。VM-A 发出的以太网帧不会直接到达 VM-B,它需要被封装、穿越物理网络、再被解封装。
一个简单的 ping,在 VPC 里要经历多少环节?
8.1 一个 ping 包的旅途
假设 VM-A 的 IP 是 10.0.1.10(MAC 为 fa:16:3e:aa:bb:cc),VM-B 的 IP 是 10.0.1.20(MAC 为 fa:16:3e:bb:cc:dd)。两台 VM 在同一个子网 10.0.1.0/24 内,但分别运行在不同的宿主机上,VM-A 在 Host-A,VM-B 在 Host-B。
VM-A 要发一个 ping 包给 VM-B。ping 的本质是一个 ICMP Echo Request,它需要被封装在一个 IP 包里,而 IP 包又需要被封装在一个以太网帧里。以太网帧有两个关键字段需要填写:源 MAC 地址和目的 MAC 地址。
源 MAC 是 VM-A 自己的网卡地址,这个它知道。目的 MAC 呢?VM-A 只知道 VM-B 的 IP 地址是 10.0.1.20,不知道它的 MAC 地址是什么。
在传统物理网络里,解决这个问题的方式很直接:VM-A 发一条 ARP 广播,"谁是 10.0.1.20?请把你的 MAC 地址告诉我。"这条广播帧的目的 MAC 填全 F(FF:FF:FF:FF:FF:FF),交换机收到后会向同一广播域内的所有端口泛洪。VM-B 收到这条广播,发现目标 IP 是自己的,就用单播回复:"10.0.1.20 的 MAC 是 fa:16:3e:bb:cc:dd。"VM-A 拿到这个回复,把 IP-MAC 映射写入自己的 ARP 缓存,然后就可以填好以太网帧的目的 MAC,把 ping 包发出去了。
在传统网络里,这条 ARP 广播只需要在本广播域内泛洪,一个 VLAN 里可能有几十台、上百台机器,代价完全可控。
但在 VPC 里,情况完全不同。VM-A 和 VM-B 不在同一台物理机上,中间隔着 VxLAN Overlay。一条 ARP 广播,在这里意味着什么?
8.2 广播在 Overlay 上的代价
先想一个最直觉的方案:让 ARP 广播在 VxLAN 上正常泛洪。VM-A 发出 ARP 广播,宿主机-A 的 VTEP 把这个广播帧封装成 VxLAN 包,然后发给同一 VNI 下的所有其他 VTEP。每个 VTEP 收到后解封装,把 ARP 广播交给本地属于这个 VNI 的 VM。
这在技术上是可行的。VxLAN 规范中定义了两种方式来处理 BUM(Broadcast, Unknown Unicast, Multicast)流量:一种是组播,把同一 VNI 的所有 VTEP 加入一个组播组,广播帧通过组播树分发;另一种是头端复制,源 VTEP 把广播帧复制 N 份,逐一发给所有对端 VTEP。
BUM 流量是什么?
BUM 是 Broadcast、Unknown Unicast、Multicast 三类流量的统称。它们的共同特点是:交换机不知道该把帧发给谁,只能发给所有人(或一组人)。在传统网络中,BUM 流量是正常的网络行为;在云网络中,BUM 流量是需要被尽可能消除的开销。ARP 代答、DHCP 代答、未知单播丢弃,都是针对 BUM 流量的抑制手段。
两种方式都能工作。但算一笔账就知道代价有多大。
假设一个中等规模的 VPC,里面有 500 台 VM,分布在 100 台宿主机上。VM-A 发一次 ARP 广播,源 VTEP 需要把这个广播帧发给其他 99 台宿主机的 VTEP,如果用头端复制,就是 99 次封装和发送。
这只是一台 VM 的一次 ARP 请求。
500 台 VM,每台在启动时、ARP 缓存过期时、通信新目标时,都会发 ARP 请求。假设每台 VM 平均每分钟产生 1 次 ARP 请求,一分钟内的 ARP 广播总量是:500 次请求 x 99 次复制 = 49500 次封装转发。每秒大约 825 次,仅仅是 ARP 流量。
这还只是一个 VPC。一台宿主机上可能运行着属于几十个不同 VPC 的 VM,每个 VPC 都有自己的广播域。所有 VPC 的 ARP 广播叠加在一起,VTEP 的 CPU 和带宽会被广播流量大量消耗,而这些流量没有承载任何有效的业务数据。
规模再大一些,问题更严重。一个大型 VPC 可能有几千台 VM 分布在上千台宿主机上,ARP 广播的总量是 O(N x M) 级别,N 是 VM 数量,M 是宿主机数量。这个乘积随着规模增长会迅速膨胀到不可接受的程度。
组播方案能缓解头端复制的压力,但组播本身的管理极其复杂,物理网络需要运行 IGMP Snooping、PIM 等组播协议,每个 VNI 对应一个组播组,组播组的数量随 VPC 数量线性增长。大多数云厂商在实践中选择不依赖物理网络的组播能力,组播的运维成本太高,而且很多物理网络设备对组播的支持并不完善。
无论用哪种方式,结论是一样的:在云的规模下,允许广播在 Overlay 上泛洪是不可接受的。
那怎么办?VM-A 需要知道 VM-B 的 MAC 地址才能发包,ARP 广播又不能泛洪。有没有一种方式,能让 VM-A 拿到 VM-B 的 MAC,但不需要广播?
8.3 ARP 代答:控制面预先回答
换一个角度想这个问题。
ARP 广播的本质是什么?VM-A 不知道 VM-B 的 MAC 地址,所以需要"问"。在传统网络里,没有人事先知道这个信息,所以只能靠广播去"发现"。
但在 VPC 里,情况不一样。VM-B 是云平台创建的,它的 IP 地址是云平台分配的,它的 MAC 地址是云平台生成的,它运行在哪台宿主机上是云平台调度的。SDN 控制器在 VM-B 创建的那一刻,就已经知道了 VM-B 的全部网络信息:IP 10.0.1.20,MAC fa:16:3e:bb:cc:dd,位于 Host-B 的 VTEP 后面。
既然控制面已经知道答案,为什么还要让数据面去"发现"?
这就是 ARP 代答的核心思路:不让 ARP 广播出去,由宿主机直接回答。
具体的工作方式是这样的。SDN 控制器在 VM-B 创建时,就把 VM-B 的 IP-MAC-VTEP 三元组下发到了相关的宿主机节点(在云的网络实践中,通常是"按需下发"到运行了同一 VPC 内 VM 的宿主机,以减少控制面广播风暴)。Host-A 上的虚拟交换机(OVS 或类似组件)本地维护着一张局部的映射表,里面记录了当前它需要通信的同 VPC 内 VM 的 IP 和 MAC 的对应关系。
当 VM-A 发出 ARP 请求时,这个请求首先到达 Host-A 上的虚拟交换机。虚拟交换机拦截这个 ARP 请求,检查目标 IP 10.0.1.20,在本地映射表里一查,找到了:10.0.1.20 对应的 MAC 是 fa:16:3e:bb:cc:dd。
虚拟交换机直接构造一个标准的 ARP 应答帧:把 fa:16:3e:bb:cc:dd 填入应答的"发送方 MAC"字段,把 10.0.1.20 填入"发送方 IP"字段,然后把这个应答帧返回给 VM-A。
VM-A 收到 ARP 应答,把 10.0.1.20 → fa:16:3e:bb:cc:dd 这对映射写入自己的 ARP 缓存。从 VM-A 的视角看,它发了一个 ARP 请求,收到了一个 ARP 回复,和在传统网络里的体验完全一样。它不知道这个回复是宿主机"代替"VM-B 回答的,也不需要知道。
图 8.1:ARP 代答的时序
sequenceDiagram
participant SDN as SDN Controller
participant HA as Host-A (vSwitch)
participant VMA as VM-A
participant VMB as VM-B
Note over SDN,HA: Phase 1: VM-B creation (earlier)
SDN->>HA: Push mapping: 10.0.1.20 → MAC fa:16:3e:bb:cc:dd @ VTEP-B
Note over VMA,HA: Phase 2: VM-A sends ARP
VMA->>HA: ARP Request: Who has 10.0.1.20?
HA->>HA: Lookup local table: 10.0.1.20 → fa:16:3e:bb:cc:dd
HA->>VMA: ARP Reply: 10.0.1.20 is at fa:16:3e:bb:cc:dd
Note over VMA: VM-A updates ARP cache
Note over VMA: ARP broadcast never left Host-A
整个过程中,ARP 请求没有离开 Host-A。没有广播,没有泛洪,没有 N 倍的流量放大。一次本地查表,一次本地回复,问题就解决了。
ARP 请求本身携带的信息很简单:发送方的 IP 和 MAC("我是谁"),以及目标 IP("我要找谁"),目标 MAC 留空。代答只需要把目标 MAC 填上,构造一个标准的 ARP 应答帧返回即可。这个过程对 VM 完全透明,VM 的网络协议栈不需要做任何修改,它甚至不知道自己运行在一个虚拟化的网络环境中。
8.4 ARP 抑制与静默网络
ARP 代答解决了 ARP 广播的问题。但 ARP 不是 VPC 里唯一的广播源。
VM 启动时会发 DHCP Discover 广播,"我需要一个 IP 地址,谁能给我?"IPv6 环境下有 NDP(Neighbor Discovery Protocol)的邻居请求。还有各种应用层的广播和组播流量。每一种广播,如果放任它在 Overlay 上泛洪,都会产生同样的 N 倍放大问题。
云网络的设计理念是:VPC 内的网络应该是"静默"的。
所谓静默,就是尽可能消除所有不必要的广播和泛洪流量。ARP 用代答解决,DHCP 同样可以由宿主机本地代答,VM 的 IP 地址本来就是云平台分配的,宿主机完全有能力直接回复 DHCP 请求,不需要广播到网络上去找 DHCP 服务器。对于未知单播(目标 MAC 不在本地映射表中的单播帧),传统交换机的做法是泛洪,发给所有端口,看谁回应。但在 VPC 里,VTEP 不会泛洪未知单播,而是直接丢弃或上报控制面。
这种"静默网络"的设计有一个前提:控制面必须足够准确和及时。
传统网络的数据面有自学习能力,交换机通过 ARP 广播和响应来学习 MAC 表,不需要任何集中管理。即使没有控制面,数据面也能自己工作。但 VPC 网络把这个能力上移到了控制面:所有的 IP-MAC 映射、所有的转发规则,都由 SDN 控制器预先下发。如果映射表不完整,比如 VM-B 刚创建,控制器还没来得及把映射推送到 Host-A,ARP 代答就无法回答 VM-A 的请求。如果映射表过期,比如 VM-B 已经迁移到了另一台宿主机,但旧的映射还没更新,代答给出的信息就是错误的。
完全禁止广播,意味着完全依赖控制面的正确性和时效性。这是一个权衡:用控制面的复杂性,换取数据面的简洁和高效。在云的规模下,这个权衡是值得的,但它对控制面的可靠性提出了很高的要求。
8.5 VxLAN 封包:数据面的完整路径
ARP 代答完成了。VM-A 的 ARP 缓存里已经有了 VM-B 的 MAC 地址 fa:16:3e:bb:cc:dd。现在,VM-A 可以发出真正的 ping 包了。
我们现在来看下这个包的旅程:
第一步:VM-A 构造以太网帧。
VM-A 的网络协议栈构造一个 ICMP Echo Request,封装在 IP 包里(源 IP 10.0.1.10,目的 IP 10.0.1.20),再封装在以太网帧里(源 MAC fa:16:3e:aa:bb:cc,目的 MAC fa:16:3e:bb:cc:dd)。这个帧从 VM-A 的虚拟网卡发出。
第二步:帧到达宿主机的虚拟交换机。
Host-A 上的虚拟交换机收到这个帧,查流表。目的 MAC fa:16:3e:bb:cc:dd 属于 VNI 5678,对端 VTEP 是 Host-B 的物理 IP 172.16.0.2。这条信息是 SDN 控制器预先下发的,和 ARP 代答用的映射表来自同一个数据源。
第三步:VTEP 封装。
Host-A 的 VTEP 开始封装。原始的以太网帧作为 payload,外面依次套上四层头部:
图 8.2:VxLAN 封装后的完整报文结构
┌─────────────────────────────────────────────────────────────┐
│ Outer Ethernet Header (14B) │
│ Dst MAC: Next-hop MAC │ Src MAC: Host-A MAC │ 0x0800 │
├─────────────────────────────────────────────────────────────┤
│ Outer IP Header (20B) │
│ Src IP: 172.16.0.1 (Host-A) │ Dst IP: 172.16.0.2 (Host-B)│
│ Protocol: UDP (17) │
├─────────────────────────────────────────────────────────────┤
│ Outer UDP Header (8B) │
│ Src Port: hash │ Dst Port: 4789 │ Length │ Checksum │
├─────────────────────────────────────────────────────────────┤
│ VxLAN Header (8B) │
│ Flags │ Reserved │ VNI: 5678 (24-bit) │ Reserved │
├─────────────────────────────────────────────────────────────┤
│ Original Ethernet Frame │
│ Dst MAC: fa:16:3e:bb:cc:dd │ Src MAC: fa:16:3e:aa:bb:cc │
│ IP: 10.0.1.10 → 10.0.1.20 │ ICMP Echo Request │
└─────────────────────────────────────────────────────────────┘
VxLAN 头中填入 VNI 5678,这是 VM-A 和 VM-B 所属 VPC 的隔离标识。外层 UDP 的目的端口是 4789(VxLAN 的标准端口),源端口是根据内层帧的某些字段做哈希计算得出的,这个哈希值的作用是让物理网络的 ECMP(等价多路径)能够把不同流的包分散到不同的物理链路上,避免所有 VxLAN 流量都走同一条路径。外层 IP 的源地址是 Host-A 的物理 IP 172.16.0.1,目的地址是 Host-B 的物理 IP 172.16.0.2。
封装完成后,这个包从 Host-A 的物理网卡发出,进入物理网络。
第四步:Underlay 路由。
物理网络看到的只是一个普通的 IP 包,源 172.16.0.1,目的 172.16.0.2,协议 UDP。物理交换机不知道也不关心这个 UDP 包里面装的是什么。它按照正常的三层路由规则,把包从 Host-A 所在的 ToR 交换机转发到 Spine,再从 Spine 转发到 Host-B 所在的 ToR,最终送到 Host-B 的物理网卡。
第五步:VTEP 解封装。
Host-B 的网络协议栈收到这个 UDP 包,发现目的端口是 4789,这是 VxLAN 的标识。VTEP 开始解封装:剥掉外层以太网头、外层 IP 头、UDP 头、VxLAN 头,取出里面的原始以太网帧。
VTEP 检查 VxLAN 头中的 VNI:5678。在本地查找 VNI 5678 对应的虚拟网络,找到目的 MAC fa:16:3e:bb:cc:dd 对应的虚拟端口,VM-B 的虚拟网卡。
第六步:VM-B 收到 ping 包。
原始的以太网帧被交给 VM-B。VM-B 的网络协议栈看到的是一个完全正常的以太网帧:源 MAC fa:16:3e:aa:bb:cc,目的 MAC fa:16:3e:bb:cc:dd,里面是一个 ICMP Echo Request。VM-B 回复一个 ICMP Echo Reply,过程反向再走一遍,VM-B 的回复帧被 Host-B 的 VTEP 封装,通过 Underlay 送到 Host-A,Host-A 的 VTEP 解封装后交给 VM-A。
图 8.3:同子网 ping 包的完整数据路径
sequenceDiagram
participant VMA as VM-A<br/>(10.0.1.10)
participant HA as Host-A<br/>VTEP (172.16.0.1)
participant UL as Underlay<br/>Network
participant HB as Host-B<br/>VTEP (172.16.0.2)
participant VMB as VM-B<br/>(10.0.1.20)
VMA->>HA: Ethernet Frame<br/>src:MAC-A dst:MAC-B<br/>ICMP Echo Request
HA->>HA: Lookup: MAC-B → VNI 5678, VTEP 172.16.0.2
HA->>UL: VxLAN Encap<br/>Outer: 172.16.0.1→172.16.0.2<br/>VNI: 5678
UL->>HB: IP Routing (standard L3)
HB->>HB: VxLAN Decap<br/>Check VNI 5678, find VM-B port
HB->>VMB: Original Ethernet Frame<br/>ICMP Echo Request
VMB->>HB: ICMP Echo Reply
HB->>UL: VxLAN Encap (reverse)
UL->>HA: IP Routing
HA->>VMA: ICMP Echo Reply
一个 ping,六步完成。VM-A 和 VM-B 都以为自己在一个普通的局域网里,发帧、收帧,和物理网络没有任何区别。但实际上,这个帧穿越了整个 VxLAN Overlay:被封装、被路由、被解封装,中间经过了虚拟交换机、VTEP、物理交换机等多个环节。
8.6 Underlay 路由:物理网络的角色
封装后的包进入物理网络后,物理网络扮演的角色其实很简单:把包从 A 送到 B。
物理交换机看到的是一个标准的 IP 包,源 IP 是 Host-A 的物理地址,目的 IP 是 Host-B 的物理地址。物理网络不知道这个包里面装的是 VxLAN,不知道里面有一个 VM-A 发给 VM-B 的 ICMP 包,也不需要知道。它只需要按照自己的路由表,把包送到目的地。
这就是 Overlay 和 Underlay 的职责分离。
Overlay 负责租户隔离和虚拟网络的语义,VNI 区分不同的 VPC,VTEP 做封包解包,SDN 控制器管理转发规则。Underlay 负责物理连通,把封装后的包从一台宿主机送到另一台宿主机,用的是标准的三层路由(BGP、OSPF 或静态路由),走的是 Leaf-Spine 或其他物理拓扑。
两层网络各管各的。Underlay 可以独立演进,从三层 Clos 架构换成其他拓扑,不影响上面跑的任何 VPC。Overlay 可以独立扩展,新建 VPC、新增子网、启动 VM,不需要在物理网络上做任何配置变更。这种优雅的分层是云网络能够快速弹性伸缩的基础。
不过,VxLAN 封装会让包变大,50 字节的额外头部意味着封装后的包会超过标准以太网 1500 字节的 MTU。这个问题以及它的解决方式(Jumbo Frame、MSS 适配),在第五章已经详细讨论过,这里不再展开。需要记住的是:Underlay 网络的 MTU 必须为 Overlay 封装留出足够的余量,否则就会出现分片或丢包。
图 8.4:Overlay 与 Underlay 的职责分离
flowchart TB
subgraph Overlay ["Overlay (Virtual Network) - 完全依靠 VNI 隔离数据面"]
direction LR
subgraph VPC_A ["VPC-A (VNI 5678)"]
VMA["VM-A<br/>(10.0.1.10)"] <-. "相互通信" .-> VMB["VM-B<br/>(10.0.1.20)"]
end
subgraph VPC_B ["VPC-B (VNI 9012)"]
VMC["VM-C<br/>(10.0.1.10)"] <-. "相互通信" .-> VMD["VM-D<br/>(10.0.1.20)"]
end
VPC_A ~~~ VPC_B
end
subgraph Underlay ["Underlay (Physical Network) - 标准物理连通,不感知 VxLAN Payload"]
direction TB
Spine(("Spine Switch"))
ToR1["ToR-1"]
ToR2["ToR-2"]
ToR3["ToR-3"]
Spine --- ToR1
Spine --- ToR2
Spine --- ToR3
HostA["Host-A<br/>(172.16.0.1)"]
HostB["Host-B<br/>(172.16.0.2)"]
HostC["Host-C<br/>(172.16.0.3)"]
ToR1 --- HostA
ToR2 --- HostB
ToR3 --- HostC
end
%% 表示虚机分配在物理机的对应位置
VMA -.- HostA
VMB -.- HostB
VMC -.- HostC
两个 VPC 可以使用完全相同的 IP 地址段(都是 10.0.1.0/24),但因为 VNI 不同,它们在数据面上完全隔离。物理网络只看到宿主机及其之间的 IP 包,对 VPC 的存在一无所知。
同子网内的通信路径,到这里已经完整了。从 ARP 代答到 VxLAN 封包,从 Overlay 封装到 Underlay 路由,一个 ping 包穿越了虚拟交换机、Overlay 隧道、物理交换机三重世界。但这一切都建立在一个前提上:源和目的在同一个子网里。当目的 IP 不在本子网范围内,二层转发走不通的时候,包该往哪里去?