一、HTTP/2、队头阻塞与拥塞控制
今天被广泛使用的 Hysteria 看似是个新奇的产物,但它的渊源却能追溯到十多年前的 HTTP/2 时代。
HTTP/2 作为一个应用层协议,采用了一种特别的传输方式:只用一条 TCP 连接,同时承载几十上百个数据流(stream)。每个 stream 被切成小块(帧),附上 stream ID 后全部混杂在一起传输。这样做的好处是只需进行一次 TCP 握手,大大加快了网页的加载速度。
但代价同样十分沉重。只要传输途中丢了一个数据包,或者任何一方的网络出现中断,整个 TCP 连接就会暂停,等待丢失的数据包重传。在此期间,其他已经到达的数据,哪怕属于别的 stream、跟丢掉的那个包毫无关系,也只能待在内核缓冲区里等。排在队头的一个包卡住,整队数据都得跟着干等,这种现象就叫作队头阻塞。
不仅如此,丢包还会触发拥塞控制算法。你暂时只需要知道:它会让网速进一步下降。
二、QUIC 的不完全答案
随着大家对网速的要求不断提高,现有的协议似乎都无法真正解决这个问题。这时候,QUIC 千呼万唤始出来。它的全称本来是 Quick UDP Internet Connections,但标准化之后就不再被当作缩写,而是直接作为名字使用了。
为了解决队头阻塞,QUIC 的方案是让同一个连接中的各个 stream 相互独立:哪个 stream 丢了包,就单独为它重传,其他 stream 照常传输,不必陪着它一起等。就这样,队头阻塞的问题在 QUIC 中得到了解决。
同时,TCP 的实现大都在内核态,而 QUIC 的实现多在用户态的软件当中。如果你对其中的实现有不满意的地方,直接动手改起来也很方便。
但是,拥塞控制的问题怎么办?尽管丢包时不会再出现队头阻塞,但在拥塞控制方面,QUIC 可是完全继承了 TCP 的思路:一个连接里的所有 stream,依然共用同一个拥塞控制器。换言之,一旦发生丢包,依然会降速。
三、降速换不来回报
可是,假如这个算法直接把网速减没了,那还有谁愿意用它呢?事实上,它本来就不是为你个人而设计的。
在互联网的早期,全世界的线路大都孱弱不堪,网络上的流量轻而易举地就能超出线路所能承载的上限。于是所有人都开始丢包、重传,到最后网络上全是重传包,根本无法传输任何有效的信息。假如我们无法立即扩充网络带宽,那么解决办法就是每个人都少传一点:这样虽然每个人分到的带宽少了,但至少网络还能用。正是出于这样的考虑,才有了拥塞控制。可见,它并非一个错误的设计。
然而时至今日,情况不同了:在跨境线路上,降速已经换不来回报了。触发拥塞控制的通常有两种情况:
-
真的拥塞:在如今流量浩如烟海的互联网上,个人的退让对缓解网络拥塞的作用可以忽略不计。
-
运营商限速:这种丢包是人为造成的,手段大致有两种:
- 硬限速:设定一个速率上限,超出的部分直接丢掉;
- 随机丢包:不论你发多少,都按一定比例丢掉一部分。
无论哪种,拥塞控制算法都会让你的网络进一步变慢。
在这样的背景下,传统拥塞控制算法(如 CUBIC)一丢包就降速的思路,显然已经不可取了。BBR 算法则能缓解这个问题:它不再单纯依靠丢包来判断拥塞,而是通过测量带宽和延迟,算出网络里最合适的在途数据量,尽量把自己能用的带宽都用上。
四、Brutal
我们终于来到了 Hysteria(下文简称 Hy)的部分。
与一些把多个连接塞进同一条 TCP 连接的代理协议不同,Hy 直接建立在 QUIC 之上,因此那些源自 HTTP/2 设计思路的队头阻塞问题,也就不复存在了。
那么拥塞控制呢?Hy 也交出了它的答卷。还记得前面说过 TCP 在内核态,而 QUIC 在用户态吗?内核态的 TCP 与操作系统深度耦合,改动起来可能需要 root 权限或者专门的内核模块,还是挺麻烦的;而 QUIC 运行在用户态,在它的基础上做改动要自由得多,甚至还能直接继承 QUIC 本身的特性,并跟上它的后续更新。正因如此,“臭名昭著”的 Brutal 算法才得以诞生。
如果你仔细读过第三节,不难感受到:在跨境线路上,按照拥塞控制的本意行事,并不能让你的上网体验立刻好起来。既然礼貌的退让换不来回报,那就做好战斗的准备吧!Brutal 算法的思路就是反其道而行之:只要填写带宽字段、自己指定发包速度,就能不受降速的影响;丢了多少包,它就按一定比例补回来,一点也不退让。
不过反过来看,Brutal 好用的前提,其实是线路上大部分流量还在守规矩。
而且,假如随机丢包的问题 BBR 已经能在一定程度上缓解,而硬限速又确实谁也突破不了,那 Brutal 比 BBR 多赢的部分主要是从哪来的?可能就在于:真正发生拥塞的时候,它还在拼命不让道。
因此,如果带宽字段填得太大,吃亏的其实还是你自己:Brutal 会按你填写的速率发包,超出实际带宽的那部分,会直接在你自己的线路上被丢掉;而它越是发现丢包,就越会加码去补,于是越补越堵。
既是参考文档,也是延伸阅读:
hy2就是版本答案
其实这玩意,简单来说就是对H3的封装,只不过可以控制两端的发送包,以强制性速率的方式进行包的重传,其实有点怎么说呢,设计上抛弃了部分效率,从而带来了一个很不妙的结果: 对于一个需要控制超时的请求/响应, 这玩意实现起来还有点困难,因为底层可能在不断重传,但是因为你又新发了一些新的包来响应失败,所以这玩意对于实际工程化落地还是不太行的,会导致消息风暴
而且现在国内运营商基本上对于quic协议的支持很有限,或者说quic是被牺牲的那个,在高峰期UDP类的协议会有大量丢包,导致效率也会慢很多,要真实现的话,我推荐KCP+TCP这样去实现,至少这样能够在应用层面控制消息风暴,虽然实现起来困难就是了