如何在 Cloudnet 上提升 QUIC 和 UDP 的传输效率
大家好,我们来聊聊性能优化了。记得之前我们如何提升了 TCP 传输效率吗?现在,我们有了新动作——我们显著提高了 Linux 系统上 UDP 传输的速度。就像以前一样,我们计划将这些改进贡献给 WireGuard 社区。
UDP 协议是一种相对简单的传输方式,它不像 TCP 那样会确认数据包是否成功送达。因此,UDP 特别适合那些对即时响应有严格要求的应用,比如在线游戏或视频会议。而最近,随着新兴协议 HTTP/3 和 QUIC 的崛起,UDP 的使用率激增。
我们通过使用一种技术叫做分段卸载,提高了基于 HTTP/3、QUIC 等 UDP 协议的应用的传输效率。在标准的 Linux 系统上,我们使得 Cloudnet 的 UDP 传输速度提高了四倍,甚至超过了同一硬件上的内核级 WireGuard 实现。
想要体验这些?你可以在 Cloudnet v1.54 中尝试。继续阅读,我们将分享更多细节;如果你只对结果感兴趣,也可以直接跳到成果部分。
背景
接下来,我们会探讨 wireguard-go——Cloudnet 数据传输的核心。它通过 TUN 设备接收操作系统的数据包,对它们进行加密,然后通过 UDP 协议发送到另一端。返回的数据包也通过相同的路径解密后返回到操作系统。
我们先前的改进主要集中在提升每次 I/O 操作中传输的数据包数量。不管是 TCP 还是 UDP,我们都采用了一些技术来提高数据包处理的效率。但这些技术对于 TCP 更加有效,因为 UDP 流量在我们的 wireguard-go 实现中几乎没有得到改善。考虑到越来越多的应用开始采用 HTTP/3 和 QUIC,我们现在把重点放在了提升 UDP 的性能上。
我们在 Cloudnet v1.36 和 v1.40 中做的改变更新了这个数据包管道,大大增加了 wireguard-go 上的 TCP 吞吐量。在这两种情况下,我们都专注于增加每个 I/O 操作端到端传输的数据包数量。在 TUN 驱动端,这涉及到 TCP 分段卸载(TSO)和通用接收卸载(GRO)。在 UDP 套接字端,我们利用了 UDP 通用分段卸载(UDP GSO)和 UDP 通用接收卸载(UDP GRO)。分段和接收卸载都使多个数据包能够作为一个单一元素通过堆栈传递。分段卸载涉及在最接近传输边界的地方将单个“怪兽”数据包分段,这是要写入自然大小的数据包的地方。接收卸载涉及将多个数据包合并成一个“怪兽”数据包,这是最接近接收边界的地方,预期在这里读取自然大小的数据包。
在这些卸载被用于 UDP 的地方,UDP 是作为底层协议的。我们在 TUN 端实现的卸载是针对 TCP 的,并不适用于 UDP 覆盖流量。这导致 UDP 流在 wireguard-go 上几乎没有什么好处。TCP 一直是高吞吐量应用的传输协议的首选,所以最初专注于 TCP 吞吐量是有道理的。然而,随着 HTTP/3 和 QUIC 的出现,这种情况正在开始改变。
HTTP/3 和 QUIC
HTTP/3 是 HTTP/2 的继任者,它使用 QUIC,这是一个相对较新的基于 UDP 的多路复用传输协议。
QUIC 有许多优于 TCP 的优点,包括但不限于:
- 紧密集成 TLS,使其不易受到中间盒子干扰或依赖传输层元数据的影响
- 更快的连接握手(假设不需要 HTTP/2 over TCP 来引导)
- 对头阻塞的抵抗力更强;流感知从传输协议延伸到 HTTP/3
- 使拥塞控制快速演化成为可能,因为它存在于用户空间
- 全球约有 27% 的网络和服务器已经支持 HTTP/3。
所以,HTTP/3 和 QUIC 的采用正在增加,我们需要扩展我们的性能工作以使其受益。
基线
关于基准测试的免责声明:这篇文章包含基准测试!这些基准测试在写作时是可重现的,我们提供了我们运行它们的环境的详细信息。但是基准测试结果在不同的环境中会有所不同,而且随着时间的推移,它们也往往会过时。你的里程可能会有所不同。
我们需要设置一个 UDP 吞吐量基线以供后续比较。在我们之前的文章中,我们使用 iperf3 进行了 TCP 基准测试,但在写作时,iperf3 不支持 UDP GSO/GRO。没有这个支持,它不会反映出与广泛使用的 QUIC 实现相比的实际性能。所以,我们将使用 secnetperf,这是 msquic 的一个实用程序,来代替。引用 msquic 的 README:
MsQuic 是微软对 IETF QUIC 协议的实现。它是跨平台的,用 C 写的,设计成一个通用的 QUIC 库。MsQuic 还有 C++ API 包装类,并为 Rust 和 C# 暴露了互操作层。
msquic 的一位维护者,Nick Banks,在 IETF 内部工作,并提出了一个 QUIC 性能协议,用于测试 QUIC 实现的性能特性。secnetperf 实现了这个协议。
使用 secnetperf,我们为 wireguard-go@2e0774f 和内核 WireGuard 在两对主机之间的 QUIC 吞吐量进行了基线测试,这两对主机都运行着 Ubuntu 22.04,使用的是写作时可用的 LTS 硬件启用内核:
2 x AWS c6i.8xlarge 实例类型 2 x “裸机”服务器,由 i5-12400 CPU 和 Mellanox MCX512A-ACAT NICs 提供动力 AWS 实例位于同一区域和可用区:
在我们之前的文章中,我们分析了火焰图(),这些图突出显示了通过内核网络堆栈和 wireguard-go 可以提高 CPU 周期/字节效率的地方。这个分析的结果导致我们在 wireguard-go 的两端实现了传输层卸载,这提高了覆盖网络上的 TCP 流量的吞吐量。现在,我们需要在这项工作的基础上,同样使 UDP 流量在 wireguard-go 上受益。进入 tx-udp-segmentation。
NETIF_F_GSO_UDP_L4 是 Linux 内核中用于在代码中定义它的符号。引用内核文档 NETIF_F_GSO_UDP_L4 接受一个超过 gso_size 的 UDP 头和负载。在分段时,它在 gso_size 边界上分段负载,并复制网络和 UDP 头(如果小于 gso_size,则修复最后一个)。
这个 netdev 特性在 Linux v4.18 中被添加,最近在 Linux v6.2 中被添加为一个可以在 TUN 驱动中切换的特性。TUN 驱动在 v6.2 中的支持是提高 UDP 吞吐量所需的缺失的部分。开启它后,wireguard-go 可以从内核接收“怪兽” UDP 数据报:
反向方向的工作方式类似。它不需要一个显式的 netdev 特性来支持 UDP GRO,而是简单地依赖于相同的 virtio 网络基础设施来支持合并。
现在,来看看总体结果。
应用 TUN UDP GSO/GRO 导致 wireguard-go 的吞吐量大幅提高,因此也在 Cloudnet 客户端中提高。
有了这一新的改变集,Cloudnet 上的 UDP 吞吐量在裸机 Linux 上增加了 4 倍,并超过了该硬件上的内核 WireGuard 实现。
AWS c6i.8xlarge 实例在约 7Gb/s 的地方遇到了一个墙,这似乎是底层网络的人为限制。
rx-udp-gro-forwarding 和 rx-gro-list
关于 UDP 吞吐量在转发拓扑中的两个 Linux 内核网络设备特性很重要,即数据包从一个接口进入并从另一个接口离开。
第一个是 rx-udp-gro-forwarding,引用其来自 Linux 内核的注释:
如果在接收接口上没有启用 rx-udp-gro-forwarding,那么被转发的 UDP 数据包,即不是目标为本地套接字的数据包,将不会是合并的候选者。这限制了 GRO 在堆栈的其余部分的效果,降低了吞吐量。最初,对于转发的数据包,默认启用了 UDP GRO,这是在这个特性存在之前。这是无意的,如引入该特性的内核提交所提到的。
我们建议在你的默认路由接口上启用 rx-udp-gro-forwarding,如果你正在运行 Cloudnet 版本 1.54 或更高版本作为子网路由器或出口节点,并且使用的是 Linux 6.2 或更新的版本.
最后
我们还介绍了一些技术细节,包括如何在 Linux 系统上启用特定的网络设备特性来进一步提升 UDP 性能。我们的工作使得 Cloudnet 上的 UDP 传输速度得到了大幅提高,尤其是在不使用虚拟化环境的 Linux 系统上。
希望以上内容能帮助你了解我们是如何优化 Cloudnet 上的 UDP 传输效率的。如果你想要了解更多的技术细节或参与到我们的性能提升工作中,请继续关注我们的更新。