HTTP/3 解决了什么问题,又引入了什么新问题?

6 阅读8分钟

HTTP/2 有一个很尴尬的处境:它在应用层做了多路复用,让多个请求可以在一条连接上并行跑。但它底下趴着的是 TCP,TCP 是一条严格有序的字节流。

这意味着什么?想象一条单车道公路,五辆车(五个请求)排成一列跑。第二辆车抛锚了,后面三辆车全堵在后面,哪怕它们各自要去的目的地完全不同。

技术上的情况也是一样。你在一条 TCP 连接上跑了五个 HTTP/2 请求,其中第二个请求的某个数据包丢了。TCP 不管你上层有几个请求——它只知道自己是一条有序的流,丢了一个包,后面所有包都得等着,等重传的包到了才能继续往上交付。

结果就是:第三、四、五个请求的数据其实已经到了,在操作系统的接收缓冲区里躺着,但应用层拿不到。一个包丢了,五个请求全卡住。

这就是 HTTP/2 的 TCP 层队头阻塞。讽刺的是,HTTP/2 费了很大力气消除了 HTTP/1.1 的应用层队头阻塞,结果一层一层剥开,发现底下的 TCP 又挡着了。在丢包率超过 2% 的网络环境下(地铁里、电梯里、高铁上),HTTP/2 的实际体验可能比 HTTP/1.1 开六条连接还差——因为 HTTP/1.1 的六条连接是独立的,一条卡了不影响其他五条。

HTTP/3 的核心改动就是把 TCP 换掉了。

QUIC:在 UDP 上重新造了一个"更好的 TCP"

HTTP/3 的传输层是 QUIC,QUIC 跑在 UDP 上。但说"HTTP/3 用了 UDP"容易产生一个误解——好像它是拿 UDP 裸传数据,不管丢包不管顺序。不是的。QUIC 在 UDP 之上自己实现了可靠传输、拥塞控制、流量控制,该有的全有。你可以把它理解成一个"用户态的 TCP",但设计上做了几个关键改进。

流级别的独立重传。 回到刚才公路的比喻——QUIC 把单车道换成了多车道高速公路。每个请求跑自己的车道,第二车道的车抛锚了,第三、四、五车道照样跑。技术上,QUIC 里每个 HTTP 请求是一个独立的 stream,stream 之间没有顺序依赖。stream 3 的一个包丢了,只有 stream 3 需要等重传,stream 4 和 stream 5 的数据已经到了就直接交付给应用层。队头阻塞在传输层就被消灭了。

握手更快。 TCP + TLS 1.3 需要先三次握手建立 TCP 连接(1 RTT),再做 TLS 握手(1 RTT),一共 2 个 RTT 才能开始传数据。QUIC 把传输层握手和加密握手合在一起了,新连接只要 1 RTT。如果你之前连过这个服务器,QUIC 还支持 0-RTT——客户端用上次协商好的密钥直接发加密数据,服务器收到就能处理,连一个 RTT 都不用等。

打个比方:TCP + TLS 像是去银行办业务,先排队取号(三次握手),再验证身份(TLS 握手),然后才能办事。QUIC 像是刷脸进门直接办。来过的老客户甚至不用刷脸,直接走 VIP 通道。

连接迁移。 TCP 靠四元组(源IP、源端口、目标IP、目标端口)来识别一条连接。就像你去银行办业务是凭你的柜台号和窗口号来对应的,你一换位子,银行就不认你了,得重新取号排队。你从 WiFi 切到 4G,IP 地址变了,TCP 连接就断了,得重新握手。

QUIC 换了个思路——不认位子,认人。它用一个 Connection ID 来标识连接,跟 IP 地址无关。你在高铁上从一个基站切到下一个基站,IP 变了,但 QUIC 连接还在,不需要重新建连。

部署的时候被现实教做人

UDP 在现实网络里是二等公民。 TCP 像是有几十年信用记录的老客户,互联网上的中间设备——运营商的路由器、企业的防火墙、NAT 设备——都认它,给它开绿灯。UDP 像是一个刚来的陌生人,很多门卫直接不让进。超过 40% 的企业防火墙默认屏蔽非 DNS 的 UDP 流量。国内运营商对 UDP 的 QoS 限制也比 TCP 严格,跨省长距离传输的时候 UDP 丢包率明显更高。

国内有些中等体量的公司尝试上线 QUIC 之后发现在部分网络环境下体验反而变差了,最后又下线了。不是协议本身有问题,而是国内的网络基础设施对 UDP 不够友好。大厂能跟运营商谈 UDP 白名单,普通公司没这个资源。

所以目前所有用 HTTP/3 的大厂都做了降级策略:先尝试 QUIC,如果 UDP 被封或者表现不好,自动回退到 TCP + HTTP/2。Chrome 的做法是第一次用 TCP 建连,同时并行尝试 QUIC,如果 QUIC 成功了,后续请求切到 QUIC;如果 QUIC 失败了,就继续用 TCP,一段时间内不再尝试。

CPU 开销更大。 TCP 的协议栈在操作系统内核里,经过了几十年的优化,有硬件卸载(TSO、GRO、checksum offload)等加速手段。QUIC 跑在用户态,每个包的加解密、可靠传输、拥塞控制全在应用进程里做。实际测试下来,QUIC 的 CPU 消耗大概比 TCP + TLS 高 50% 左右。在高带宽场景下差距更大——TCP 借助内核和网卡的硬件加速可以轻松跑满万兆网卡,QUIC 的吞吐量则严重依赖具体实现,差的只能跑到几百 Mbps。

这在 CDN 节点上是个实际的成本问题。同样的服务器,跑 QUIC 能扛的并发量比跑 TCP 少。不过这个问题在改善——内核旁路(DPDK)、io_uring、GSO 等技术在逐渐拉平差距。

0-RTT 的重放攻击。 QUIC 的 0-RTT 恢复连接虽然快,但有一个安全代价:0-RTT 发送的"早期数据"可以被攻击者截获并重放。攻击者把你上次发的 0-RTT 数据包原封不动地再发一遍给服务器,服务器可能会重复执行。

如果你的 0-RTT 请求是 GET 一个页面,重放了也没事,因为 GET 是幂等的。但如果是一个 POST 扣款请求走了 0-RTT,被重放就扣了两次钱。所以规范要求 0-RTT 只能用于幂等请求,非幂等操作必须等完整握手完成后再发。这需要应用层自己保证,QUIC 协议本身不会帮你拦。

全程加密让网络运维变难了。 QUIC 从握手第一个包开始就加密,连 TCP 的那些明文头部信息都没了。传统的 DPI(深度包检测)设备看到 QUIC 流量就是一堆加密的 UDP 包,分不清里面是视频流还是 API 调用。对安全审计、流量分析、异常检测来说,这是个很大的挑战。

从隐私角度看这是好事——运营商和中间人更难窃听你的流量了。但从企业网络管理的角度看就很头疼。

NAT 超时问题。 NAT 设备给 UDP 连接维护的状态表超时时间通常比 TCP 短得多。TCP 连接有 FIN 包来明确关闭,NAT 设备知道什么时候回收状态。UDP 没有关闭信号,NAT 设备只能靠超时来回收。一旦 NAT 状态被回收,QUIC 连接的后续包就会被丢掉。虽然 QUIC 的 Connection ID 理论上支持 NAT rebinding(重新绑定),但各家实现差异很大——Google 的实现支持完整的 NAT rebinding,Cloudflare 的实现就不太行。Safari 遇到 NAT 超时的时候会固定等 15 秒,期间页面白屏。

大部分人不需要自己折腾 HTTP/3

截至 2025 年底,Cloudflare 的数据显示全球大约 30% 的 Web 流量已经跑在 HTTP/3 上。Google、Meta、Cloudflare 是主力推动者,主流浏览器都已支持。

但"浏览器支持"和"你的业务该自己部署 QUIC"完全是两回事。

你的用户主要在弱网环境——移动端 App、东南亚市场、地铁高铁场景——QUIC 的收益是实打实的。流级别的无队头阻塞和连接迁移在这种场景下体感明显。

你的业务是数据中心内部通信、大文件传输、或者用户坐在办公室里用有线网络,HTTP/2 + TLS 1.3 已经够了。网络条件好的时候 QUIC 和 TCP 的差距可以忽略不计,为了那点差距去背上 UDP 被防火墙拦、CPU 开销更高、NAT 超时白屏这一堆运维成本,不值。

实际操作上,大部分团队的做法是让 CDN 来扛。Cloudflare、AWS CloudFront 这些 CDN 默认已经开启 HTTP/3 了,你的源站继续用 HTTP/2,CDN 边缘节点负责跟浏览器之间走 QUIC。降级策略、NAT rebinding、0-RTT 安全限制这些破事全是 CDN 在处理。你不需要自己改一行代码,用户打开 Chrome 就已经在走 HTTP/3 了。

所以 HTTP/3 解决的问题是真实的——TCP 队头阻塞、握手慢、连接不可迁移,这些确实是二十年来的老痛点。引入的新问题也是真实的——UDP 被歧视、CPU 开销、0-RTT 安全风险、运维黑盒。这两边不是谁压倒谁的关系,而是取决于你的用户在什么网络环境下用你的产品。高铁上刷短视频的用户和办公室里用 ERP 系统的用户,面对的根本不是同一个问题。