connect() - 为什么这么慢?

💡 原文英文,约2900词,阅读约需11分钟。
📝

内容提要

Cloudflare 鼓励从 IPv4 向 IPv6 地址过渡。他们的缓存服务消耗了许多 IPv4 地址,导致延迟。他们提出了三种解决方案:“选择、测试、重复”,“通过随机移动范围选择端口”,以及 IP_LOCAL_PORT_RANGE 套接字选项。还提到了其他协议的端口选择算法。

🔎

延伸解读

端口选择:性能瓶颈的根源

文章通过火焰图和合成测试发现,TCP连接建立缓慢的主要原因是端口选择算法。Linux内核在__inet_hash_connect()中采用奇偶端口分半扫描:先遍历所有偶数端口,再遍历奇数端口。这导致奇数端口连接显著慢于偶数端口,因为偶数端口耗尽后才使用奇数端口。这种设计原本是为了避免connect()和bind()工作负载的冲突,但在高并发connect()场景下反而成为性能瓶颈。

用户空间解决方案的权衡

针对内核低于6.8的系统,文章提出两种用户空间方案:一是“选择、测试、重复”,随机选端口并尝试连接,冲突时重试,适合端口利用率70-80%的场景,但冲突时增加系统调用开销;二是利用IP_LOCAL_PORT_RANGE套接字选项,在系统端口范围内随机偏移一个窗口,让内核在窗口内选择端口。测试表明窗口大小500或1000时性能较好,窗口越大错误率越高,因为随机偏移机会减少。

内核6.8的改进与协议差异

内核6.8引入新补丁,设置IP_LOCAL_PORT_RANGE为完整系统端口范围即可激活优化,无需手动窗口偏移。该方案随机化起始偏移的奇偶性,然后增量扫描,性能与用户空间方案相当且略快。其他协议中,DCCP使用与TCP相同的端口选择算法,因此受益于内核改进;UDP则采用不同算法,没有奇偶端口分割,但存在SO_REUSEADDR导致的端口冲突风险,需要额外注意。

❓

Q&A

Cloudflare 为什么需要减少 IPv4 地址的使用?

Cloudflare 鼓励企业从 IPv4 过渡到 IPv6,以减少 IPv4 地址消耗。由于 IPv4 地址成本上升,他们更倾向于重新分配地址而非购买更多。他们的缓存服务是 IPv4 地址的主要消耗者之一,因此需要优化以减少使用。

Cloudflare 的缓存服务在什么情况下会建立大量 TCP 连接?

当非常流行的源站有未缓存的资产,且许多互联网用户同时尝试访问时,缓存未命中会导致 Cloudflare 向单个目标建立超过 50,000 个 TCP 单播连接。

为什么使用一个 IPv4 地址比两个 IPv4 地址的 connect() 性能更差?

测试显示,使用一个 IPv4 地址时,超过一半的连接处于慢速情况,而两个 IPv4 地址时慢速连接较少。性能瓶颈主要在于端口选择算法,尤其是奇偶端口的分配:奇数端口的连接速度显著慢于偶数端口。

TCP 端口选择算法中奇偶端口分裂的原因是什么?

奇偶端口分裂是为了避免 connect() 和 bind() 密集型工作负载之间的冲突。偶数偏移用于 connect() 工作负载,奇数偏移用于 bind() 工作负载。这种分裂在 connect() 工作负载不超过端口范围一半时工作良好。

对于内核版本低于 6.8 的系统,有哪些用户空间解决方案可以优化端口选择?

有两种用户空间解决方案:一是“选择、测试、重复”,即随机选择端口并测试连接,直到成功;二是“通过随机移动范围选择端口”,利用 IP_LOCAL_PORT_RANGE 套接字选项,在系统端口范围内随机偏移一个自定义窗口,减少搜索空间。

内核 6.8 及以上版本如何改进端口选择?

内核 6.8 引入了一个新补丁,消除了窗口移动的需要。只需将完整的系统端口范围传递给 IP_LOCAL_PORT_RANGE 套接字选项,内核就会随机化起始偏移为奇数或偶数,然后增量循环,而不是跳过每个其他端口。这简化了端口选择过程并提高了性能。

UDP 的端口选择算法与 TCP 有何不同?

UDP 使用不同的算法(udp_lib_get_port()),它增量循环整个端口范围,但使用随机数作为步长变量,依赖 uint16_t 溢出循环回选定的端口。没有奇偶端口分裂。此外,使用 SO_REUSEADDR 时,端口可能冲突,因为 udp_lib_lport_inuse() 忽略 UDP 2 元组检查。

🏷️

标签

➡️

继续阅读