TCP上的多路复用,成为了『减速器』

warning: 这篇文章距离上次修改已过204天,其中的内容可能已经有所变动。

这个问题可以拆分成很多个问法不同但结果一致的小问题,比如为什么这两年上网(尤其是代理上网)速度感觉变慢了,为什么SSH代理被抛弃了,以及为什么HTTP2会帮倒忙

这是和友人讨论之后得出来的结论,总结就是,TCP HoL Blocking(队头阻塞)是原罪

19216是笔者在使用的加密代理端口19216是笔者在使用的加密代理端口

1. 前言

如果要恰当地形容这个问题,我会认为他是同一问题,在不同年代和不同协议栈上的多次重演。引言中提到可以拆分的小问题,更具体来说,可以有如下情况:

  • 为什么几年前科学上网快,现在慢
  • 为什么SSH做代理,感官上很慢,『卡成狗』
  • 为什么代理工具的Mux功能『测速有反效果』
  • 为什么OpenVPN TCP很慢
  • 为什么需要有HTTP/3

笔者不是 TCP 专家,所做的研究也仅仅浅尝辄止,这里记录一下研究成果。如读者有进一步学习的需求,可自行查阅其他资料。

2. TCP的特性

开始任何讨论之前,需要明白一点:今年是2025,而TCP诞生于1981年,最早是在 RFC 793 中做出规定,因此关于他的很多东西都可以认为是『落后于时代』的,这是一种必然性。此外,笔者习惯把 TCP segment 称呼为『数据包』,因此下面提到TCP数据包,基本上可以认为指的就是 TCP segment 。

简单说来,TCP 作为一个可靠流传输协议,特性之一就是,某个数据包丢了就要重传,这样才能实现可靠交付。但考虑这么一个情况:一串连续数据包,在传输的时候中间丢了一个(后面的包都成功送达),那么在重传的时候怎么办呢?TCP 给出的答案是:在缓冲区先等着,等那个丢掉的包重传成功了,再连同后面的包一起提交给上层,确保按顺序交付。一个不太准确的比方就是,排队,只有前面的人办完事了,后面的人才能跟上去,不能插队。

本来这挺好的,不然只管收到不管顺序,还要上层自己处理乱序问题,那还要你TCP有何用,不如干脆用UDP,自己加点重传算了。但是来到今天讨论的问题上,这个特性反而成为了缺点,也就是我们今天要讨论的队头阻塞

3. 多路复用引入的问题

现在我们开始考虑TCP之上的多路复用。

首先引入最简单的多路复用模型,也就是在一条实际连接上承载多条逻辑连接,各个逻辑连接之间通过各自的标识符区分,比如流ID,实现互不混淆。接着,我们假设有两台设备,A和B,他们之间建立了一条实际的TCP连接,运行多路复用程序,协商使用上面提及的多路复用协议,并建立了两条逻辑连接,ID分别为111222。现在A向B发送数据,比如说我们发送:

[111]Hello [222]NiHao [111]World! [222]Shijie!

TCP提交给上层之后,在B机上的多路复用程序看来,他可以把这个数据流拆分为两条:

[111] Hello World!
[222] NiHao Shijie!

只需要把这些数据包提交给所需的(不论是不是同一个)程序就行了,程序会根据需要处理信息。

本来一切都很美好,多路复用减少了多次建立TCP连接的开销,大家都很舒服。但问题是,公网是一个很骨感的环境,有丢包,有乱序,有重传,有分散丢包,有连续丢包。现在我们假设给上面的链路引入丢包,比如说实际B机器收到的TCP数据包是:

[111]Hello ...... [111]World [222]Shijie

TCP可以感知到中间丢了一个包,按照规则,他会等这个包重传成功了,再把后面的包提交给上层,也就是我们的多路复用程序。所以此时程序收到的包变成了:

[111]Hello  # 后面的在等重传

解开来就是:

[111] Hello
[222] # 没有数据了!

没错,此时两条流都受到了影响,也就是说,TCP的队头阻塞,会影响到后续数据,也就是影响到了绝大部分的逻辑连接。

而且,显然,假设上面两条逻辑连接拆分为两条实际的TCP连接,那么受到影响的风险就会被分散,比方说在随机丢包的情况下,连接111丢包,连接222没有丢,那么111不会影响222的运行,因此反而会显得更加舒服。

4. 问题的解答

现在我们可以解答第一小节提出的一堆问题了。

4.1 几年前上网快,现在慢?

今年是HTTP/2(下面简称H2)正式成为RFC7540的十周年,因此我们大致可以认为终端设备从那个时候开始逐渐支持H2。新标准的采用总是要点时间的,因此W3Techs的统计数据显示,直到2023年7月,世界上排名前1000万的网站中,有四到五成的网站开始支持H2。另一方面,根据Cloudflare统计,截止到今天,他们的CDN网络分发的H2流量已经达到了60%以上:

H2最大的特点就是支持多路复用,但很可惜,他的多路复用还是建立在TCP之上的,正好就是我们今天所讨论的场景。换句话说,一旦目标网站开始支持H2,浏览器就会使用H2进行连接,并顺带利用上这个多路复用特性,于是问题就出现了:对同一个源(Origin),浏览器只会建立1条TCP连接,剩下的全交给多路复用来处理。由于同一个子域名+同一个端口+同一个协议(非常常见的配置)被认为是同源的,换句话说,一旦底层TCP连接发生队头阻塞,这个源里的后续资源全部都会发生阻塞。典型症状是打开网站后白屏转圈,按F12看控制台,一大片资源被卡住无法加载,只能傻等。

这就解释了为什么是这几年。因为这几年,一个是国际链路(甚至近段时间的省间链路)开始显得拥堵/被QoS,导致丢包多;一个是H2开始被各大网站广泛支持,浏览器默认H2连接,也默认多路复用。两层因素加在一起,成就了这个神奇的局面。

解决这个问题的方案还是有一些的,比如服务器端启用BBR(套了CDN的话,得看CDN心情);把资源分散到不同的子域名(例如图片放img子域名),使其处于不同Origin,迫使浏览器新开TCP连接;甚至改用HTTP/3,等等。

4.2 SSH代理卡成狗?

十几年前吧,VPN日渐式微的时候,SSH代理曾经风光过一段时间。只需要一句简单的ssh -D,在对端没有明确限制的情况下,就可以把任何SSH服务器(即使只有低权限甚至无登录权限账号)变成一个代理服务器,还自带加密与鉴权。那时候是真的风光,各大论坛都在分享可用的SSH账号,淘宝也有人在出售SSH账号,真的是风光无限。

不过用多了SSH代理的人都会有同一个感觉,那就是,有时候一慢慢一片。直到13年前后SS横空出世,许多从SSH过来的人都觉得,SS的体验『丝滑到惊为天人』,于是毅然抛弃了SSH。关于为什么慢,当年的看法是『因为SSH经过了高强度加密和校验,所以很慢』,甚至还有『一辆卡车用20km/h的速度运送一瓶可口可乐』的比喻。甚至当人们看到初代SS采用了简单的RC4加密时,更加觉得SSH是因为加密繁重,才导致很慢的。

然而,在今天看来,SSH慢,显然不是因为繁重的加密,比如下面这个小实验,远端的iperf3服务器运行在J1900(发布于2013年)上,建立SSH加密通道后测速(使用-R参数,远端发流给本地),也能很轻松跑到500Mbps以上。就算当年VPS的没这么强,速度打0.5折,也有25Mbps,都已经足够用来看1080P了(而且当年宽带速度普遍也就4-20M上下),多多少少可以说明加密性能并非瓶颈。

当年还有人提出,SSH基于TCP,会出现TCP in TCP的问题,导致慢。但显然此说法也不成立,因为SSH的动态代理功能本质上还是『把数据拿出来单独代理』,本地已经终结掉浏览器发起的TCP了,自然不会存在TCP in TCP的问题。假如是这种做法有问题,那么后期的SS等一众协议,都是基于这个做法的,为什么他们就快很多呢?

很显然,问题还是出在我们今天所讨论的多路复用上。SSH的默认机制和H2差不多,也是仅仅建立1条加密后的TCP连接,来把多个代理请求送到对端。当年是什么网络环境?搞不好还是共享的4M ADSL跨洋拨美国SSH,对端可能还是一个虚拟主机而非专门的代理服务器,再加上当时国际出口并不宽裕,队头阻塞问题很容易就把在单条SSH连接上承载的各个代理请求给一波带走。另一方面,SS则是对每一个代理请求单独开一条连接,且当时用的还是HTTP/1.1,浏览器加载页面需要发起6-8个TCP连接,所以SS也会发起6-8条连接,再加上看不同网站也要开不同连接,因为零散丢包引起的队头阻塞问题就会被分散了,至少不至于全堵死。(当然,内层流量出现大量的H2之后,问题就回到4.1了)

当然了,这里除了队头阻塞,还有TCP拥塞控制协议的问题。当年没有BBR,而上面强调『跨洋』就是因为此时已经可以引发TCP LFN(长肥网络)问题,导致TCP速度起来慢,上限低,不过与今天的话题无关,因此略过。

4.3 Mux有反效果?

一方面,目前各大加密代理软件对于Mux的实现,基本上都是在原来的代理协议上套一层Mux协议(因为代理协议已经成型,为了保持兼容性,不能再动了),而这多套的协议会带来额外的传输开销,减慢速度,这个是正常现象。

另一方面,就是和上面的SSH一样的原理了,一旦队头阻塞,一波请求全带走。

4.4 OpenVPN TCP慢?

另一方面,和上面的SSH还是一样的原理,原因也是队头阻塞把一波请求全带走。

而且此时还更糟糕,因为VPN是三层协议,会完整传输IP层的数据,不会帮你终结TCP。在可靠传输层上实现另一个可靠传输层是个坏主意。TCP假定底层传输是不可靠的,所以丢包就要重传。但是在TCP over TCP的场景下,内层TCP(应用程序发起)无法感知外层TCP(OpenVPN)之外的网络情况,因此很容易出现外层重传内层的重传包,内外TCP窗口反复震荡等情况,合称就是TCP Meltdown

4.5 HTTP/3?Hysteria?

HTTP/3和Hysteria,都基于QUIC,而QUIC又基于UDP。能出现这些基于无连接不可靠的UDP协议,原因就是,他们需要避免TCP队头阻塞的问题,让各个逻辑流成为(部分意义上)的实际流,避免在丢包环境下导致队头阻塞的跨流扩散。

5. 写在最后

很显然,业界普遍认为,基于UDP重新实现协议,实现应用层对传输情况的更多感知,是一条出路。不过因为UDP在运营商那似乎不受待见,因此感觉...一根筋两头堵?

看看怎么发展吧,不过老实说,到了今天这个时间,白天还好,一旦进入晚上高峰期,没有Hy2协议的帮助,那就真的卡成狗咯。

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2025年12月13日 22:23

添加新评论

提醒:『评论回复邮件提醒』功能正在测试中
评论后,如果站长有回复,会有邮件通知