一、概述 (Overview)
Linux 内核中的 ECMP(Equal-Cost Multi-Path)选路机制允许将流量分布到多条等价路径上。
核心实现主要分为两个阶段, 分别对应两个函数。
- hash计算 :
fib_multipath_hash()(net/ipv4/route.c)—— 计算 hash 值
如根据skb或flow的五元组、三元组等策略,计算出hash值。 - 根据hash选择nexthop:
fib_select_multipath()(net/ipv4/fib_semantics.c)—— 用 hash 选 nexthop
本文基于 Linux v6.16 源码,逐层剖析 ECMP 的 hash 计算与路径选择全流程。
使用coebuddy+claude-4.6-ops辅助生成。
二、整体架构 (Architecture)
2.1 内部流程
2.2 ECMP入口
ECMP选路的入口是 fib_select_path(),当路由查找命中多路径时触发:
1 | /* net/ipv4/fib_semantics.c */ |
三、第一阶段:Hash 计算 — fib_multipath_hash()
fib_multipath_hash() 根据 sysctl net.ipv4.fib_multipath_hash_policy 分发 4 种 hash 策略:
| 策略 | sysctl 值 | Hash 字段 | 适用场景 |
|---|---|---|---|
| L3 | 0 | saddr + daddr | 通用默认 |
| L4 | 1 | 五元组 | 双 ISP / 数据中心 |
| Inner L3 | 2 | 内层 saddr + daddr | GRE/VXLAN 中转 |
| Custom | 3 | 位掩码自定义 | 高级定制 |
各策略的完整源码解析与 hash 算法(SipHash/jhash2、seed 防极化)详见 ECMP Hash 计算详解: fib_multipath_hash() 四种策略全解析。
四、第二阶段:用 Hash 选 Nexthop — fib_select_multipath()
4.1 函数实现
定义在 net/ipv4/fib_semantics.c:
1 | /* net/ipv4/fib_semantics.c */ |
4.2 upper_bound 权重区间计算 — fib_rebalance()
fib_rebalance() 在路由变化时被调用,为每个 nexthop 计算 fib_nh_upper_bound,将 hash 值空间 [0, 2^31-1] 按权重比例划分:
1 | /* net/ipv4/fib_semantics.c */ |
4.3 图解:Hash 到 Nexthop 的映射
- 每个nexthop占据一个区间段,依次从低到高排列,占满真个hash空间。
- 每个nexthop记录一个
upper_bound,作为空间最大值。 最小值是前一个空间最大值+1 - 各 nexthop 的
upper_bound值由fib_rebalance()计算,公式为DIV_ROUND_CLOSEST_ULL((u64)w << 31, total) - 1:
等权场景:
3 条路径(weight 均为 1),hash 空间三等分:
| Nexthop | Weight | upper_bound | 公式 | 覆盖区间 | 占比 |
|---|---|---|---|---|---|
| NH0 | 1 | 715827882 | ≈ 2³¹/3 − 1 | [0, 715827882] |
33.3% |
| NH1 | 1 | 1431655764 | ≈ 2·2³¹/3 − 1 | (715827882, 1431655764] |
33.3% |
| NH2 | 1 | 2147483646 | = 2³¹ − 2 | (1431655764, 2147483646] |
33.3% |
加权场景:
3 条路径,weight 分别为 1:2:1(总权重 4)
各 nexthop 的 upper_bound 值(总权重 4):
| Nexthop | Weight | upper_bound | 公式 | 覆盖区间 | 占比 |
|---|---|---|---|---|---|
| NH0 | 1 | 536870911 | ≈ 2³¹/4 − 1 | [0, 536870911] |
25% |
| NH1 | 2 | 1610612735 | ≈ 3·2³¹/4 − 1 | (536870911, 1610612735] |
50% |
| NH2 | 1 | 2147483646 | = 2³¹ − 2 | (1610612735, 2147483646] |
25% |
4.4 根据源地址优选路由出口(v6.16 新增,commit 32607a332cfe)
v6.16 中 fib_select_multipath 引入了 根据源地址优选路由出口 的机制,不再是简单的 hash <= upper_bound 即返回:
| 条件 | 得分 |
|---|---|
nh_saddr == saddr(源地址匹配) |
+2 |
hash <= nh_upper_bound(hash 区间匹配) |
+1 |
优先级逻辑:
- 满分 3 分(源地址 + hash 都匹配):立即返回,最佳选择
- 2 分(源地址匹配但 hash 不在区间):优先于纯 hash 匹配
- 1 分(hash 匹配但源地址不匹配):如果没有 saddr 约束,也立即返回
解决的问题:TCP
connect中tcp_v4_connect会做 3 次路由查询(选源地址、确定路由、选端口后再查),每次fib_select_multipath可能因 hash 不同选中不同 nexthop,导致数据包的源 IP 属于 veth0 但从 veth1 发出。在双 ISP 场景下,运营商路由器会因源地址不属于该链路而丢包。该机制确保后续查询优先选中与已选源地址匹配的 nexthop,即根据源地址优选路由出口。
Commit 32607a332cfe 详解
Commit Message 原文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 ipv4: prefer multipath nexthop that matches source address
With multipath routes, try to ensure that packets leave on the device
that is associated with the source address.
Avoid the following tcpdump example:
veth0 Out IP 10.1.0.2.38640 > 10.2.0.3.8000: Flags [S]
veth1 Out IP 10.1.0.2.38648 > 10.2.0.3.8000: Flags [S]
Which can happen easily with the most straightforward setup:
ip addr add 10.0.0.1/24 dev veth0
ip addr add 10.1.0.1/24 dev veth1
ip route add 10.2.0.3 nexthop via 10.0.0.2 dev veth0 \
nexthop via 10.1.0.2 dev veth1
This is apparently considered WAI, based on the comment in
ip_route_output_key_hash_rcu:
* 2. Moreover, we are allowed to send packets with saddr
* of another iface. --ANK
It may be ok for some uses of multipath, but not all. For instance,
when using two ISPs, a router may drop packets with unknown source.
The behavior occurs because tcp_v4_connect makes three route
lookups when establishing a connection:
1. ip_route_connect calls to select a source address, with saddr zero.
2. ip_route_connect calls again now that saddr and daddr are known.
3. ip_route_newports calls again after a source port is also chosen.
With a route with multiple nexthops, each lookup may make a different
choice depending on available entropy to fib_select_multipath. So it
is possible for 1 to select the saddr from the first entry, but 3 to
select the second entry. Leading to the above situation.
Address this by preferring a match that matches the flowi4 saddr. This
will make 2 and 3 make the same choice as 1. Continue to update the
backup choice until a choice that matches saddr is found.
Do this in fib_select_multipath itself, rather than passing an fl4_oif
constraint, to avoid changing non-multipath route selection. Commit
e6b45241c57a ("ipv4: reset flowi parameters on route connect") shows
how that may cause regressions.
Also read ipv4.sysctl_fib_multipath_use_neigh only once. No need to
refresh in the loop.
This does not happen in IPv6, which performs only one lookup.
Signed-off-by: Willem de Bruijn <willemb@google.com>
翻译
标题:ipv4:优先选择与源地址匹配的多路径 nexthop
使用多路径路由时,尽量确保数据包从与其源地址关联的设备发出。
问题现象:tcpdump 中可能出现源 IP 属于 veth0 但数据包从 veth1 发出的情况(如上面的抓包示例)。
根据 ip_route_output_key_hash_rcu 中的注释,这在以前被认为是 WAI(按预期工作):”我们允许使用另一个接口的 saddr 发包”。但对于双 ISP 等场景,运营商路由器会因源地址不属于该链路而丢包。
根因:tcp_v4_connect 建立连接时会做 三次路由查询:
ip_route_connect首次调用——选择源地址(此时 saddr 为零)ip_route_connect再次调用——saddr 和 daddr 已确定ip_route_newports调用——源端口也已选定后再查一次
由于每次查询传入 fib_select_multipath 的熵(hash 输入)不同,三次查询可能选中不同的 nexthop。例如第 1 次选中了第一条路径的源地址,但第 3 次选中了第二条路径,导致源 IP 和出口设备不匹配。
解决方案:在 fib_select_multipath 中优先选择与 flowi4.saddr 匹配的 nexthop,使第 2、3 次查询与第 1 次保持一致。在匹配到 saddr 之前,持续更新备选 nexthop。
实现细节:
- 在
fib_select_multipath自身中实现,而非传入fl4_oif约束,以避免影响非多路径的路由选择(commit e6b45241c57a 表明那样做会导致回归问题) - 同时优化了
sysctl_fib_multipath_use_neigh的读取,在循环外只读一次 - IPv6 不存在此问题,因为 IPv6 只做一次路由查询
五、被调用场景与调用栈详解 (Callers & Call Stack Details)
ECMP 选路存在 两条主要路径 和 两条辅助路径,最终都汇聚到 hash 计算 + nexthop 选择:
| 路径 | 典型场景 | 入口函数 | 备注 |
|---|---|---|---|
| 本地发包 | tcp_v4_connect() / udp_sendmsg() |
fib_select_path() |
经 ip_route_output_key_hash_rcu() 触发 |
| 转发 | ip_rcv() → ip_forward() |
ip_mkroute_input() |
直接调用 fib_multipath_hash() + fib_select_multipath(),不经过 fib_select_path() |
| ICMP Redirect | 收到重定向报文 | fib_select_path() |
辅助路径 |
| PMTU 更新 | 路径 MTU 变化 | fib_select_path() |
辅助路径 |
6.1 路径一:本地发包(Output Path)
典型场景:tcp_v4_connect()、udp_sendmsg() 等本地发包时查找出站路由。
1 | tcp_v4_connect() / udp_sendmsg() / ... |
tcp_v4_connect 三次路由查询详解详见 tcp_v4_connect 三次路由查询详解。
6.2 路径二:转发路径(Forward / Input Path)
典型场景:收到需要转发的数据包。
1 | ip_rcv() // net/ipv4/ip_input.c |
注意:转发路径中
ip_mkroute_input()直接调用fib_multipath_hash()+fib_select_multipath(),不经过fib_select_path()。因为转发时不需要源地址选择逻辑。
6.3 路径三:ICMP Redirect 处理
略过
6.4 路径四:PMTU 更新
略过
六、关键 sysctl 参数 (Key sysctl Parameters)
| Sysctl | 默认值 | 作用 |
|---|---|---|
net.ipv4.fib_multipath_hash_policy |
0 | hash 策略:0=L3, 1=L4, 2=inner L3, 3=custom |
net.ipv4.fib_multipath_hash_fields |
默认掩码 | policy=3 时自定义参与 hash 的字段 |
net.ipv4.fib_multipath_hash_seed |
随机 | hash seed,防止多跳极化 |
net.ipv4.fib_multipath_use_neigh |
0 | 是否检查邻居可达性跳过故障路径 |
net.ipv4.conf.*.ignore_routes_with_linkdown |
0 | linkdown 的 nexthop 是否设为 upper_bound=-1 |
七、实践建议 (Practical Recommendations)
双线/多 ISP 场景
1 | # 使用 L4 五元组 hash,让不同连接均匀分散 |