当前互联网几乎所有的 HTTP 通信都由 TCP/IP 来承载,但 TCP 为了可靠性而牺牲性能被很多人所诟病。再到 QUIC 及 HTTP over QUIC 的发布,经过改进的 UDP 仿佛正在为未来替代 TCP 做准备。虽然 HTTP/3 还没有大面积普及,但越来越多的公司及个人也在不断尝试在传输层提升网络连接的效率。这篇文章主要讨论 TCP 及其问题和 QUIC 协议相关的内容。

关于 TCP 及其性能评估

传输层 TCP 协议为 HTTP 提供了一条可靠的比特传输管道。当 HTTP 传输一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序输送。TCP 收到数据流后,会将数据流砍成数据块(段),并将其封装在 IP 分组中,数据格式如下图:

DATA

传输协议在设计时需要对各种条件和场景进行策略的权衡取舍,TCP 为了可靠性设计了一系列的规则比如三次握手,四次挥手等等。下面我们来展开讨论下 TCP 连接细节以及性能问题。

建立连接

TCP 慢启动拥塞控制

为了防止 TCP 连接一开始向对方发送大量数据而导致网络拥塞崩溃,TCP 连接一般会随着时间进行自我调谐,起初限制连接的最大速度,如果数据传输成功,则会提高传输速度,这种自我调谐被称为 TCP 慢启动,用于防止因特网的突然过载和拥塞。

TCP 连接控制速度大小则通过拥塞控制窗口,在建立之初双方会协商传输的数据大小,然后发送方的拥塞控制窗口大小会跟随接收方的响应而变化,如果发送成功则线性增长,如果丢包则减半。

三次握手

为什么要三次握手
3-way

建立 TCP 连接时,需要通信双方首先要对 Socket,序列号以及窗口大小信息达成共识,Socket 是由互联网地址标识符和端口组成,窗口大小用来做流控制,序列号则是用来追踪通信发起方发送的数据包序号,接收方可以通过序号来向发送方确认某个数据包的成功接收。

历史连接判断

我们知道一个客户端是可以重复的与同一台服务器建立连接进行通信的,RFC 793 中就指出三次握手的首要原因就是阻止历史重复连接导致的混乱问题。

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

假如 TCP 连接为两次握手,那服务器方将只能选择接受或拒绝发送方多次的请求,它并不清楚哪一个是过期的连接。

为解决这个问题,TCP 选择了三次握手并在连接引入了 RST 这一控制消息,发送方在收到接收方的 ACK 确认号后,会判断是否为历史连接,如果是会发送 RST 控制消息中止这次连接。如果不是则会继续完成握手流程。

序列号控制

由于网络具有很大的不确定性,可能会导致下面的问题:

  • 数据包被发送方多次发送造成的数据重复
  • 数据包在传输过程中被路由或者其他节点丢失
  • 数据包到达接收方可能无法按照发送顺序

所以 TCP 在数据包中加入了 SEQ 字段,有了数据包的序列号则可以:

  • 接收方可以通过序列号对重复的数据包去重
  • 发送方会在对应数据包未被 ACK 时重复发送
  • 接收方可以根据数据包的序列号对它们进行重排
避免数据丢失造成的循环

我们再假设另外一个场景,发送方发送了一个连接请求分组,对方收到后并发送确认应答分组,如果这时候认定连接建立,接收方会开始给发送方传输数据分组,但在应答分组丢失的情况下发送方不知道是否建立好连接,将会忽略任何数据分组直到应答分组收到为止。而接收方在发出的分组超时后会重复发送,这也形成了某种意义的死循环。

性能损耗

三次握手需要双方一共发送 3 个 TCP 分组(通常是 40 ~ 60)个字节,那么三次则是 120 字节到 180 字节,但大多数 HTTP 请求都不会携带大量的数据,小的 HTTP 事务很可能会在 TCP 建立上花费 50% 或者更多时间,同时也会增加很多字节的额外开销。

延迟确认与重传机制

由于我们无法确保发送的数据分组一定能被对方收到,所以 TCP 实现了自己的确认机制来确保数据的成功传输。

每个 TCP 段都有一个序列号和数据完整性校验和 (checksum),接收者收到完好的段时都会向发送者回送小的确认分组。如果发送者没有在指定窗口时间(通常有 100 ~ 200 ms)内收到确认信息时,发送者就认为分组已被破坏或损毁,并重发数据。

ACK 的方式很容易保证消息的顺序性,但在某些情况下会导致发送方重传已经接收的数据:比如发送方发送四个数据段,后三个传输成功但第一个失败了,由于 ACK 语义是当前数据段前的全部数据段都已经被接收和处理,所以接收方无法发送 ACK 消息,发送方看没有 ACK,所有数据段对应的计时器就会超时并重新传输数据。在丢包严重的网络下,这种重传机制会造成大量的带宽浪费。

断开连接

四次挥手

4-way

如图所示,TCP 连接的拆除需要经过 four-way handshake,客户端或服务器均可主动发起挥手动作。

为什么建立连接是三次握手而断开时是四次呢?

建立连接时,服务器开始处于 LISTEN 状态,收到 SYN 报文后可以把 ACK 和 SYN 放在一个报文回送给客户端。
但关闭连接时,服务器收到对方的 FIN 报文时,仅仅表示对方不再发数据了但还能接收数据,自己未必全部数据都已经发送给对方,所以此时可以即刻关闭,也可以发送一些数据后再发送 FIN 报文给对方关闭连接,所以一般 ACK 和 FIN 会分开发送。

TIME_WAIT

当 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段使用期的两倍 2MSL, 以确保这段时间内不会创建相同地址和端口号的新连接。另外 TIME_WAIT 的存在也是为了保证 TCP 双工可靠的终止,虽然四次挥手发送和协调完毕,但我们必须假设网络是不可靠的,无法保证最后发送的 ACK 报文一定会被对方收到,因此对方处于 LAST_ACK 状态下的 socket 可能会因为超时没收到 ACK 报文而重发 FIN 报文,所以这个 TIME_WAIT 可以用来重发可能丢失的 ACK 报文。

TIME_WAIT 时间是动态可配的,所以需要我们自己设定策略来选择 MSL 时间。

HTTP/2 的优化

HTTP/2 基于 Google 推行的 SPDY,专注于性能,最大的目标是用户和网站间只需要单个连接。所以增加了二进制分帧,多路复用等强大的功能。同时 HTTP/2 协议也是最大限度的兼容 HTTP/1.x 的,request 模型,scheme 并没有发生变化,不识别 HTTP/2 的代理服务器也可以将请求降级为 HTTP/1.x.

HTTP/2 并没有改变 HTTP/1.x 的语义,只是在应用层使用二进制分帧的方式传输。因此引入了新的通信单位:帧,消息,流。HTTP/2 是一个彻底的二进制协议,头信息和数据包都是二进制的,统称为“帧“。

http-2

帧的类型包括了 DATA 帧 (用来承载请求或相应的内容,包括 Pad Length, Data, Padding 等字段),HEADERS 帧 (用来承载 start line + header 的 HTTP Header 帧), PRIORITY 帧 (stream 流发送方指定了建议优先级),PING 帧(用作心跳检测及计算 RTT 往返时间), GOAWAY 帧 (用于启动连接关闭或发出严重错误信号) 等等。

分帧使得服务器单位时间接收到的请求数变多,可以提高并发数,也为多路复用提供了底层支持。

多路复用

多路复用就是在一个 TCP 连接中可以存在多个 stream 流。

connection

多个 stream 同时存在时是无序的,所以需要 streamID 来标识,stream ID 使用无符号的 31 位整数标识,客户端发起的流必须使用奇数编号,服务器发起的则必须使用偶数编号,流标识符零 (0x0) 用于发送控制消息,并不能建立新的 stream .

数据流在发送中的任意时刻,客户端和服务器都可以发送信号 (RST_STREAM 帧) 来取消这个数据流。

多路复用有效的解决多个链接时慢启动,重复 TCP 握手耗时的问题使得 TCP 效率更高。

HTTP/2 其他特性和存在的问题

HTTP/2 使用 HPACK 来开启头部压缩,另外客户端和服务器同时维护一张头信息表,所有字段会存入该表,生成索引号,同样字段再发送时只发送索引号即可。HTTP/1.1 平均响应头有 500 个字节左右,而 HTTP/2 平均只有 20 多个字节,只有以前的 4% 左右。

HTTP/2 还增加了服务端推送可以让服务端主动把资源文件推送给客户端。

但 HTTP/2 依旧存在一些问题比如:

  • 建连延时

TCP 连接依旧需要三次握手,消耗 1.5 个 RTT,再加上 TLS 握手,时间耗费将会更长。

RTT(Round-Trip Time):往返时延。表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。

  • 队头阻塞没有彻底解决

HTTP/2 在丢包时,整个 TCP 都要等待重传,会阻塞该 TCP 连接中的所有请求。

  • 多路复用导致的服务器压力

多路复用没有限制同时请求数,可能会导致许多请求的短暂爆发,导致 QPS 暴增。

  • Timeout

网络带宽和服务器资源有限,多个并行的流会导致资源被稀释,然后出现超时情况。

QUIC

QUIC(Quick UDP Internet Connections)协议基于 UDP 协议,它比较出色的解决了队头阻塞的问题,HTTP/3 也名 HTTP over QUIC,集成了 QUIC,TLS1.3 等等。

http3

QUIC 优势

主要特点为:

  1. 改进的拥塞控制,可靠传输
  2. 快速握手
  3. 多路复用
  4. 连接迁移

改进的拥塞控制,可靠传输

  • 应用层面能实现不同的拥塞控制算法

一个应用程序的不同连接能支持配置不同的拥塞控制,应用程序不需要停机和升级就能实现拥塞控制的变更,可以针对不同业务,网络和不同的 RTT 来使用不同的拥塞控制算法

  • 单调递增的 Packet Number 代替了 TCP 的 seq

每个 Packet Number 都严格递增且唯一,而 TCP 重传存在二义性,重新发送时数据包中标识符都不变。

  • 不允许丢弃确认过的 Packet

QUIC 中的 ACK 包含了与 TCP 中等价的信息,但 QUIC 不允许 ACK 确认过的 Packet 被丢弃。这样不仅可以简化发送端与接收端的实现难度,还可以减少发送端的内存压力。

  • 前向纠错能力 FEC

FEC 中,QUIC 数据帧的数据混合原始数据和冗余数据,来确保无论到达接收端的 n 次传输内容是什么,接收端都能够恢复所有 n 个原始数据包。

  • 更多 ACK 块和增加 ACK Delay 时间

QUIC 可以同时提供 256 个 ACK Block,在重排序时相对于 TCP 更有弹性,在丢包率比较高的网络下可以提升网络的恢复速度,减少重传量。

同时在计算 RTT 时会把接收到包到发送 ACK 的这段时间(ACK Delay)计算进去。

  • 基于 stream 和 connection 级别的流量控制

QUIC 的流量控制类似 HTTP/2,在 Connection 和 Stream 级别提供了两种流量控制。

快速握手

由于 QUIC 基于 UDP,所以只需花费 0~1 个 RTT 就可以建立连接。

多路复用

QUIC 是为多路复用设计的,携带个别流的数据包丢失时,通常只会影响该流,QUIC 连接上的多个 stream 之间并没有依赖,也不会有底层协议限制。

这也很大程度上缓解了队头阻塞的影响。

连接迁移

TCP 通过 ip, 端口号来确定连接, 而 QUIC 则通过 ConnectionID (64 bit) 来区别不同连接。主要 Connection ID 不变连接就不需要重新建立。

面临的问题

QUIC 对于弱网环境的优化是明显的,但是也由于各种原因面临着一些问题:

NAT 设备端口记忆问题

对于基于 TCP 的 HTTP(S) 传输,NAT 设备可以根据 TCP 报文头的 SYN/FIN 状态来了解通信开始结束状态,对应记忆 NAT 映射的开始和结束,但是 UDP 中不存在 SYN/FIN 状态位,如果 NAT 设备的记忆短于用户会话时间则用户会话会被中断。

NAT 设备禁用 UDP

在一些网络环境下(比如校园网),UDP 协议会被路由器等中间网络设备禁止(包括我国的运营商= =), 这时客户端会直接降级, 选择备选通道。

负载均衡

QUIC 客户端存在网络制式切换,就算是同一个移动机房,可能第一次业务请求会落到 A 服务器,后续再连接则落到 B 服务器,重复走握手流程。

更多 QUIC 的内容可以参照 chromium/quic 官方文档


以上为这篇文章全部内容,部分资料参考自: