域名解析出 IP 之后,包还要经过两道决定它能不能到、以及能带多大的关卡:路由表按最长前缀选出口,链路按 MTU 决定单个包的上限。
这两件事平时不用管,直到出现一类很难查的故障——小请求全部正常,大一点的响应就卡住不动,而且两端的日志都看不出异常。这几乎总是 MTU 的问题,而它之所以难查,是因为链路某处把用来协商 MTU 的 ICMP 报文丢掉了,协商失败得静悄悄。
这篇从 CIDR 前缀和路由表的匹配规则讲起,再到内核用什么数据结构做这个匹配、路径 MTU 是怎么被发现的、它在哪些情况下会失效,以及封装(VPN、隧道、Overlay)为什么让这个问题变常见。
一、LPM 的数学建模与底层数据结构
无类别域间路由(CIDR)彻底打破了传统的 ABC 类网络划分。当路由器收到一个目标 IP,它必须在数以百万计的 BGP 路由表中找到最精确的子网匹配。如果在数据面上采用线性遍历,设备在微秒内就会被流量打挂。
现代 Linux 内核在 IPv4 路由表中使用的是 LC-Trie(Level-Compressed Trie,层级压缩前缀树)。标准二叉前缀树需要 \(O(W)\) 次查找(对于 IPv4,\(W = 32\)),而 LC-Trie 通过压缩稀疏分支和单一路径节点,极大地降低了查找深度。
从数学角度来看,对于一个具有 \( n \) 条路由表项的 LC-Trie,其期望查找深度被严格约束在 \( O(\log n) \)。内核通过巧妙的内存布局,将整个树节点压缩为连续的数组,从而保证一次 FIB 查找能完美命中 CPU L1/L2 Cache Line。
/* 节选自 linux/net/ipv4/fib_trie.c */
struct key_vector {
t_key key;
unsigned char pos; /* 2log(KEYLENGTH) bits needed */
unsigned char bits; /* 2log(KEYLENGTH) bits needed */
unsigned char slen;
union {
/* 数组大小动态扩展为 2^bits */
struct key_vector __rcu *tnode[0];
struct fib_alias __rcu *leaf;
};
};
内核中的 fib_lookup 函数负责遍历该结构。得益于 RCU(Read-Copy-Update)锁机制,数据面(Data Plane)的查找是完全无锁(Lock-free)的,彻底消除了多核并发时的缓存颠簸(Cache Contention)。

通过 eBPF/XDP 可视化路由决策
在 Cloudflare 或 Meta 这样的极限性能架构中,数据包根本没有机会进入传统的内核 IP 协议栈。取而代之的是,eBPF (Extended Berkeley Packet Filter) 程序被直接挂载在网卡驱动的 XDP(eXpress Data Path)层,并在解析出目标 IP 后,直接调用 FIB。
flowchart TD
A[网卡 Rx Ring 接收数据
Dest: 10.22.18.7] --> B{XDP Hook
执行 eBPF 字节码}
B -->|调用 bpf_fib_lookup| C((内核 FIB
LC-Trie))
C -.->|命中 10.22.16.0/20| B
B --> D{MTU 物理边界检查}
D -->|<= MTU| E[返回 XDP_REDIRECT
零拷贝直接转发出网卡]
D -->|> MTU| F[返回 XDP_PASS
推送到通用 sk_buff 协议栈处理]
F --> G[ip_local_deliver / ip_forward]
二、MTU、MSS 与封装重叠的数学灾难
路由决定了出站网卡(Egress Interface),而网卡的硬件特性决定了最大传输单元(MTU)。物理以太网的 MTU 一般定死在 1500 字节。对于上层的 TCP 负载来说,必须严格计算最大报文段长度(MSS)。
在现代数据中心中,流量往往被多次封装(例如运行在 IPsec 隧道上的 VXLAN Overlay 网络)。此时的 MSS 计算公式为:
$$ MSS = MTU_{phys} – H_{eth} – H_{ipsec} – H_{vxlan} – H_{outer\_ip} – H_{inner\_ip} – H_{tcp} $$
以标准的 IPv4 over IPsec (AES-GCM) 为例:
物理网卡 MTU: 1500 bytes
IPv4 Header: 20 bytes
ESP 头部 + IV + Trailer + ICV: ~40 bytes
内层 IPv4 Header: 20 bytes
内层 TCP Header: 20 bytes
实际有效 MSS = 1500 - 20 - 40 - 20 - 20 = 1400 Bytes
绝对不要依赖网络层的 IP 分片(IP Fragmentation)。 IP 分片会触发内核 ip_fragment() 函数,该函数会暴力分配新的 sk_buff 结构、复制 Payload 并重新计算 Checksum。在 10Gbps+ 的流量下,IP 分片会瞬间打爆系统的 ksoftirqd 软中断,引发灾难性丢包。强制在传输层执行 MSS 钳制(MSS Clamping)或依赖网卡硬件卸载(TSO/GSO)是唯一出路。
三、内核级故障排查:`perf` 与 PMTUD 黑洞探测
传统的路径 MTU 发现(PMTUD)高度依赖网络设备返回的 ICMP Fragmentation Needed (Type 3 Code 4) 报文。然而,当安全防火墙一刀切屏蔽 ICMP 时,就形成了所谓的 PMTU 黑洞,TCP 协议栈等不到缩减 MSS 的信号,连接直接卡死。
高级工程师可以直接使用 perf 和 eBPF 在内核探针(kprobe)层面追踪这些失败事件:
# 追踪内核接收到的 ICMP 需要分片事件
sudo perf record -e icmp:icmp_unreach -a -g
# 追踪 TCP 当 PMTUD 失效时启动的 MTU 盲探针 (RFC 4821)
sudo perf probe -a tcp_mtu_probing
sudo perf record -e probe:tcp_mtu_probing -aR
# 实时追踪 eBPF FIB 路由查找失败的原因
sudo bpftrace -e 'kprobe:bpf_fib_lookup { @[kstack] = count(); }'
当传统的 ICMP 探测失败时,Linux 内核通过 RFC 4821(PLPMTUD)机制在传输层盲探最优 MTU。开启 sysctl net.ipv4.tcp_mtu_probing=1 后,内核会在发生超时重传时主动下调 MSS。
/* 节选自 net/ipv4/tcp_timer.c 中处理 PMTU 黑洞的核心逻辑 */
static void tcp_mtu_probing(struct inet_connection_sock *icsk, struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 尝试探测更大 MTU,或者在探测失败(黑洞)时收缩 MSS */
if (tcp_mtu_probe(sk)) {
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
}
}
四、深度生产架构 Post-Mortem 事故复盘
eBPF / XDP 路由旁路的 MTU 陷阱
在为全球级 CDN 边缘节点构建多 T 级吞吐的路由网关时,传统的 Linux
ip_forward架构在单核 2Mpps(每秒百万包)左右便触及了sk_buff内存分配的性能天花板。我们重构了整个数据面,将 eBPF 程序通过 XDP 直接挂载到 NIC 接收队列。该程序提取目标 IP,调用bpf_fib_lookup()查询路由表,重写源和目的 MAC 地址,最后通过XDP_REDIRECT绕过内核直接从出站网卡打出。吞吐量直接飙升至单核 14Mpps,实现了完美的 Zero-copy 转发。然而,一场隐形的灾难降临了:MTU 陷阱。XDP 处理的是最底层的原始以太网帧,它并不理解 IP 分片。如果入站的数据帧大于出站接口的 MTU 限制,
XDP_REDIRECT会没有任何报错地 静默丢弃 (Silent Drop) 该数据包。最终的修复方案是:在 eBPF C 代码中加入严格的 MTU 边界判定,一旦发现超长数据包,立刻在 eBPF 内部手搓一个ICMP Packet Too Big报文,并通过XDP_TX原路反射给发送方,以此强行补全网络层的 PMTUD 语义规范。
五、数据面自动化验证脚本
现代 SRE 运维拒绝使用正则表达式去解析 ip route 的输出。以下是一段直接通过 Netlink Socket 与 Linux 内核进行二进制通信的 Python 脚本,以编程方式还原 FIB 的精确判决。
from pyroute2 import IPRoute
import math
ipr = IPRoute()
destination = "10.22.18.7"
# 直接查询内核 Netlink 套接字,获取精准的路由决策
route = ipr.route('get', dst=destination)[0]
attrs = dict(route['attrs'])
egress_iface = attrs.get('RTA_OIF')
gateway = attrs.get('RTA_GATEWAY', 'Direct Connect')
mtu = attrs.get('RTA_METRICS', {}).get('RTAX_MTU', 1500)
print(f"目标 IP: {destination}")
print(f"出站网卡 Index: {egress_iface}")
print(f"下一跳网关: {gateway}")
print(f"Path MTU: {mtu} bytes")
# TSO 硬件分片数学计算
ip_tcp_headers = 40
payload_size = 65535 # TSO 支持的最大巨型帧 (Super-frame)
segments = math.ceil(payload_size / (mtu - ip_tcp_headers))
print(f"网卡 TSO 引擎将在物理层把此巨型帧切分为 {segments} 个 MTU 标准段。")
相关 DPDK 和 XDP 测试跑分,已开源归档在 cidr-mtu-results.csv。
六、动画解析:零拷贝重定向与切片
七、工程防坑指南 (Anti-Patterns)
- 滥用主机路由 (/32): 在路由表中疯狂下发数百万条
/32的独立路由会彻底破坏 LC-Trie 的压缩效率,导致 CPU L3 Cache 大面积失效(Cache Miss),将查询延迟推高几个数量级。务必执行路由聚合(Route Aggregation)。 - 全局禁用 ICMP: 这是最愚蠢的安全策略。干掉
ICMP Type 3 Code 4会直接导致 PMTUD 瘫痪,在 VPN 和 Overlay 架构中引发无数“薛定谔的超时”。安全组必须对 PMTU ICMP 予以放行。 - 无视巨型帧 (Jumbo Frames): 在后端分布式存储网络(如 Ceph, iSCSI)中,如果死守 1500 字节的 MTU,海量的 TCP 头部开销和网卡中断会把服务器性能拖垮。内网存储务必开启 9000 字节 MTU。
- 错把 TSO 当违规: 新人常用
tcpdump抓包,看到服务器吐出高达 65KB 的超大 TCP 包,就以为 MTU 配置失效了。实际上这是内核开启了 TSO(TCP Segmentation Offload),它将切包的苦力活卸载给了物理网卡的芯片,抓包工具在网卡前端抓取到的仅仅是未切片的“巨型聚合帧”。
八、路由与 MTU 证据矩阵
排查路由和 MTU 问题时,最忌只记录“通”或“不通”。下面的矩阵把每个结论绑定到可复核证据:路由命中、下一跳、出站 MTU、ICMP 可达性和硬件卸载状态。这样读者可以判断问题来自最长前缀匹配、PMTUD 黑洞、overlay 封装,还是抓包工具对 TSO/GSO 的误读。
| 问题现象 | 优先检查的层 | 应保留的证据 | 判断边界 |
|---|---|---|---|
| 目标 IP 走错出口 | FIB/LPM | ip route get、Netlink 查询结果、匹配前缀和下一跳。 |
确认是否是更长前缀覆盖默认路由,而不是 DNS 问题。 |
| 小包正常,大包超时 | Path MTU | DF ping、tracepath、ICMP Packet Too Big、接口 MTU。 |
若 ICMP 被屏蔽,应再检查 PLPMTUD 和 MSS 收缩。 |
| 隧道链路偶发卡死 | Overlay 封装 | GRE/IPsec/WireGuard 额外头部、有效 MSS、重传事件。 | 封装后 payload 可用空间减少,不能只看物理网卡 MTU。 |
| 抓包显示 65KB 大包 | TSO/GSO 卸载 | ethtool -k、抓包位置、网卡卸载开关和实际出线帧。 |
内核前端抓到的是 super-frame,不代表线缆上违反 MTU。 |
FAQ
为什么内核路由表不用哈希表 (Hash Table)?
哈希表确实有 \(O(1)\) 的极致速度,但它只适合 精确匹配 (Exact Match)。而 IP 路由基于最长前缀匹配,路由器在拿到数据包之前,根本不知道该套用掩码 /24 还是 /20。Trie 树天然契合层级递进的匹配逻辑。内核只有在连接跟踪表(Conntrack,五元组精确匹配)中才会大规模使用哈希结构。
MTU 怎么会影响 BGP 邻居建立?
BGP 是建立在 TCP 端口 179 之上的。如果两台骨干路由器通过 IPsec 隧道建立 BGP Peer,且 MTU 不匹配。因为最初的 BGP OPEN 握手报文很小,TCP 能够正常建连;但随后推送包含几十万条记录的 BGP UPDATE 大包时,超过 MTU 的包被丢弃。这会导致 BGP 状态机在 ESTABLISHED 和 IDLE 之间无限震荡(Flapping)。
References
了解了路由的硬件加速和切包边界后,下一篇文章我们将通过火焰图和 BBR 数学模型,深度剖析 TCP 协议栈如何基于这些网络底层的丢包反馈,来动态演算拥塞窗口。