http3_quic优缺点及原因

WHAT: HTTP3/QUIC是什么?

QUIC: Quick UDP Internet Connections
谷歌主导的网络协议,用udp代替tcp来作为http的传输协议。
HTTP3就是HTTP over QUIC

如上图所示是http2和http over quic的对比。
可以看到图中的分层不是完全对齐的,这是因为以前的分层方式不合理,影响了性能,因此quic重新划分了更合理的分层结构。
原来的tcp(传输层)和tls(会话层)要分别建立连接,增加了不必要的rtt。
quic在tls1.3的基础上,统一建立连接,然后再基于udp传输。
QUIC层的分层:
UDP层: 只管传输,不管连接;
Connection层: 复杂建连相关、拥塞控制(bbr_v2)、安全(tls1.3);通过cid来确认唯一连接;
Stream层: 负责多路复用;基于Connection层,通过StreamID进行唯一流确认,stream对stream frame进行传输管理;

使用TCP的协议: SPDY/HTTP2
使用UDP的协议: quic/HTTP3

WHY: QUIC的优点(为啥要使用QUIC)

  • 连接迁移优化
  • 队头阻塞优化
  • 拥塞控制优化
  • 握手优化

quic的优点其实也对应着解决了以前http协议栈中的几个缺点。

连接迁移功能

以前tcp的缺点

移动客户端ip变化以后,连接断开,需要重新握手、创建新的连接。
我们日常生活中坐车、旅行等场景下,用手机上网,经过不同的基站,移动网络切换是比较常见的场景,
因此可能会经历频繁的连接重建,体验较差。

为什么tcp有这个缺点:
tcp用通信双方的ip+port(4元组)来标示一个连接,因此客户端ip变了的话,连接就变了。
tcp很老了,最早的时候手机还不普及,固定电脑的ip基本上不会变。

http over quic则可以在客户端ip变化时,依然保持连接不断,减少了rtt,提高了体验;

quic如何解决这个问题的

上面有说的tcp这个缺点的原因是标示连接唯一性的方式,所以很自然的思路是用别的方式来标示连接唯一性。
quic的方法是用connectionId(cid,64位)来唯一确定一个连接,客户端ip变化以后,cid不变,因此连接可以不断开。

实现

对于quic server:
如果根据客户端ip路由,连接迁移会失败;得根据cid路由;
一般得路由到同主机、同进程,才能保证连接迁移成功。
或者把连接相关的有状态信息存储在分布式缓存中(比如把session ticket存储在redis中)。

quic协议考虑到cid对于路由的重要性,因此cid是明文存储的。

解决了队头阻塞问题(多路复用功能增强)

以前tcp的缺点

tcp队头阻塞的原因:
窗口更新机制: 滑动窗口的更新依赖于最左边的包的ack,如果队头的包一直没有ack,窗口不往后滑;
拥塞控制算法: 丢包以后启动丢包重传,窗口变小;
这些最终表现是tcp层面的队头阻塞。

  • http1.0: 明文短连接,多个tcp连接;
  • http1.1: keepalive机制,长连接,复用同一个tcp连接,降低了建立连接的开销(节省了握手时间);
    客户端:pipeline机制,可以并行发;
    服务端:必须顺序回,有队头阻塞;(同域名下的请求复用同一个连接,但必须排队)
  • http2: 用stream层解决http层面的队头阻塞(让同域名下的请求不排队了,间隔进行),但是还是有tcp层面的队头阻塞;

    quic如何解决这个问题的

    解决TCP层面的队头阻塞,本质上是要开发一个新的流量控制、拥塞控制的整套方案放在UDP层之上。
    quic的改进:
    (1)窗口更新机制: 当已经读取的数据大于最大接收窗口的一半时,发送WINDOW_UPDATE帧告诉发送者,接收窗口已经更新。
    (2)拥塞控制算法: 由于底层是udp,没有拥塞控制算法,因此quic需要实现拥塞控制算法。
    quic在应用层实现拥塞控制算法,如bbr,bbr_v2,核心思想是用rtt来预测带宽情况。
    tcp用丢包事件来判断带宽情况,比较不准。但tcp之所以这么傻,也是因为tcp中的一些约束,很难准确判断rtt的大小。
    比如seq number机制,重传的时候无法判断是超时重传的包还是第一个包经过很长时间收到了,因为seq number相同。
    quic的seq number改进: 通过streamId+offset, packet number的机制,重传的包packet number递增,这样可以算准rtt。

吞吐量更大(新拥塞控制算法)

接上一节,quic开发了新的拥塞控制算法,吞吐量更大,更能利用带宽:
cubic vs bbr:

可以看到tcp默认的cubic拥塞控制算法频繁上下调整滑动窗口大小,锯齿状;
而bbr倾向于平稳发送,在实际带宽比较平稳的场景下,吞吐量更大。(图中折线下方的面积更大)

原来tcp为什么没有解决这个问题:
(1)tcp在linux内核里,升级太困难了。
(2)参考上一节,tcp的一些约束导致rtt算不准。比如ack delay、重传包的seq number不变。

耗时短。握手-rtt减少;重连-0RTT特性。

原来https over tcp的缺点:
(1)握手阶段: tls层和tcp层重复建立连接;
(2)握手阶段: tls1.2需要考虑历史包袱,考虑达到最大兼容性,握手需要2RTT;
(3)重连阶段: tls1.2需要一次握手;

quic如何解决这个问题的

(1)握手阶段合并: 只建立一次连接,省掉tcp握手的1rtt。
将建立连接这件事统一放在quic层里,udp只专心做传输的事情。
(2)握手阶段: 1RTT。运气不好的话还是2RTT。
参考tls1.3,去掉不安全的算法,在客户端预置一些密码套件。
第一个握手客户端就直接选定加密协议,并生成相应的随机数,相当于跳过了tls1.2第一个rtt的协商;
如果服务端不支持,则使用HelloRetryRequest继续。
大部分情况下,都可以省掉第一次的rtt;
(3)重连阶段: 0 RTT。
参考tls1.3的session ticket(quic叫server config),客户端和服务端都缓存之前握手协商好的配置。
重连的时候,客户端直接把配置和数据一起发给服务端。

0RTT的前提:

1。client不清缓存;(过期前)
2。通过cid路由到同一个server进程;
(或者服务端对于server config有统一缓存)
缺点:牺牲了一定时间内的前向安全;(过期时间内)

QUIC的缺点

CPU开销大

http over quic与http over tcp相比,cpu开销可能会更大。
如果没有特别优化,单机qps可能下降50%。
主要有以下几个原因:

内核态vs用户态

(1)原来拥塞控制在tcp,在内核,执行在内核态,性能更高;
解决方案: 暂时没想到;
(2)quic限定udp报文大小<=1mtu(IPV6下为1350,IPV4下为1370)。
由于包的大小不能很大 => 因此包的数量会较多=> 系统调用很多 => 性能下降。
解决方案:
(2.1)批处理;用sendmmsg,一次系统调用发多个udp包;
(2.2)网卡GSO offload方案,降低cpu消耗;

quic对包的大小有两个限制
(1) initial包>=1200: 预防UDP攻击;(反射攻击)
如果数据本身不足1200,用padding方式填充到1200字节。
反射攻击:
被利用服务器输出流量与输入流量的比值我们称之为放大系数。
这个系数与被利用服务器所提供的 UDP 服务有关。
之前提到的利用 Memcache 漏洞的 DRDoS 攻击,可以获得稳定的 60000 倍放大系数。
而我们日常使用的 DNS 则可以轻松的获得 50 倍的放大系数。
由放大系数反推,我们可以知道,如果一个 UDP 服务被利用以后,放大系数小于等于1的话,则不存在利用价值.
因为这个时候,只从带宽流量方面考虑的话,还不如直接利用攻击主机对被攻击服务器进行攻击效率高。

(2)<=1MTU(IPV6下为1350,IPV4下为1370):
IP层是没有超时重传机制的,如果IP层对一个数据包进行了分片,只要有一个分片丢失了,只能依赖于传输层进行重传。
结果是所有的分片都要重传一遍,这个代价有点大。
由此可见,IP分片会大大降低传输层传送数据的成功率,所以我们要避免IP分片。

加解密开销大

谷歌fork的openssl分支:boringSSL(笑死)
具体原因不太清楚,可能是谷歌选择的加解密算法比较安全、计算开销比较大。
也有可能是开源实现里,选择密码编码学算法比较普通,没有给生产环境级别的优化。

解决方案:
如果在生产环境使用,需要相应修改加密套件。

容易被拦截

因为一些运营商不待见udp。
根据腾讯的文档:https://toutiao.io/posts/tixau8w/preview
QUIC失败率较高的三个省份为:贵州、广西和新疆;
失败率较高的运营商为:教育网和长城宽带。

解决方案:
需要支持降级到普通https。

nginx支持不全

比如nginx reload会断开已建立的quic连接.(因为对ng来说是udp)

解决方案:
需要修改nginx内核。

  • 内核逻辑调试工具
    systemtap 内核逻辑调试工具,查看内核调用栈
    内核的timewait监控: /proc/net/netstat中 的 TCPTimeWaitOverflow
    Broken pipe(32) tcp_max_tw_buckets

QUIC应用场景

弱网环境:因为quic的拥塞控制算法bbr在丢包时性能更好;
因为quic的重连、建连rtt少;
对安全重视的环境:因为tls1.3更安全;
移动端:因为连接迁移功能;
并发请求多的应用: 因为quic没有tcp层的队头阻塞,多路复用更彻底;

参考

可以参考谷歌开源的chromium中的quic协议栈,以及相应的server demo。
谷歌浏览器内核支持quic,其他浏览器内核就不一定了。
谷歌: https://www.chromium.org/quic/
腾讯:https://zhuanlan.zhihu.com/p/32560981
微博: https://www.infoq.cn/article/2018%2F03%2Fweibo-quic

推荐文章