做Linux网络内核调试的人,几乎都绕不开VXLAN。K8s里Flannel的VXLAN后端、OpenStack里的overlay网络、各种容器网络方案,喊的都是同一个东西:用UDP隧道把二层帧送到远端。很多人对“VXLAN原理”聊得头头是道,但一落到内核里就会懵——同样是vxlan0,为什么tcpdump -i vxlan0能看到包,对端VM就是不通?为什么MTU要设1450而不是1500?这些问题的答案,都藏在Linux内核VXLAN的收发包流程里。
这篇文章我以内核5.15/6.1左右的源码主线来拆,代码路径主要在drivers/net/vxlan/vxlan_core.c。适合正在看内核网络源码的人、被overlay网络问题折磨的运维、以及刚接触云网络想搞懂数据面的同学。我会把“发包查哪张表、收包从哪个回调进来、两张FDB怎么配合、MTU和offload怎么影响数据面”一次讲透。
1. VXLAN在内核里的位置:一台用软件做出来的远端二层交换机
1.1 VXLAN本质:MAC-in-UDP
VXLAN做的事情很简单,把原始的二层以太网帧当成一个整体,外面套上VXLAN头、UDP头、IP头,然后从三层网络送出去。对端收到后剥掉外层头,把原来的二层帧重新注入到本地的虚拟网络中。
这个“远程二层网络”里有两个核心概念:
- VTEP(VXLAN Tunnel Endpoint):隧道端点,通常就是运行vxlan模块的Linux主机或交换机,负责封装和解封装。
- VNI(VXLAN Network Identifier):24比特的网络标识,用来区分不同的二层网络。VLAN只有12比特,VXLAN的VNI能支持更多租户/网络隔离。
VXLAN头非常标准,8个字节,其中最重要的就是VNI:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |R|R|R|R|I|R|R|R| Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | VXLAN Network Identifier (VNI) | Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+I标志位是1表示这是一个有效的VXLAN报文;VNI就是后面24位。Linux内核在做收包的时候,第一件事就是看这个VNI能不能和本地vxlan设备对上,对不上直接丢。
1.2 内核vxlan模块在数据面上的角色
从内核视角看,vxlan设备就是一张虚拟网卡。它注册了自己的net_device_ops,实现了ndo_start_xmit作为发包入口;收包则是通过UDP封装回调从协议栈拿到报文后,注入到这张虚拟网卡上。
但它和普通网卡最大的区别是:普通网卡发出去的包直接走物理链路,而vxlan设备发出去的包是先查一张“MAC地址到远端VTEP IP”的映射表,然后重新封装成UDP/IP包,交给本机的三层的路由栈发出去。
所以VXLAN收发包流程本质上是两级转发:
- 本地一级:内层MAC地址域,决定这个二层帧该进哪个本地端口或虚拟设备。
- 远端一级:外层IP地址域,决定这个封装后的UDP包该从哪个underlay路由送到哪个VTEP。
理解了这个两级结构,后面看代码就不会乱。
2. 发包流程:内层帧如何在vxlan_xmit里被查表、封装、甩给路由栈
2.1 入口:bridge把内层帧交给vxlan_xmit
典型场景里,vxlan0端口是挂在Linux bridge下的。VM发出来的帧先进bridge,bridge查自己的FDB,发现目的MAC对应端口是vxlan0,于是把帧从vxlan0这个端口发出去。
具体路径是这样的:
VM/veth -> br0 -> bridge FDB -> dev_queue_xmit(br0 -> vxlan0) -> __dev_queue_xmit -> dev_hard_start_xmit -> vxlan_xmitvxlan_xmit就是vxlan设备的ndo_start_xmit。到这一步,内层以太网头仍然原封不动留在skb里,eth_hdr(skb)可以直接读到目的MAC。
如果你的环境没有bridge,直接给vxlan0配了IP,流程也一样。比如从本机ping一个远端内网IP,协议栈会先在vxlan0上发ARP请求。ARP请求是一个广播帧,照样会走vxlan_xmit被封进UDP隧道发出去。所以记住一个原则:vxlan设备只认识二层帧,所有发给它的包都是以太网帧。
2.2 vxlan_xmit_one:查FDB、选隧道目的、封装
vxlan_xmit本身做的事情不多,核心逻辑在vxlan_xmit_one里。我按源码顺序拆一下:
- 取出VNI。如果设备配置了
collect metadata,VNI从skb_dst(skb)->tun_info里拿;否则用创建vxlan设备时指定的id。 - 用内层目的MAC查vxlan设备的FDB,函数是
vxlan_fdb_find_rcu(vxlan, eth_hdr(skb)->h_dest, vni)。 - FDB命中,拿到对应的
struct vxlan_rdst,里面有远端IP、远端端口、远端VNI。FDB没命中,就用设备创建时的默认remote或组播地址。 - 确认有足够的headroom,
skb_cow_head避免改写skb时复制整个包。 - 调用
vxlan_xmit_one内部逻辑构建外层头,最终通过udp_tunnel_xmit_skb把外层IP/UDP头补上,并走ip_local_out发出。
这里面最容易踩坑的是第3步。很多人以为vxlan的FDB和bridge的FDB是一张表,不是。vxlan设备的FDB解决的是“内层MAC应该封装到哪个远端VTEP”,bridge的FDB解决的是“本地VM的帧应该从哪个本地端口出去”。两张表在一条链路里是串联关系。
udp_tunnel_xmit_skb这个函数很关键。它一次性完成:
- 选择外层源IP和目的IP(目的IP就是你查FDB得到的远端VTEP地址)
- 填UDP头,目的端口默认4789
- 填外层IP头,计算校验和
- 通过路由栈发出
如果vxlan设备有多个远端IP(比如FDB里一个MAC对应多个VTEP),内核会逐个调用发送路径,相当于把同一份内层帧复制多份,分别封装发往不同VTEP。
2.3 为什么UDP源端口要用内层五元组哈希
VXLAN用UDP端口有个好处:源端口可以随便变。Linux内核在封装时不会傻傻地用固定源端口,而是用内层报文的五元组哈希出一个源端口值,函数是vxlan_src_port。
这么做的目的是为了underlay的负载均衡。物理网络上的交换机一般会基于五元组做ECMP哈希。如果所有VXLAN流都从同一个UDP源端口出去,那它们在外层看来就是同一条流,ECMP没法拆开,很容易把流量打到一个劣化链路或一个CPU队列上。源端口按内层哈希变化后,外层五元组就不同了,underlay路由器才能把不同租户的流量散到多个路径上。
在物理网卡支持RSS的情况下,这个哈希还会影响收包端的CPU分发。所以遇到VXLAN吞吐打不满、单核CPU被打爆的时候,先查一下UDP源端口分布和网卡RSS队列配置,比盲目调内核参数管用。
发包侧的调试命令也简单,抓外层包就能看到VXLAN封装长什么样:
tcpdump -ni eth0 udp port 4789 -vv你会看到类似这样的输出:
IP 192.168.1.10.45871 > 192.168.1.2.4789: VXLAN, flags [I] (0x08), vni 42 00:ff:aa:bb:cc:01 > ff:ff:ff:ff:ff:ff, ethertype ARP外层是192.168.1.10 -> 192.168.1.2的UDP包,里面装的是一个完整的ARP广播帧。这就是VXLAN封装最直观的样子。
3. 收包流程:UDP 4789端口上的包是怎么被认领并还原理的
3.1 UDP socket上的encap_rcv回调
VXLAN的收包并不是协议栈把UDP数据送到某个用户态进程,而是直接在内核UDP层被截获。
vxlan模块在创建UDP socket时,会调用setup_udp_tunnel_sock,把这个socket的encap_rcv回调指向vxlan_rcv。当物理网卡上收到目的端口为4789的UDP包,经过IP层进入udp_rcv,再到udp_queue_rcv_skb时,内核会检查这个UDP socket是否设置了encap_rcv。设置了就直接调它,而不是走正常的数据报socket接收逻辑。
这和其他隧道协议如GENEVE、ERSpan是同一个套路。用一句话说:VXLAN收包不是“内核把包交给了用户态应用”,而是“内核在UDP层把包截留下来,转交给vxlan驱动处理”。
3.2 vxlan_rcv逐项检查和MAC学习
vxlan_rcv拿到skb后,做几件很机械的事:
- 检查外层UDP长度、VXLAN头长度。
- 检查VXLAN头的flags和VNI,VNI是从
vxlan_hdr(skb)->vx_vni里取的。 - 用
vxlan_lookup按socket和VNI找到本机对应的struct vxlan_dev。找不到就丢包,因为本地没有租户报这个VNI。 - 如果可以学习,调用
vxlan_snoop做源MAC学习:记录“内层源MAC是从哪个远端IP学习到的”,更新vxlan FDB。 - 把skb的接收设备改成vxlan设备,
skb->dev = vxlan->dev,然后调用eth_type_trans设置好协议类型。 - 交给
gro_cells_receive或netif_receive_skb,让报文进入本机网络栈,或者被bridge收走。
第4步的vxlan_snoop是VXLAN能实现“MAC自动学习”的核心。对端主机上VM A发出的帧到了你这台VTEP后,你的vxlan设备会把“VM A的MAC地址对应远端VTEP IP 192.168.1.10”这个信息记进自己的FDB。以后再往VM A发帧,就不需要广播泛洪了,直接封装发给192.168.1.10即可。
如果关闭了学习功能(创建vxlan时加了nolearning标志),那vxlan FDB基本靠人工维护或全部泛洪,多节点场景很容易出现“查不到FDB所以封不到正确VTEP”的问题。
3.3 从gro_cells到bridge/协议栈
gro_cells_receive是我很想强调的一点。早期vxlan收包是直接netif_rx上交的,性能一般。现在驱动用每个vxlan设备自己的gro_cells结构,把从隧道解出来的内层帧先交给本设备的NAPI调度,通过napi_gro_receive做一次GRO合并,再送到netif_receive_skb。
这带来的好处是:如果一条TCP流拆成了很多个小VXLAN包到达,内核可以在隧道层就把它们合并成大skb再往上走,减少bridge和协议栈的处理次数。很多人在同一套underlay下,发现老内核的VXLAN性能差得离谱,部分原因就是缺少GRO路径。
报文到达netif_receive_skb之后,如果vxlan0是bridge的端口,那这个内层帧就会被bridge接收,bridge会学源MAC在vxlan0上,再查目的MAC决定送到哪个本地端口。如果目的MAC不在本地,bridge又会把帧丢回vxlan0,vxlan驱动再按自己的FDB封装发往下一个VTEP。这就是“VXLAN交换机”的完整行为。
4. 翻车点:很多人把bridge的FDB和vxlan设备的FDB混成一张表
4.1 两张表的职责对比
我用一张表把这几年被问得最多的问题说清楚。
| 表名 | 属于谁 | 查询键 | 查到的内容 | 出现环节 |
|---|---|---|---|---|
| bridge FDB | Linux bridge | 内层目的MAC | 本地端口(veth/vxlan0) | VM帧第一次到达br0时 |
| vxlan FDB | vxlan设备 | 内层目的MAC | 远端VTEP IP/UDP端口 | vxlan_xmit内层封装的查表 |
注意看,两张表的key都是内层目的MAC,但value完全不同。一个告诉你在本地该走哪个网卡端口,另一个告诉你在隧道里该封装给哪个远端IP。
4.2 一次完整的东西向流量是怎么串起两张表的
假设两套宿主机,每套都有br0、VM、vxlan0:
VM A(00:00:00:00:00:01) -> br0VM A发往VM B(MAC为00:00:00:00:00:02)时:
- br0查bridge FDB,发现
00:00:00:00:00:02对应端口是vxlan0,帧被送到vxlan设备的vxlan_xmit。 - vxlan_xmit查vxlan设备自己的FDB,发现
00:00:00:00:00:02对应的远端IP是192.168.1.2,于是按这个IP封装成UDP包发出去。 - 对端
vxlan_rcv解封装,把内层帧交给vxlan0。 - 对端br0收到从vxlan0上来的帧,查bridge FDB,发现
00:00:00:00:00:02对应veth端口,于是把帧送到VM B。
中间任何一张表缺了对应entry,流量就会降级成泛洪。在没有组播的云环境里,某些平台还会借助控制面下发FDB,避免未知单播全部打到默认remote。
实际操作中,你可以用bridge命令同时看到这两张表:
# 查看vxlan设备的FDB(MAC -> 远端VTEP) bridge fdb show dev vxlan0 # 查看bridge的FDB(MAC -> bridge端口) bridge fdb show如果你发现bridge fdb show dev vxlan0里有远端MAC,但bridge fdb show里查不到对应端口,那问题多半出在bridge这一层;反过来也一样。这个排查方向很管用。
5. 收发包流程里最容易出问题的三个隐藏环节:MTU、GRO/GSO、校验和
5.1 MTU计算:别把vxlan的50字节额外开销忘了
VXLAN封装会额外加上外层以太网头(14字节)、外层IP头(20字节)、UDP头(8字节)、VXLAN头(8字节),合计50字节。超过underlay MTU的包会被分片,而很多网络中间设备会直接丢弃分片包,表现就是“小包通、大包不通”。
如果underlay物理链路MTU是1500,vxlan0的MTU一般建议设成1450,给整条overlay路径留出余量。对于企业内部网络能开9000巨型帧的,vxlan0可以设成8950左右。
实际遇到问题时别光看配置,要实测:
# 从VM或宿主机上测 ping -M do -s 1450 <对端内网IP>如果-s 1450通而-s 1472不通,基本就是MTU问题。TCP场景下还可能表现为“连接建立成功但传输卡死”,因为大的TCP段在隧道里被MTU卡住,又拿不到ICMP提示,导致MSS协商异常。VXLAN下建议把内层的MSS显式调小。
5.2 GSO/GRO与硬件offload:决定VXLAN跑得快不快
发送路径上的GSO(Generic Segmentation Offload),允许协议栈把很大的TCP数据块作为一个大skb交给vxlan驱动,vxlan驱动在封装完之后,再让网卡或内核拆成普通MTU大小的报文发出去。网卡支持tx-udp_tnl-segmentation时,大包分割可以下推到物理网卡,CPU占用会明显下降。
接收路径上的GRO在前面说过,是把多个小的隧道包合并成大skb。这两项开没开,对VXLAN转发性能影响极大。
排查时可以用:
ethtool -k eth0 | grep tunnel重点关注tx-udp_tnl-segmentation和rx-gro-list这类能力。如果网卡不支持,内核会在软件层面用skb_segment完成分割,CPU会上升,但功能正常。真正恐怖的是网卡宣称支持但驱动bug导致丢包,这种情况只能靠计数器对比来发现。
5.3 校验和:tcpdump里看到bad checksum不一定是坏包
VXLAN的外层UDP校验和,在IPv4下不是强制要求,但很多网卡驱动默认开着计算。由于硬件校验和offload的存在,tcpdump在网卡驱动把校验和字段填好之前抓包,经常显示bad udp checksum,这不一定代表网络上有坏包,可能只是抓包点早于硬件填充。
判断方法很简单:在收包端看ethtool -S里有没有rx_csum_offload_errors这类计数器,或者直接关掉网卡校验和卸载再抓一次。如果你在内层也开了类似vxlan硬件校验,记得确认driver版本是否匹配,否则会出现间歇性丢包。
开启和关闭vxlan设备本身的一些选项也有影响,比如:
ip link set vxlan0 type vxlan udpcsum ip link set vxlan0 type vxlan noudpcsumIPv6 underlay场景,UDP校验和是强制的,别为了省CPU关掉。
6. 一次“对端vxlan0能收到包,VM还是不通”的完整排查思路
6.1 三个阶段抓包定位
我遇到最多的VXLAN问题不是“包完全不来”,而是“包到了对端vxlan0,但VM不通”。这种问题讲真比纯断网难搞,因为它涉及的是解封装之后的行为。
我的习惯是把抓包点分成三段:
- underlay入口:
tcpdump -ni eth0 udp port 4789 - 解封装之后:
tcpdump -ni vxlan0 - 虚拟机内部:
tcpdump -ni eth0
如果eth0上能看到VXLAN包,但对端vxlan0上没包,说明问题出在vxlan收包侧。这时候优先查:
- 两端VNI是否一致:
ip -d link show vxlan0里的id和remote。 - 两端UDP端口是否一致:默认4789,自定义别对不上。
- vxlan设备是否处于
UP状态、是否被拉进了正确的network namespace。 - 对端的VNI过滤是否因为
collect metadata等原因导致查找失败。
如果vxlan0上能看到包,但VM还是不通,那问题已经在bridge/路由这一层了。
6.2 VNI、端口、FDB和rp_filter的排查顺序
当然,有些问题在FP层看不出来,我在实际排障时通常会按这个顺序往下走。
第一看FDB。在发送端执行:
bridge fdb show dev vxlan0如果要到VM B的MAC没有对应远端IP,并且你处于多节点环境,而创建vxlan时用的是remote单播模式,那未知单播只会发往这一个默认remote。一旦流量目的不在这个VTEP后面,包就送不到正确宿主机。这种环境应该用组播、或者依赖控制面下发FDB。
第二看bridge的FDB:
bridge fdb show | grep vxlan0如果目的MAC在发送端bridge里都缺,那说明bridge不知道这个MAC该从vxlan0走,可能会把帧继续从其他端口广播,甚至丢掉。
第三看rp_filter。多网卡、多VTEP环境下,VXLAN解封装后的内层源地址和物理入接口可能不一致,触发反向路径过滤,导致回包被丢。需要确认:
sysctl net.ipv4.conf.all.rp_filter sysctl net.ipv4.conf.<iface>.rp_filter如果值不是0,且你的overlay流量路径确实是非对称的,可以针对overlay接口单独调成0。真遇到这个问题时,外面抓包看得到请求,但回包出不去,很容易被误判成对端丢包。
6.3 一些小经验
调VXLAN问题,我自己的习惯是把tcpdump落点分成underlay入口、vxlan0、VM侧三段,先定位到段,再回头看VNI、FDB、MTU、rp_filter。多数的“对端vxlan0能看到包但VM不通”,最后都是MTU或bridge FDB过期导致的。另外,看内核源码时不要一头扎进vxlan_xmit里不出来,先把收发包主链路跑通,再去追vxlan_rcv里的学习逻辑和vxlan_fdb_update,会比逐行啃源码高效得多。