浏览器给每个源开六条连接,是 HTTP/1.1 时代的默认做法。连接数有限,一条连接上一次只能有一个请求在传输,其余请求排队;响应还必须按请求顺序返回,管线化因此基本没被用起来。HTTP/2 针对的就是这一层:在一条 TCP 连接上引入二进制分帧层,把请求和响应切成帧,标上流标识符,交错发送。
帧的类型不多:HEADERS 携带压缩后的头部,DATA 携带正文,RST_STREAM 中止单个流,WINDOW_UPDATE 更新流量控制窗口,GOAWAY 关闭连接。客户端发起的流用奇数编号,服务端用偶数。同一条连接上可以同时存在几十上百个流,头部用 HPACK 压缩,重复的 Cookie 和 User-Agent 不再每个请求重发一遍。RFC 9113 还明确废弃了 RFC 7540 中基于依赖树的优先级方案,由服务端自行决定先发哪个流。
问题出在下面那层。TCP 对上层暴露的是一条有序字节流,内核只有在收到连续字节后才把它们交给应用。任何一个报文段丢失,后面已经到达的字节都要在内核缓冲区里等着,直到重传补上缺口。HTTP/2 的流是逻辑概念,帧仍然要按顺序塞进这条字节流;TCP 卡住时,连接上所有流一起停,包括那些数据早已到达的流。丢包率越高、RTT 越大,这种连接级别的停顿越明显:一次重传等待会把所有并发请求一起拖慢,而 HTTP/1.1 的六条连接至少还能让一部分请求绕开。
还有一层不是 TCP 造成的:单个流内部仍然有序。一个流上的 DATA 帧必须按发送顺序交付给应用,大响应体不能拆开乱序到达。流与流之间可以交错,流内部不行。
流量控制也值得看。HTTP/2 有连接级和流级两个窗口,接收方通过 WINDOW_UPDATE 放行。慢速消费的流会耗尽自己的窗口,但它在途中的数据同样占着连接窗口;窗口设得太小,带宽延迟积大的链路上吞吐上不去。默认初始窗口 65535 字节,在长肥管道上明显不够,实现通常要显式调大。
结论不复杂:HTTP/2 把应用层的排队问题解决了,把队头阻塞推到了传输层。要在丢包链路上消除它,只能换传输方式——QUIC 把流的状态搬进用户态,每个流独立恢复丢失的数据,一条流丢包不影响另一条。HTTP/3 走的就是这条路。
判断:服务端已经在用 HTTP/2 而延迟仍不理想时,先量丢包率和 RTT,再检查初始窗口大小与 SETTINGS_MAX_CONCURRENT_STREAMS 的取值。在稳定低丢包的内网里,多路复用的收益主要落在头部压缩和连接数减少上,延迟改善有限;跨公网的收益则取决于链路质量,这一点需要按实际路径测量,资料未给出通用数值。
评论
还没有公开评论,欢迎留下第一条讨论。