跳到主要内容

6.8 多路径路由

策略路由给了我们选择不同路线的自由,但它解决的是「根据目的地以外的条件(比如源地址)来选路」的问题。

这里有一个更微妙的需求:假设你是一个网络管理员,你手里有两条通往互联网的宽带,电信和联通,带宽相当。你不想让其中一条闲置,也不想手动配置一半电脑走电信、另一半走联通。你想要的很简单:把流量摊开

或者,你是一个服务器管理员,你有两块网卡接在同一个交换机上,单纯想通过「双管齐下」来跑满 1Gbps 的带宽上限。

这就是多路径路由要解决的问题。


什么是多路径路由

从内核的角度看,这听起来有点反直觉。我们在前面的章节里一直强调,路由查找是「精确匹配」——给你一个目的地,你给我一个确定的下一跳。如果下一跳有两个,那算谁?

所有

多路径路由允许你为一个路由条目配置多个下一跳。你可以像这样说:

「要去 192.168.1.10 的数据包,你可以走 192.168.2.1,也可以走 192.168.2.10。看着办,只要把它们分摊开就行。」

在命令行上,这长这样:

# 简单的多路径:两条路平分秋色
ip route add default scope global nexthop dev eth0 nexthop dev eth1

# 加权的多路径:第二条路承载更多流量
ip route add 192.168.1.10 nexthop via 192.168.2.1 weight 3 \
nexthop via 192.168.2.10 weight 5

你可以把每一条 nexthop 想象成一条车道。上面第二条命令的意思是:我有 8 个单位的车流量,3 个单位走第一条车道,5 个单位走第二条车道。

但内核不是真的「看着办」,它需要一套精确的算法来决定每一个具体的数据包该走哪条路。这套机制就藏在 fib_info 里。


内核的表示:fib_info 与 fib_nh

在 IPv4 的 FIB(转发信息库)子系统里,fib_info 结构体是路由信息的核心载体。我们在前面讲过,普通的路由条目有一个 fib_nh(FIB Nexthop),但在多路径场景下,这个字段变成了一个数组。

这里有一个微妙的区别:

  • 单路径fib_info 指向一个单一的 fib_nh 结构。
  • 多路径fib_info 包含一个 fib_nh 数组,数组的长度由 fib_nhs 成员指定。

当你执行上面那条带两个 nexthopip route add 命令时,内核会创建一个 fib_info 对象,里面的 fib_nhs 被设为 2,fib_nh 数组里塞进了两个 fib_nh 对象。

这时候,每个 fib_nh 对象里还有一个关键字段:nh_weight

这个字段就是我们刚才提到的「权重」。如果你在 ip route 命令里显式写了 weight 3,内核就把它填进去;如果你没写,fib_create_info() 方法会默认把它设为 1。


谁来做决定:fib_select_multipath()

路由表决定了「有哪些路可选」,但数据包到达时,必须从中挑出一条路来走。这个做决定的黑盒叫做 fib_select_multipath()

这个函数在两个关键的地方被调用:

  1. 发送路径:在 __ip_route_output_key() 里。这是本地产生的数据包准备出门时经过的流程。
  2. 接收路径:在 ip_mkroute_input() 里。这是转发数据包时经过的流程。

但这里有一个判断条件。在发送路径的代码里,你会看到这段逻辑:

struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {
...
#ifdef CONFIG_IP_ROUTE_MULTIPATH
// 只有当存在多条路径,并且没有强制指定输出设备时,才做选择
if (res.fi->fib_nhs > 1 && fl4->flowi4_oif == 0)
fib_select_multipath(&res);
else
#endif
...
}

为什么有个 flowi4_oif == 0 的检查?

因为应用程序在发送数据时,可以通过 bind() 或者 sendmsg() 的辅助消息明确指定「必须从 eth0 出去」。一旦用户做出了这种硬性规定,多路径负载均衡就没有意义了——哪怕另一条路再空,你也得走这一条。这种情况下,内核会跳过 fib_select_multipath(),直接走用户指定的那条路。

在接收路径里,通常没有这种限制,因为转发过来的数据包本身并不携带「请从 eth1 出去」这种预设。

选择算法

fib_select_multipath() 的内部实现其实挺有意思,它不像我们直觉以为的那样简单地「轮流坐庄」(1, 2, 1, 2...)。它引入了随机性

这种随机性并不是为了不可预测,而是为了加权公平

它利用了系统时间(jiffies)作为哈希计算的种子。对于每一个到来的数据包,内核会计算一个哈希值(基于源 IP、目的 IP、源端口、目的端口等),结合每条路径的 nh_weight,算出这个数据包应该去往哪一个 fib_nh

这个算法的设计目标是:

  • 同一条流(五元组相同)的数据包,应该走同一条路。否则,TCP 乱序会导致性能急剧下降。
  • 不同的流,能够按照权重比例分配到不同的路径上。

最终选定的路径索引,会被保存到 fib_result 结构体的 nh_sel(Nexthop Selector)字段里,后续的转发逻辑就根据这个索引去查 fib_nh 数组。


代码的归宿

如果你去翻源码,想找一个叫 multipath.c 的文件,你会失望。

在组播路由那里,内核专门写了一个 net/ipv4/ipmr.c 模块,清清爽爽。但在多路径路由这里,代码就像是化作了「幽灵」,散落在通用路由代码的各个角落,被大量的 #ifdef CONFIG_IP_ROUTE_MULTIPATH 包裹着。

这说明多路径路由在内核设计者眼里,不是一个独立的子系统,而是路由查找逻辑的一个增强特性

这种做法有一个历史遗留的副作用。如果你去翻老内核的代码(2007 年以前),你会发现 IPv4 曾经有一个专门的「多路径路由缓存」。这个缓存在 2.6.23 内核中被移除了。

这里千万不要混淆:多路径路由缓存的移除路由缓存的移除

  • 多路径缓存是在 2007 年移除的,因为它作为实验性功能,一直没能工作得很好。
  • 真正的路由缓存是 2012 年在内核 3.6 中移除的,那是为了解决多核 CPU 下的缓存同步开销问题。

现在的多路径实现,是在 FIB 查找阶段直接完成的,没有额外的缓存层,这反而让逻辑更清晰了。


配置开关:CONFIG_IP_ROUTE_MULTIPATH

最后,记得检查你的内核配置。

要让上面这一切跑起来,你的内核配置里必须有 CONFIG_IP_ROUTE_MULTIPATH=y。很多发行版为了精简内核,可能会把这个功能关掉或者做成模块。如果你发现加了 nexthop 命令不生效,先去 make menuconfig 里翻翻看。