谈谈 Linux 与 LACP 链路聚合

简单举例讲一下 Linux 是如何拆东墙补西墙,造成用户困扰的。

背景

工作后这几年,我的家用网络设备的性能一年比一年强。从当年的单口千兆 ARM 开发板和普通无线网关,后来的 6 口千兆网卡 Intel 软路由,到现在的 4 口万兆网卡 AMD Ryzen 路由器和双网口万兆 QNAP NAS,内网吞吐性能始终保持着每年几倍的提升。近来我为了能够更好地利用 NAS 上的 2TB SSD cache,将 NAS 到主路由的链路从原来的单条 10 Gbps 升级到两条 10 Gbps,使用 LACP 协议进行链路聚合,并且两端的 Linux bond driver 均采用 layer 3+4 的 hash 算法,以实现不同连接自动分配到两条 10 Gbps 链路。

理想情况下,现在我使用 iperf 测试主路由与 NAS 之间的链路速度,只需要使用两个连接(参数 -P2)即可实现接近 20 Gbps 的双向吞吐。但实际操作时,却发现从主路由到 NAS 这个方向的两个连接可以实现 20 Gbps 的吞吐,但是从 NAS 到主路由的方向无论在 iperf 参数里指定多少个连接,都只会走其中一个链路。而使用我的 Windows PC 与 NAS 建立两个连接进行测速,从 NAS 端发送数据时,却可以将两个连接分别分配到两个 10 Gbps 链路上。

回想一年前我还在使用千兆链路做 LACP 聚合时,也曾遇到过类似的问题。iperf 多连接测速时,从 Linux 发送数据到 MikroTik 交换机时,多个连接可以分配到所有的链路上;但是从 MikroTik 交换机到 Linux ,却只会使用其中一半的链路。当时以为是 MikroTik 交换机的 bug,于是没有深究具体的原因,但是这次出现问题的发送端是基于 Linux 内核的 QNAP NAS,于是我决定调查一下具体的原因。

分析

首先整理所有机器的软件与硬件信息,以及网络拓扑。

  • 主路由,操作系统是 Proxmox VE 6.0,内核基于 Linux 5.4 修改。安装 Intel XL710 四口万兆网卡,其中前两个端口使用 bond driver 组成 LACP 聚合端口(layer3+4 hash 算法),并和第三个端口加入同一个 Linux 桥。
  • NAS,操作系统是 QNAP QTS 4.4.1,内核基于 Linux 4.2.8 修改。NAS 上的两个万兆端口组成 LACP 聚合端口(layer3+4 hash 算法),连接到主路由。
  • Windows PC,操作系统是 Windows 10 1909。使用 Intel XL710 双口万兆网卡,其中第一个网口使用光纤连接到主路由的第三个万兆网口

根据前面观察到的现象,NAS 发包与主路由发包时的行为产生了明显的差异,但是主路由与 NAS 的 LACP 相关配置是完全相同的,本来不应该有任何差异。于是我推断是 Linux 内核在 4.2.8 与 5.4 版本之间的某个版本里修改了 layer3+4 hash 算法的实现。

因此,我们可以开始分析 Linux 的 bond driver 是如何将发送端的数据包分配到多个链路的。阅读 bond_main.c 文件,我们可以将问题定位到 bond_xmit_hash 这个函数,在 v5.4 与 v4.3 两个 tag下,分别位于 3300 行3134 行。经过对比,我们可以发现新版的 Linux 内核里,bond_xmit_hash 函数在返回 hash 时,丢弃了 hash 的最后一位,而旧版本 Linux 是直接返回这个 hash。

使用 git blame 功能,我们将这个改动定位到具体的 commit。这个改动于 2017 年进入主线 Linux,存在于 Linux 4.14 以及所有之后的版本。阅读 commit 描述,我们可以发现,这个改动是为了 workaround commit 07f4c90 对 LACP 产生的影响。后者于 2015 年进入主线 Linux,存在于 Linux 4.2 以及所有之后的版本。

整理分析上述调查的结果,我们可以得出以下结论

  • Linux 4.2 之前,应用程序调用 connect() 建立 TCP 连接时,Linux 会顺序分配端口号。因此,在连接较多时,可能会造成 bind(0) 调用失败,或因为扫描可用端口而造成比较严重的性能问题。
  • 在 Linux 4.2 里,为了解决上述问题,Google 的开发人员修改了端口分配逻辑,在应用程序调用 connect() 建立 TCP 连接时,偏向于分配一个偶数的端口。但这个改动造成了链路聚合的 layer3+4 hash 算法将所有的数据包分配到半数的链路。这是因为 hash 最后一位永远是 0。
  • 两年后,在 Linux 4.14 里,为了解决 Linux 4.2 产生的问题,Linux 开发人员修改了链路聚合的 hash 算法,丢弃最后一个 bit,如此一来,由 Linux 发起的两个连续创建的连接,由 Linux 的 bond driver 发送时,就能正确地被分配到不同的链路上。

再来结合我的实际环境分析问题。

  • NAS 与路由器两端的 Linux 版本均高于 4.2,因此使用 iperf 建立连接时,极大概率会被分配到偶数端口。
  • NAS 的 Linux 版本低于 4.14,发送数据包时,由于 layer3+4 hash 的最后一位永远是 0,因此会被全部分配到同一个 10 Gbps 端口。
  • 主路由的 Linux 版本高于 4.14,发送数据包时,由于 layer3+4 hash 的最后一位被丢弃,因此能够正常分配到多个 10 Gbps 端口。
  • Windows 建立 TCP 连接时不会偏好偶数端口,因此由 Windows 连续建立多个连接,NAS 发送数据包时可以正常分配到多个 10 Gbps 端口。但是由 Windows 连续建立两个连接发送数据,经由主路由传送到 NAS 时,端口号唯一的差异是最后一位,而这一位遭丢弃,于是需要连续建立至少3个连接,才能分配到两个链路上。
  • 分析去年 MikroTik 交换机仅能使用半数端口作为链路聚合使用的现象,其实并不是 MikroTik 的 bug,而是 Linux 的端口全是偶数,导致 Marvell 交换机 SoC 硬件实现的 layer2+3+4 hash 算法将这些数据包分配到半数的链路上。Marvell 交换机 SoC 并没有像新版 Linux 一样丢弃 hash 的最后一个 bit。

至此,我们已经完成对近两年来一直在困扰我的 Linux 与 LACP 的问题的分析,并且解释了其中涉及到的所有软件与硬件的表现行为。

解决方案

要么忍,要么滚。你对 Linux 有什么不满,就应该去加入它并且努力尝试改变它,而不是在这里瞎逼逼。

——热心群众

虽然还没有正式地开始执行,但是我目前想到了如下的解决方案。

  • 不使用任何 Linux 4.2 之后的 Linux 版本,以及 Red Hat 维护的 3.10 内核或更新的版本。并且说服客户也不要使用,这一点很重要,因为需要 LACP 链路聚合的通常是服务端,而服务端的端口号通常是固定的(如 80/443),数据包走哪些物理端口取决于客户端选择了怎样的端口号。
  • 仅使用基于 Linux 4.14 或更新版本的操作系统,或采用相同算法的操作系统和交换机芯片。抛弃诸如 Windows 的其它操作系统,以及基于 Marvell SoC 的交换机硬件。
  • 抛弃 LACP,升级到单条 25/40/100/200 Gbps 链路。

遗憾的是,对于我来说这些方案都是不可行的,因此我也只能暂时忍一忍了。

结语

Linux 这些改变对我造成了相当严重的困扰,而且持续多年,期间我更换了许多网络硬件,然而这个问题依然存在,直到我有时间阅读内核源码找出背后的原因。

起初,Linux 为了解决一个性能问题,引入了一个简单粗暴的解决方案,却因此改变了应用程序的行为,对另一个组件的性能产生了严重影响。后来为了修复这个新产生的性能问题,又去改变另一个组件的行为,却因此导致与其它硬件或操作系统的互操作性受到影响,而到这一步,Linux 开发者终于成功地将问题扩散至 Linux 之外,扩散到了他们无法修复的地方,例如成千上万的旧版本设备,大量的交换机硬件,以及其它的操作系统,比如 Windows。而 Linux 本身却尚未完美地解决这个问题。

这无疑是一个现代软件工程的反面教材。为了一个可有可无的优化,去改变应用程序的行为,后来发现这个行为变化涉及到远远超过自己想象的各个方面,导致随后又不得不拆东墙补西墙去到处修补这些问题。而这些修补又产生了新的行为改变,最终扩散到自己的控制范围之外,永远无法完美修补,从而导致全世界所有人都无法完美使用这一功能,造成用户的困扰,甚至导致用户不得不去阅读代码来证明自己的猜想。

实属滑稽。

谈谈 Linux 与 LACP 链路聚合》有4个想法

  1. czx

    LACP是什么东西?只听说过ECMP(如果要提高网络带宽,也许可以通过multi-path路由聚合多条光纤来实现),及考虑把云端的负载均衡技术(HAProxy、Intel的支持大并发连接的DPDK、还有k8s里面的基于BPF的Cillium)应用到家庭网络中来……

    回复

czx进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注