HTTP/2 与 HTTP/3:复用、队头阻塞与 QUIC 的真实边界
HTTP/2 和 HTTP/3 没有重新定义 GET、404、缓存或条件请求。它们改变的是语义如何映射到连接、流和帧,以及丢包、流控和压缩状态如何影响并发请求。只用“HTTP/2 多路复用,HTTP/3 更快”概括,会漏掉最重要的工程边界:HTTP/2 解决了应用层串行,却仍受 TCP 有序交付约束;HTTP/3 把不同流的传输阻塞隔离开,却没有消灭拥塞、应用依赖和 QPACK 阻塞。
三代承载模型
HTTP/1.1 可以复用连接,但同一连接上的响应次序约束使并发处理困难,客户端往往建立多条 TCP 连接。HTTP/2 把消息拆成带 stream id 的帧,让多条流交错在一条 TCP 连接上,并用 HPACK 压缩 Header。RFC 9113 同时定义连接级和流级流控、优雅关闭与错误范围。
HTTP/2 帧可以交错,但底层 TCP 只向上提供连续字节。序列空间出现缺口时,后续已到达的字节也不能交给 HTTP/2 解析器,因此一处丢包会暂停该连接上所有流。这是传输层队头阻塞,不是 HTTP/1.1 的响应排队。
HTTP/3 由 RFC 9114 定义,运行在 RFC 9000 的 QUIC 上。QUIC 在 UDP 之上实现可靠传输、拥塞控制和独立流,并把 TLS 1.3 握手集成进传输建立。某条 QUIC 流丢失数据时,其他流不必等待它补齐,因此隔离了跨流传输阻塞。
“消灭队头阻塞”仍然是错的
HTTP/3 没有消灭三类等待。第一,连接共享拥塞窗口,路径拥塞仍会降低所有流吞吐;第二,应用依赖仍存在,例如 HTML 没返回,后续资源发现就无法开始;第三,QPACK 为提高压缩率使用动态表,Header 块引用尚未到达的表项时仍会阻塞。QPACK 通过独立编码器流和阻塞流上限控制风险,而不是让依赖凭空消失。
HTTP/2 的 HPACK 动态表也有安全与容量成本。压缩状态跨请求共享时,要防止基于长度差异推断秘密,并限制解码后的 Header 大小,不能只限制压缩字节。大量微小流还会带来调度、窗口更新和内存开销;单连接复用并不意味着只建一条连接永远最优。
流控是两级资源协议
HTTP/2 的连接窗口限制所有流总未消费字节,流窗口限制单流。接收方只有在应用真正消费数据后才应归还窗口;若读取层提前把所有内容搬进无界内存再更新窗口,流控只是把背压从网络层移除。一个不消费响应体的慢请求可以耗尽流窗口;大量慢流则能耗尽连接窗口和堆内存。
QUIC 同样有连接级与流级流控,并额外限制可创建流数量。服务端需要监控活跃流、阻塞流、连接窗口、流窗口、排队字节、重置原因和 GOAWAY 之后的新流行为。网关若在 HTTP/3 入站与 HTTP/1.1 出站之间转换,前端的高并发流最终仍可能在后端连接池排队,瓶颈只是换了位置。
优雅下线不能直接断开复用连接。HTTP/2/3 使用 GOAWAY 表达“不再接受更高编号的新请求”,允许既有流完成;客户端收到后应在新连接上发送后续请求。若负载均衡器先杀连接再等待应用摘流,数百条流会同时失败并触发重试风暴。
协议选择依赖发现与回退
HTTPS 上 HTTP/2 通常通过 TLS ALPN 协商 h2。HTTP/3 使用 QUIC/UDP,ALPN 标识 h3;客户端还需要得知服务端可用的 HTTP/3 端点,可来自先前响应的替代服务信息或 HTTPS DNS 记录。UDP 被网络策略阻断、QUIC 握手失败或服务端不支持时,客户端必须在总 deadline 内回退,而不是先耗尽一套完整超时再走 TCP。
连接迁移利用 QUIC connection id,使 NAT 重绑定或网络接口变化后不必只靠四元组识别连接。但迁移不是无条件“移动网络不断线”:新路径仍须验证,拥塞状态需要谨慎处理,中间设备和应用会话也可能施加限制。
Java 内置客户端的能力必须按实际运行时描述。Java SE 26 的 HttpClient 支持 HTTP/1.1、HTTP/2 与 HTTP/3,但 HTTP/3 不是默认选择,实际使用还受 HTTPS、代理、服务端能力与实现选项影响。Java 17 实验不声称建立 HTTP/3,而是证明“偏好版本”不等于“协商结果”。
examples/backend-development/network-web/http2-http3/HttpClientNegotiationDemo.java 向本地只支持 HTTP/1.1 的服务发起偏好 HTTP/2 的请求;TransportLossIsolationDemo.java 用三个流量槽模拟 TCP 缺口与独立流丢失的影响范围:
javac --release 17 --add-modules jdk.httpserver -Xlint:all -Werror examples/backend-development/network-web/http2-http3/HttpClientNegotiationDemo.java examples/backend-development/network-web/http2-http3/TransportLossIsolationDemo.java
java --add-modules jdk.httpserver -cp examples/backend-development/network-web/http2-http3 HttpClientNegotiationDemo
java --add-modules jdk.httpserver -cp examples/backend-development/network-web/http2-http3 TransportLossIsolationDemopreferred=HTTP_2 negotiated=HTTP_1_1 status=204
activeStreams=3 h2BlockedByTcpLoss=3 h3BlockedByStreamLoss=1第二个程序是影响域模型,不是网络协议实现;它把关键架构差异固化成断言。真正的协议验证还应使用抓包、qlog、服务端连接指标和受控丢包实验。
性能比较必须控制变量
比较协议时至少固定:请求并发、对象大小与数量、连接是否预热、DNS/TLS 是否计入、RTT、带宽、丢包率、CPU、压缩设置、代理链和服务端实现。局域网零丢包下,HTTP/3 的用户态加密和 UDP 处理成本可能高于成熟 TCP 栈;高 RTT、丢包和大量并发流时,它的隔离价值才更明显。只报告平均吞吐会隐藏握手和尾延迟。
应该同时观察连接建立 p50/p95/p99、TTFB、总完成时间、重传、丢包、拥塞窗口、活跃/阻塞流、GOAWAY、协议回退率、CPU 每请求和每连接内存。按 ASN、地区、网络类型和客户端版本分组,才能发现某些网络系统性阻断 UDP。
部署 HTTP/3 应先小流量启用,保留 HTTP/2 回退,验证防火墙、DDoS 防护、负载均衡、可观测与证书链均支持 QUIC。出现问题时可关闭协议广告而不是回滚业务语义。协议升级的目标不是追逐版本号,而是缩小高并发和不可靠路径下的故障影响域。
一条大连接也会扩大故障域
复用减少握手和慢启动,却把更多请求绑定到同一连接。连接级协议错误、GOAWAY 处理缺陷、证书终止点重启或 NAT 状态丢失时,数百条流可能同时失败。客户端应限制单连接最大并发流,在连接接近年龄或流量上限时预建替代连接,并把失败重试打散;服务端下线则先发 GOAWAY,再等待可接受的最高流完成。
连接合并允许在满足证书、地址和权威性条件时复用一条连接服务多个 origin,它能节省资源,也可能让一个 origin 的限流或故障影响另一个 origin。客户端和网关必须按 origin 维护授权、Cookie 与流量配额,不能因为共享连接就共享安全上下文。
QUIC 的可观测方式不同
QUIC 数据包头部与大部分传输信息受保护,传统中间设备无法像检查 TCP 那样直接推断所有状态。诊断需要端点导出的 qlog、连接 id、握手与路径事件、流级重传和拥塞指标。负载均衡若依赖四元组固定路由,会与连接迁移冲突;通常需要理解连接 id 的路由策略或由 QUIC 终止层统一处理。
UDP 无连接不等于 QUIC 无状态。每条 QUIC 连接仍有密钥阶段、包号空间、拥塞控制、流表和路径验证状态。DDoS 防护要限制初始包、验证地址所有权并控制握手资源,不能照搬“UDP 全部放行”或“UDP 全部限到极低”的粗粒度策略。
协议回退必须避免降级盲区
HTTP/3 失败后回退 HTTP/2 可以提高可用性,但若监控只看最终请求成功,就会长期掩盖某地区 UDP 全部不可用。应记录首选协议、实际协议、失败阶段、回退耗时和缓存的可用性信息;发布时设置回退率阈值。安全策略也要在各版本一致,不能出现 HTTP/3 严格校验证书而回退路径关闭验证的情况。
压测要加入连接迁移、0-RTT 拒绝、包重排、MTU 黑洞和服务端 GOAWAY,而不只是固定 RTT 下跑吞吐。只有在异常路径上仍能守住总 deadline、内存上限和正确回退,协议升级才算完成。
