Geneve收包流程详解
网络流量如何从物理网卡 eth0 进入 OVS 上的 tap 口
场景说明
一个外部主机发送数据到 KubeVirt VM,数据如何从物理网卡一路到达 VM 的 tap 口。
Geneve 隧道(Kube-OVN 默认方式)
eth0 仍然由内核协议栈管理,OVS 通过隧道端口利用 eth0 发包
┌───────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ OVS br-int │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ ┌─────────────────────┐ │ │
│ │ │ veth │ │ veth │ │ geneve_sys │ │ │
│ │ │ VM-A │ │ VM-B │ │ (type=geneve) │ │ │
│ │ └──────┘ └──────┘ │ remote_ip=对端节点IP │ │ │
│ │ └──────────┬──────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────────┼───────────────┘ │
│ │ │
│ OVS 生成 Geneve 封装后的数据包 │ │
│ 交给内核协议栈发送 │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Host 内核协议栈 │ │
│ │ │ │
│ │ 路由查找: 目的IP=对端节点IP → 出接口=eth0 │ │
│ │ │ │
│ └──────────────────────────┬─────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴─────────────────────────┐ │
│ │ eth0 (物理网卡) │ │
│ │ IP: 192.168.1.10 │ │
│ │ 由内核协议栈正常管理 │ │
│ └──────────────────────────┬─────────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────┘
│
▼
═══════════════
物理网络
═══════════════
关键点:
- eth0 仍然属于内核协议栈
- OVS 通过 Geneve 隧道端口封装数据包
- 封装后的外层 IP 包通过内核正常路由经 eth0 发出
- 这是 Kube-OVN 的默认方式
详细收包流程(外部流量 → VM)
场景:Geneve 隧道方式
外部另一个节点上的 VM 发送数据到本节点的 VM。
外部节点封装好的 Geneve 数据包:
┌──────────────────────────────────────────────────────────┐
│ Outer Eth │ Outer IP │ UDP │ Geneve │ 原始以太网帧 │
│ dst: eth0 │ dst: 本机IP │ dp:6081│ VNI │ dst: VM MAC │
│ 的MAC │ src: 对端节点IP │ │ │ │
└──────────────────────────────────────────────────────────┘
完整收包路径
物理网络(线缆上的电信号/光信号)
│
▼
═══════════════════════════════════════════════════════════════
第一阶段:物理网卡收包
═══════════════════════════════════════════════════════════════
│
│ ① 物理网卡 eth0 收到以太网帧
│ - 网卡校验 FCS (帧校验序列)
│ - 网卡 DMA 将数据写入内核 Ring Buffer (sk_buff)
│ - 触发硬中断 (IRQ)
│
▼
eth0 网卡驱动 (如 ixgbe, mlx5)
│
│ ② 硬中断处理
│ - 禁止该队列的后续中断
│ - 调度 NAPI 软中断
│
│ ③ NAPI 软中断 (NET_RX_SOFTIRQ)
│ - napi_poll() 批量从 Ring Buffer 取包
│ - 构建 sk_buff 结构体
│ - 设置 skb->dev = eth0
│ - 设置 skb->protocol = ETH_P_IP (0x0800)
│
▼
netif_receive_skb(skb) ← 内核统一收包入口
│
│ ④ 内核协议栈开始处理
│ - tc ingress 规则 (如果有)
│ - XDP 处理 (如果有)
│ - 检查 skb->protocol
│
▼
═══════════════════════════════════════════════════════════════
第二阶段:内核协议栈解封装外层头部
═══════════════════════════════════════════════════════════════
│
│ ⑤ IP 层处理
│ ip_rcv()
│ - 检查外层目的 IP = 本机 IP → 本地处理
│ - ip_local_deliver()
│
▼
│ ⑥ UDP 层处理
│ udp_rcv()
│ - 检查目的端口 = 6081 (Geneve)
│ - 找到注册在该端口的 Geneve 处理函数
│
▼
│ ⑦ Geneve 解封装
│ geneve_udp_encap_recv()
│ - 剥离外层: Ethernet + IP + UDP + Geneve 头部
│ - 提取 VNI (虚拟网络标识)
│ - 获得内层的原始以太网帧
│ - 根据 VNI 找到对应的 OVS 隧道端口
│
│ 解封装前:
│ ┌──────────┬──────────┬─────┬────────┬───────────┐
│ │ Outer Eth│ Outer IP │ UDP │ Geneve │ 原始以太网帧│
│ └──────────┴──────────┴─────┴────────┴───────────┘
│
│ 解封装后:
│ ┌───────────┐
│ │ 原始以太网帧│ skb->dev = geneve_sys (OVS 隧道端口)
│ └───────────┘
│
▼
═══════════════════════════════════════════════════════════════
第三阶段:OVS br-int 流表处理
═══════════════════════════════════════════════════════════════
│
│ ⑧ 数据包进入 OVS
│ ovs_vport_receive(geneve_sys 端口, skb)
│ - 入端口 = geneve_sys (隧道端口)
│
▼
OVS 内核数据路径 (openvswitch.ko)
│
│ ⑨ 流表匹配
│ ovs_flow_tbl_lookup(skb)
│
│ 匹配字段:
│ - in_port = geneve_sys
│ - tun_id = VNI (标识 VPC/子网)
│ - dl_dst = VM 的 MAC 地址
│ - nw_dst = VM 的 IP 地址
│
│ ┌──────────────────────────────────────────────┐
│ │ OVN 逻辑流表处理: │
│ │ │
│ │ table 0: 入端口分类 │
│ │ → 识别为隧道流量,提取 VNI │
│ │ │
│ │ table 10-20: 入方向 ACL (安全组) │
│ │ → 检查是否允许该流量 │
│ │ │
│ │ table 30: 逻辑交换机 │
│ │ → MAC 地址查找,确定目的端口 │
│ │ │
│ │ table 40-50: 出方向 ACL │
│ │ → 检查出方向安全组规则 │
│ │ │
│ │ table 60: 输出动作 │
│ │ → output:veth-host (VM对应的端口) │
│ └──────────────────────────────────────────────┘
│
│ ⑩ 执行动作: 从 veth-host 端口输出
│ ovs_execute_actions()
│ → output 到 veth-host 端口
│ → dev_queue_xmit(skb)
│ → skb->dev = veth-host
│
▼
═══════════════════════════════════════════════════════════════
第四阶段:veth pair 跨 Namespace
═══════════════════════════════════════════════════════════════
│
│ ⑪ veth-host 发送
│ veth_xmit(skb)
│ - 找到 peer = veth-pod
│ - dev_forward_skb(veth-pod, skb)
│ - 数据包跨越 namespace 边界
│ - skb->dev = veth-pod
│ - netif_rx(skb) 在 Pod namespace 触发收包
│
▼
═══════════════════════════════════════════════════════════════
第五阶段:Pod Network Namespace 内部
═══════════════════════════════════════════════════════════════
│
│ ⑫ veth-pod 收包
│ netif_receive_skb(skb)
│ - veth-pod 是 Linux Bridge 的端口
│ - 进入 Bridge 处理
│
▼
Linux Bridge (k6t-net0)
│
│ ⑬ Bridge 转发
│ br_handle_frame(skb)
│ - br_fdb_update(): 学习源 MAC
│ - br_fdb_find_rcu(): 查找目的 MAC
│ - 命中: 目的端口 = tap0
│ - br_forward(tap0, skb)
│
▼
═══════════════════════════════════════════════════════════════
第六阶段:tap → vhost-net → VM
═══════════════════════════════════════════════════════════════
│
│ ⑭ tap0 收到数据包
│ tun_net_xmit(skb)
│ - 将 skb 放入 tap 的 socket 接收队列
│ - 唤醒 vhost-net 内核线程 (通过 eventfd)
│
▼
vhost-net 内核线程
│
│ ⑮ 处理 RX vring
│ handle_rx()
│ - 从 tap socket 队列取出 skb
│ - 读取 VM 预先提交的空 RX buffer (avail ring)
│ - 复制数据: skb → vring 共享内存
│ - 更新 used ring
│ - 通过 irqfd 向 VM 注入虚拟中断
│
▼
VM (Guest)
│
│ ⑯ Guest 收到虚拟中断
│ virtio-net 驱动 NAPI 轮询
│ - 从 used ring 读取数据
│ - 构建 Guest sk_buff
│ - 上送 Guest 协议栈
│ - TCP/IP 解封装
│ - 放入 socket 接收缓冲区
│
▼
VM 内应用程序
│
│ ⑰ recv() 返回数据
│
▼
应用获得数据 ✅
关键转折点图解
数据包身份的变化过程:
物理网络上:
┌──────────┬──────────┬─────┬────────┬───────────────────┐
│ Outer Eth│ Outer IP │ UDP │ Geneve │ Inner Eth + IP+TCP│
│ 物理MAC │ 节点IP │ 6081│ VNI │ VM MAC + VM IP │
└──────────┴──────────┴─────┴────────┴───────────────────┘
│
① eth0 收包 (DMA + 硬中断)
│
▼
② 内核 IP/UDP 层处理
│
③ Geneve 解封装 ← 💡 关键转折点 #1
│ 外层头部被剥离
▼ 数据包 "进入" OVS 隧道端口
OVS br-int 内:
┌───────────────────┐
│ Inner Eth + IP+TCP│ skb->dev = geneve_sys
│ VM MAC + VM IP │ OVS tunnel metadata: tun_id=VNI
└───────────────────┘
│
④ OVS 流表匹配
⑤ output 到 veth-host ← 💡 关键转折点 #2
│ 数据包 "离开" OVS 进入 veth
▼
veth pair:
┌───────────────────┐
│ Inner Eth + IP+TCP│ skb->dev = veth-host → veth-pod
│ VM MAC + VM IP │
└───────────────────┘
│
⑥ 跨 namespace ← 💡 关键转折点 #3
│ 数据包进入 Pod netns
▼
Linux Bridge → tap0:
┌───────────────────┐
│ Inner Eth + IP+TCP│ skb 放入 tap socket 队列
│ VM MAC + VM IP │
└───────────────────┘
│
⑦ vhost-net → vring ← 💡 关键转折点 #4
│ 数据包 "进入" VM
▼
VM 内部:
┌───────────────────┐
│ Inner Eth + IP+TCP│ Guest virtio-net 驱动收包
│ VM MAC + VM IP │ Guest 协议栈正常处理
└───────────────────┘
数据复制与开销标注
阶段 数据复制 模式切换 延迟
──────────────────────────────────────────────────────────────────────────
eth0 DMA 收包 复制#1 (DMA→内存) 硬中断+软中断 ~2-5μs
内核 IP/UDP 处理 无复制 无 ~1-2μs
Geneve 解封装 无复制(指针移动) 无 ~1-2μs
OVS 流表匹配 无复制 首包可能upcall ~2-5μs
veth 跨 namespace 复制#2 (skb_clone) 软中断 ~2-5μs
Linux Bridge 转发 无复制(指针传递) 无 ~1-3μs
tap → vhost-net 复制#3 (skb→vring) eventfd 通知 ~2-4μs
vring → VM 无额外复制(共享内存) VM Enter ⚡ ~1-3μs
VM 协议栈 → 应用 复制#4 (内核→用户) 系统调用 ~1-2μs
──────────────────────────────────────────────────────────────────────────
总计 4 次复制 5-7 次切换 ~15-35μs
验证和调试命令
# 查看 OVS 网桥和端口
ovs-vsctl show
# 查看 br-int 上的端口
ovs-vsctl list-ports br-int
# 查看 Geneve 隧道端口
ovs-vsctl find interface type=geneve
# 查看 OVS 流表(看数据包如何被匹配和转发)
ovs-ofctl dump-flows br-int
# 查看某个端口的统计(确认数据包是否到达)
ovs-vsctl get interface <port-name> statistics
# 在各个点抓包验证
# 物理网卡上抓包(能看到 Geneve 封装)
tcpdump -i eth0 -nn udp port 6081
# OVS 内部端口抓包
ovs-tcpdump -i <port-name>
# Pod namespace 内抓包
nsenter -t <pid> -n tcpdump -i veth-pod
# tap 设备抓包
nsenter -t <pid> -n tcpdump -i tap0
一句话总结
外部流量通过 eth0 → 内核协议栈 IP/UDP 处理 → Geneve 解封装 → OVS 隧道端口进入 br-int → 流表匹配找到目标 veth 端口 → veth 跨 namespace → Linux Bridge 转发 → tap0 → vhost-net 写入 vring → VM 收包。核心在于 Geneve 解封装是连接物理网卡和 OVS 虚拟世界的桥梁,外层 IP 负责节点间物理可达,内层帧负责 VM 间逻辑通信。
Geneve 发包流程详解
场景说明
本节点的 VM-A 发送数据到远端节点的 VM-B,需要通过 Geneve 隧道跨节点传输。
本节点 VM-A (10.0.1.10) → Geneve 隧道 → 远端节点 VM-B (10.0.1.20)
本节点 eth0: 192.168.1.10
远端节点 eth0: 192.168.1.20
整体流程概览
VM-A 发出原始数据包:
┌──────────────────────────────────┐
│ Eth: dst=VM-B MAC │
│ IP: dst=10.0.1.20 (VM-B) │
│ TCP: dst=80 │
│ Payload │
└──────────────────────────────────┘
│
│ 经过 tap → bridge → veth → OVS
▼
OVS 决定需要通过 Geneve 隧道发出,封装后:
┌─────────────────────────────────────────────────────────────────┐
│ Outer Eth: dst=网关/下一跳MAC src=eth0 MAC │
│ Outer IP: dst=192.168.1.20(远端节点) src=192.168.1.10(本节点) │
│ Outer UDP: dst=6081(Geneve) src=随机端口 │
│ Geneve: VNI=0x1234 Options(可选) │
│ ┌──────────────────────────────────┐ │
│ │ Inner Eth: dst=VM-B MAC │ │
│ │ Inner IP: dst=10.0.1.20 │ ← 原始数据包被完整包裹 │
│ │ Inner TCP: dst=80 │ │
│ │ Payload │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ 通过内核协议栈路由,从 eth0 发出
▼
物理网络 → 远端节点
完整发包流程
VM-A 应用程序 send()
│
▼
═══════════════════════════════════════════════════════════════
第一阶段:VM 内部 → tap
═══════════════════════════════════════════════════════════════
│
│ ① Guest 应用 send()
│ [复制 #1] 用户空间 → Guest 内核
│
│ ② Guest 内核协议栈封装
│ TCP 头 → IP 头 → 以太网帧头
│ src MAC = VM-A MAC
│ dst MAC = VM-A 网关 MAC (或 VM-B MAC,取决于是否同子网)
│ src IP = 10.0.1.10
│ dst IP = 10.0.1.20
│
│ ③ virtio-net 驱动发送
│ [复制 #2] sk_buff → TX vring
│ 写通知寄存器 → VM Exit ⚡
│
│ ④ vhost-net 处理
│ [复制 #3] vring → Host sk_buff
│ 注入 tap0 → netif_rx()
│
▼
═══════════════════════════════════════════════════════════════
第二阶段:tap → Linux Bridge → veth → OVS br-int
═══════════════════════════════════════════════════════════════
│
│ ⑤ tap0 → Linux Bridge (k6t-net0)
│ br_handle_frame()
│ MAC 学习 + FDB 转发 → 出端口 = veth-pod
│
│ ⑥ veth-pod → veth-host (跨 namespace)
│ [复制 #4] skb_clone + 软中断
│
│ ⑦ veth-host → OVS br-int
│ ovs_vport_receive(veth-host 端口, skb)
│
▼
═══════════════════════════════════════════════════════════════
第三阶段:OVS/OVN 流表处理 ← 💡 核心决策点
═══════════════════════════════════════════════════════════════
│
│ ⑧ OVS 内核数据路径流表匹配
│ ovs_flow_tbl_lookup(skb)
│
│ 匹配字段:
│ ┌──────────────────────────────────────┐
│ │ in_port = veth-host (VM-A 的端口) │
│ │ dl_src = VM-A MAC │
│ │ dl_dst = VM-B MAC │
│ │ nw_src = 10.0.1.10 │
│ │ nw_dst = 10.0.1.20 │
│ └──────────────────────────────────────┘
│
│ ⑨ OVN 逻辑流表流水线 (翻译为 OpenFlow 规则)
│
│ ┌─────────────────────────────────────────────────────┐
│ │ │
│ │ table 0: 入端口分类 │
│ │ match: in_port=veth-host │
│ │ action: 设置 logical_port=vm-a-port, 下一个表 │
│ │ │
│ │ table 8-16: Ingress ACL (入方向安全组) │
│ │ match: 源IP/端口 + 安全组规则 │
│ │ action: 允许/拒绝 │
│ │ │
│ │ table 24: Logical Switch MAC 学习 │
│ │ match: dl_dst=VM-B MAC │
│ │ action: 确定目的逻辑端口=vm-b-port │
│ │ │
│ │ table 32: Logical Router (如果跨子网) │
│ │ match: nw_dst=10.0.1.20 │
│ │ action: 路由查找, 可能 SNAT/DNAT │
│ │ │
│ │ table 40-48: Egress ACL (出方向安全组) │
│ │ match: 目的 IP/端口 + 安全组规则 │
│ │ action: 允许/拒绝 │
│ │ │
│ │ table 60: 输出判断 │
│ │ 💡 关键决策: VM-B 在哪个节点? │
│ │ 查询 OVN Southbound DB: │
│ │ vm-b-port → chassis=远端节点 → IP=192.168.1.20 │
│ │ │
│ │ action: set_tunnel(tun_id=VNI), │
│ │ set_field(tun_dst=192.168.1.20), │
│ │ output:geneve_sys │
│ │ ↑ │
│ │ 从 Geneve 隧道端口发出 │
│ │ │
│ └─────────────────────────────────────────────────────┘
│
│ ⑩ 执行动作: output 到 geneve_sys 隧道端口
│ ovs_execute_actions()
│
│ action 具体执行:
│ a. set_tunnel: 设置隧道元数据
│ - tun_id (VNI) = 标识 VPC/逻辑网络
│ - tun_dst = 192.168.1.20 (远端节点 IP)
│ b. output:geneve_sys
│ - 调用 geneve 隧道端口的发送函数
│
▼
═══════════════════════════════════════════════════════════════
第四阶段:Geneve 封装 ← 💡 封装核心
═══════════════════════════════════════════════════════════════
│
│ ⑪ OVS 调用 Geneve 隧道端口的发送函数
│ ovs_vport_send(geneve_sys, skb, tunnel_metadata)
│ → geneve_xmit()
│
│ ⑫ 构建 Geneve 头部
│
│ geneve_build_header()
│ ┌─────────────────────────────────────────────┐
│ │ Geneve Header (8 bytes + options) │
│ │ │
│ │ 0 1 2 │
│ │ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 │
│ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │
│ │ │ Ver │Opt Len│O│C│ Rsvd │ Protocol │ │
│ │ │ (0) │ │ │ │ │ (0x6558 │ │
│ │ │ │ │ │ │ │ =Ethernet) │ │
│ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │
│ │ │ VNI (24 bits) │ Reserved│ │
│ │ │ 0x001234 │ │ │
│ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │
│ │ │ Variable Length Options (可选) │ │
│ │ │ OVN 用此携带逻辑端口信息等 │ │
│ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │
│ └─────────────────────────────────────────────┘
│
│ ⑬ 构建外层 UDP 头部
│
│ udp_tunnel_xmit_skb()
│ ┌─────────────────────────────────────────────┐
│ │ UDP Header (8 bytes) │
│ │ │
│ │ Source Port: hash(内层五元组) & 0xFFFF │
│ │ ↑ 随机化源端口,用于 ECMP 负载均衡│
│ │ Dest Port: 6081 (IANA 标准 Geneve 端口) │
│ │ Length: Geneve头 + 内层帧长度 │
│ │ Checksum: 0x0000 (通常关闭) │
│ │ │
│ └─────────────────────────────────────────────┘
│
│ 💡 源端口随机化的意义:
│ - 物理交换机根据 UDP 五元组做 ECMP 哈希
│ - 不同的内层流量 → 不同的 UDP 源端口 → 分散到不同物理链路
│ - 避免所有隧道流量走同一条物理路径
│
│ ⑭ 构建外层 IP 头部
│
│ iptunnel_xmit()
│ ┌─────────────────────────────────────────────┐
│ │ IP Header (20 bytes) │
│ │ │
│ │ Version: 4 │
│ │ Protocol: 17 (UDP) │
│ │ TTL: 64 │
│ │ Source IP: 192.168.1.10 (本节点 eth0) │
│ │ Dest IP: 192.168.1.20 (远端节点 eth0) │
│ │ DF: 1 (Don't Fragment,通常设置) │
│ │ Total Len: 外层IP头+UDP头+Geneve头+内层帧 │
│ │ │
│ └─────────────────────────────────────────────┘
│
│ 💡 源 IP 和目的 IP 的来源:
│ - 目的 IP: OVN Southbound DB 记录了每个 chassis 的 IP
│ - 源 IP: 内核路由查找确定出接口后,使用出接口的 IP
│
▼
═══════════════════════════════════════════════════════════════
第五阶段:内核协议栈路由 + 外层以太网封装
═══════════════════════════════════════════════════════════════
│
│ ⑮ 封装后的完整数据包:
│
│ 此时 skb 内容:
│ ┌──────────────────────────────────────────────────────┐
│ │ (还没有外层Eth头) │
│ │ Outer IP │ UDP:6081 │ Geneve:VNI │ Inner Eth+IP+TCP │
│ └──────────────────────────────────────────────────────┘
│
│ ⑯ 内核路由查找
│ ip_route_output_key()
│
│ 查找路由表:
│ ┌────────────────────────────────────────────────────┐
│ │ $ ip route get 192.168.1.20 │
│ │ 192.168.1.20 via 192.168.1.1 dev eth0 │
│ │ ↑ 网关 ↑ 出接口 │
│ └────────────────────────────────────────────────────┘
│
│ 结果:
│ - 出接口 = eth0
│ - 下一跳 = 192.168.1.1 (网关) 或直连
│
│ ⑰ 邻居子系统 (ARP)
│ neigh_resolve_output()
│
│ 查找 ARP 缓存 / 发送 ARP 请求:
│ ┌────────────────────────────────────────────────────┐
│ │ 下一跳 192.168.1.1 → MAC = aa:bb:cc:dd:ee:01 │
│ │ │
│ │ 如果 ARP 缓存未命中: │
│ │ - 数据包暂存队列 │
│ │ - 发送 ARP Request │
│ │ - 收到 ARP Reply 后继续处理 │
│ └────────────────────────────────────────────────────┘
│
│ ⑱ 构建外层以太网头部
│ dev_hard_header()
│
│ ┌─────────────────────────────────────────────┐
│ │ Ethernet Header (14 bytes) │
│ │ │
│ │ Dst MAC: aa:bb:cc:dd:ee:01 (网关MAC) │
│ │ Src MAC: eth0 的 MAC 地址 │
│ │ EtherType: 0x0800 (IPv4) │
│ │ │
│ └─────────────────────────────────────────────┘
│
▼
═══════════════════════════════════════════════════════════════
第六阶段:物理网卡发送
═══════════════════════════════════════════════════════════════
│
│ ⑲ 最终完整数据包:
│
│ ┌────────────────────────────────────────────────────────────────┐
│ │ Outer Eth │ Outer IP │ UDP │ Geneve │ 原始以太网帧 │
│ │ 14 bytes │ 20 bytes │ 8B │ 8B+ │ │
│ │ │ │ │ │ │
│ │ dst: 网关 │ dst:192.168 │ dp: │ VNI: │ dst: VM-B MAC │
│ │ MAC │ .1.20 │ 6081 │ 0x1234 │ src: VM-A MAC │
│ │ src: eth0 │ src:192.168 │ sp: │ proto: │ type: 0x0800 │
│ │ MAC │ .1.10 │ 随机 │ 0x6558 │ IP+TCP+Payload │
│ │ type: │ proto: UDP │ │(Trans │ │
│ │ 0x0800 │ TTL: 64 │ │ Eth) │ │
│ └────────────────────────────────────────────────────────────────┘
│
│ 总封装开销: 14 + 20 + 8 + 8 = 50 bytes (无 Geneve Options)
│ 14 + 20 + 8 + 8 + N = 50+N bytes (有 Options)
│
│ ⑳ 进入网卡发送队列
│ dev_queue_xmit(skb)
│ → Traffic Control (qdisc) 排队
│ → 调用 eth0 的 ndo_start_xmit()
│
│ ㉑ 物理网卡驱动发送
│ ixgbe_xmit_frame() / mlx5e_xmit() / ...
│ - 填充 TX 描述符 (DMA descriptor)
│ - 将 skb 数据地址写入描述符
│ - 通知网卡 (doorbell register)
│ - 网卡通过 DMA 读取数据
│ - 网卡将以太网帧转换为电信号/光信号
│ - 发送到物理线缆
│ [复制 #5: DMA 传输]
│
│ ㉒ 发送完成
│ - 网卡发送完成中断
│ - 驱动释放 TX 描述符和 skb
│
▼
物理网络 → 交换机 → 远端节点
Geneve 封装的内核代码路径
// OVS 执行 output 到 geneve 端口
ovs_execute_actions()
→ do_output()
→ ovs_vport_send(geneve_port, skb)
// Geneve 端口发送
geneve_xmit(skb)
│
├── ① 查找路由 (确定出接口和下一跳)
│ rt = geneve_get_v4_rt()
│ → ip_route_output_key(目的IP=远端节点IP)
│ → 返回路由表项: {出接口=eth0, 网关=192.168.1.1}
│
├── ② 检查 MTU
│ if (skb->len + 封装头部总长 > 出接口MTU)
│ → 需要分片或返回 ICMP Too Big
│ → 这就是为什么隧道环境下有效 MTU 减小
│
├── ③ 构建 Geneve 头部
│ geneve_build_header()
│ → skb_push(skb, sizeof(genevehdr)) // 在 skb 前面扩展空间
│ → 填充 VNI, Protocol, Options
│
├── ④ 构建外层 UDP + IP + 发送
│ udp_tunnel_xmit_skb(rt, skb, src_ip, dst_ip, tos, ttl,
│ src_port, dst_port=6081)
│ │
│ ├── udp_set_csum() // UDP 校验和 (通常设为0)
│ ├── skb_push(skb, UDP头) // 添加 UDP 头
│ ├── iptunnel_xmit() // 添加 IP 头 + 发送
│ │ ├── skb_push(skb, IP头)
│ │ ├── ip_local_out(skb) // 进入内核 IP 输出路径
│ │ │ ├── NF_HOOK(POSTROUTING) // iptables/nftables
│ │ │ ├── ip_output()
│ │ │ ├── ip_finish_output()
│ │ │ │ ├── 检查是否需要分片
│ │ │ │ ├── neigh_resolve_output() // ARP 解析
│ │ │ │ ├── dev_hard_header() // 添加以太网头
│ │ │ │ └── dev_queue_xmit(skb) // 进入网卡发送
│ │ │ │ └── eth0->ndo_start_xmit(skb)
│ │ │ │ └── DMA 发送
skb 在封装过程中的变化
封装前 skb 内存布局:
┌─────────┬──────────────────────────────────────────────┐
│ headroom│ Eth(VM) │ IP(VM) │ TCP │ Payload │
│ (空闲) │ 14B │ 20B │ 20B │ ... │
└─────────┴──────────────────────────────────────────────┘
↑ ↑ ↑
skb->head skb->data skb->tail
步骤1: geneve_build_header() — skb_push 添加 Geneve 头
┌─────┬────────┬──────────────────────────────────────────┐
│ head│ Geneve │ Eth(VM) │ IP(VM) │ TCP │ Payload │
│room │ 8B+ │ 14B │ 20B │ 20B │ ... │
└─────┴────────┴──────────────────────────────────────────┘
↑
skb->data (向前移动了)
步骤2: udp_set_csum + skb_push — 添加 UDP 头
┌───┬─────┬────────┬──────────────────────────────────────┐
│hd │ UDP │ Geneve │ Eth(VM) │ IP(VM) │ TCP │ Payload │
│rm │ 8B │ 8B+ │ 14B │ 20B │ 20B │ ... │
└───┴─────┴────────┴──────────────────────────────────────┘
↑
skb->data (继续向前)
步骤3: iptunnel_xmit — 添加外层 IP 头
┌─┬───────┬─────┬────────┬────────────────────────────────┐
│ │Out IP │ UDP │ Geneve │ Eth(VM) │ IP(VM) │ TCP │ ... │
│ │ 20B │ 8B │ 8B+ │ 14B │ 20B │ 20B │ │
└─┴───────┴─────┴────────┴────────────────────────────────┘
↑
skb->data
步骤4: dev_hard_header — 添加外层以太网头
┌─────────┬───────┬─────┬────────┬────────────────────────┐
│Out Eth │Out IP │ UDP │ Geneve │ Eth(VM)│IP(VM)│TCP│... │
│ 14B │ 20B │ 8B │ 8B+ │ 14B │ 20B │20B│ │
└─────────┴───────┴─────┴────────┴────────────────────────┘
↑ ↑
skb->data skb->tail
💡 关键: 通过 skb_push() 不断向前扩展 skb->data 指针
不需要重新分配内存,只要 headroom 足够
如果 headroom 不够 → skb_cow_head() 重新分配(有复制开销)
MTU 问题详解
标准以太网 MTU = 1500 bytes
VM 发出的原始 IP 包最大长度:
┌───────────────────────────────┐
│ IP Header │ TCP │ Payload │ ≤ 1500 bytes
│ 20B │ 20B │ ≤ 1460B │
└───────────────────────────────┘
Geneve 封装后的外层 IP 包长度:
┌──────────┬─────┬────────┬─────────────────────┐
│ Outer IP │ UDP │ Geneve │ Inner Eth + 原始IP包 │
│ 20B │ 8B │ 8B+ │ 14B + ≤ 1500B │
└──────────┴─────┴────────┴─────────────────────┘
= 20 + 8 + 8 + 14 + 1500 = 1550 bytes ← 💥 超过物理 MTU!
解决方案:
┌───────────────────────────────────────────────────────────┐
│ │
│ 方案1: 减小 VM 的 MTU │
│ VM 内 MTU = 1500 - 50 = 1450 │
│ VM 发出的 IP 包 ≤ 1450 │
│ 封装后 ≤ 1450 + 50 = 1500 ✅ │
│ 缺点: 需要配置 VM 内的 MTU │
│ │
│ 方案2: 增大物理网络 MTU (Jumbo Frame) │
│ 物理交换机 + eth0 MTU = 9000 │
│ VM 内 MTU 保持 1500 │
│ 封装后 1500 + 50 = 1550 < 9000 ✅ │
│ 缺点: 需要全链路支持 Jumbo Frame │
│ │
│ 方案3: Kube-OVN 默认做法 │
│ 设置 Pod/VM 的 MTU = 1400 │
│ 留足够的 headroom 给隧道封装 │
│ │
└───────────────────────────────────────────────────────────┘
UDP 源端口与 ECMP 负载均衡
为什么 Geneve 的 UDP 源端口要随机化?
物理网络:
┌──── 链路 A ────┐
发送节点 ────────▶│ 交换机 (ECMP) │────────▶ 接收节点
│ │
│ hash(五元组) │
│ 决定走哪条链路 │
│ │
└──── 链路 B ────┘
如果所有隧道流量的 UDP 源端口固定:
所有包的五元组 = (src_ip, dst_ip, UDP, 固定src_port, 6081)
→ hash 值相同 → 全部走链路 A → 链路 B 空闲 ❌
随机化 UDP 源端口:
流量1: (src_ip, dst_ip, UDP, 49152, 6081) → hash=X → 链路 A
流量2: (src_ip, dst_ip, UDP, 53201, 6081) → hash=Y → 链路 B
→ 流量均匀分布 ✅
源端口计算方式:
src_port = hash(内层源IP, 内层目的IP, 内层源端口, 内层目的端口, 协议)
→ 同一条内层流 → 相同的外层 UDP 源端口 → 相同的物理路径
→ 保证同一条 TCP 连接的包不会乱序
Geneve Options(OVN 特有)
OVN 使用 Geneve Options 携带逻辑网络元数据:
┌─────────────────────────────────────────────────────────────┐
│ Geneve Options │
│ │
│ Option Class = 0x0102 (OVN) │
│ Option Type = 0x80 │
│ Option Data: │
│ ┌──────────────────────────────────────┐ │
│ │ Logical Ingress Port (逻辑入端口) │ ← VM-A 的逻辑端口│
│ │ Logical Egress Port (逻辑出端口) │ ← VM-B 的逻辑端口│
│ │ Logical Datapath (逻辑数据路径) │ ← 逻辑交换机/路由器│
│ └──────────────────────────────────────┘ │
│ │
│ 这些元数据让远端节点的 OVS 不需要重新做完整的逻辑流表匹配 │
│ 直接知道这个包属于哪个逻辑网络、从哪个逻辑端口来的 │
│ │
└─────────────────────────────────────────────────────────────┘
与收包流程的对称对比
发包 (本节点 VM → 远端节点 VM): 收包 (远端节点 VM → 本节点 VM):
───────────────────────────── ─────────────────────────────
VM send() VM recv()
↓ ↑
virtio TX vring virtio RX vring
↓ ↑
vhost-net → tap0 vhost-net ← tap0
↓ ↑
Linux Bridge 转发 Linux Bridge 转发
↓ ↑
veth-pod → veth-host veth-pod ← veth-host
↓ ↑
OVS br-int 流表匹配 OVS br-int 流表匹配
↓ ↑
output: geneve_sys input: geneve_sys
↓ ↑
┌─────────────────┐ ┌─────────────────┐
│ Geneve 封装 │ │ Geneve 解封装 │
│ + UDP 头 │ │ 剥离外层头部 │
│ + 外层 IP 头 │ │ IP→UDP→Geneve │
│ + 外层 Eth 头 │ │ → 原始帧 │
└────────┬────────┘ └────────┬────────┘
↓ ↑
内核路由 + ARP 内核 IP/UDP 栈处理
↓ ↑
eth0 DMA 发送 eth0 DMA 收包
↓ ↑
物理网络 ═══════════════════════════════════▶ 物理网络
一句话总结
Geneve 发包的核心是 OVS 流表决定目的节点 → geneve_xmit() 通过 skb_push() 依次添加 Geneve 头、UDP 头、外层 IP 头 → 内核路由查找确定出接口和下一跳 → ARP 解析添加外层以太网头 → eth0 网卡驱动 DMA 发送。整个过程中 UDP 源端口随机化实现 ECMP 负载均衡,Geneve Options 携带 OVN 逻辑网络元数据,MTU 需要预留 50+ 字节的封装开销。