11.2 HTTP/3、QPACK、发现与部署
HTTP/3 利用 QUIC 减少跨请求的传输层等待,但服务上线还需要解决三个问题:客户端如何发现它,UDP 不通时怎样回退,以及切网后的包如何找到原连接。本课沿着请求、压缩、发现和路由展开,最后用 curl 的协议输出区分“请求成功”和“确实用了 HTTP/3”。
HTTP/3 保留 HTTP 方法、状态码、字段和缓存语义,重新把帧与多路复用映射到 QUIC。应用通常不需要为此重写 REST 接口,但传输与运维配置会改变。
1. HTTP/1.1、HTTP/2 与 HTTP/3 的分层
下面只展示承载关系;HTTP/1.1 和 HTTP/2 使用 HTTPS 时,还需要 TLS:
HTTP/1.1: semantics -> HTTP/1.1 textual framing -> TCP -> IP
HTTP/2: semantics -> HTTP/2 binary frames -> TCP -> IP
HTTP/3: semantics -> HTTP/3 frames -> QUIC -> UDP -> IPHTTP/1.1 的持久连接可以处理多个请求,流水线也允许不等前一响应就继续发送请求,但响应仍须依次返回,实际客户端支持也有限。“一个连接一次只有一个在途请求”是常见实现选择,不是 HTTP/1.1 的全部协议能力。
HTTP/2 允许不同流的响应交错发送,摆脱 HTTP/1.1 流水线的响应顺序限制。TCP 字节缺口仍会挡住后续数据的交付;HTTP/3 使用独立 QUIC 流来减少这类跨流阻塞。
HTTP/3 是否更快,要看目标用户的网络与负载。低丢包、低 RTT 的路径,已有的 TCP/TLS 热连接,UDP 限速,以及 QUIC 实现的 CPU 开销,都可能改变结果。协议名称不能代替一次测量。
2. 请求流与关键单向流
每个普通请求及其响应使用一条客户端发起的双向 QUIC 流。两个方向分别携带消息,下面是顺序示意,不要求响应等请求体全部发完才开始:
client -> server: HEADERS -> optional DATA -> optional trailers(HEADERS)
server -> client: optional 1xx HEADERS -> final HEADERS
-> optional DATA -> optional trailers(HEADERS)HTTP/3 帧与 QUIC 包的边界不同:一个 HTTP/3 帧可跨多个 QUIC STREAM 帧和包;一个 QUIC 包也可以装入多条流的数据。接收端先恢复流字节,再解析 HTTP/3 帧。
连接还使用以下关键单向流:
- 每端一条控制流,首帧必须是
SETTINGS,随后按角色发送GOAWAY、MAX_PUSH_ID等控制帧; - QPACK 编码器流,发送动态表更新指令;
- QPACK 解码器流,发送字段确认等反馈。
每端必须允许对端至少创建三条单向流;在不使用动态表等特定配置下,QPACK 流可以不实际创建,但一旦创建就不能任意关闭。关键流关闭属于连接错误。RFC 9114 §6.2、RFC 9204 §4.2
遇到未知单向流类型,接收端应停止读取该流,或丢弃其后数据,不得仅因类型未知就把整个连接判错。同时仍要限制流数和缓冲,避免扩展兼容性变成无限资源消耗。
3. QPACK 怎样控制压缩依赖
HTTP/2 的 HPACK 借助 TCP 全局有序交付来同步动态表。QUIC 流可以独立到达,某个请求的压缩字段可能先于它引用的表项到达,因此 HTTP/3 改用 QPACK。
这里的字段段(field section)指一组头部或尾部字段。两个方向各有一组编码器与解码器状态:
- 编码器流把插入、复制和容量调整指令发给对端解码器;
- 解码器流把字段段确认、流取消和插入计数增量发回对端编码器;
- 请求流中的字段段带有 Required Insert Count 和 Base,描述动态表依赖及索引基准。
例如,解码器目前已收到 5 次插入,而某字段段的 Required Insert Count 为 6,它必须等第 6 次插入才能解码。另一个只引用静态表的字段段可以继续处理。这是 QPACK 依赖造成的等待,不能归因于 QUIC 把所有流重新排成了一队。
SETTINGS_QPACK_BLOCKED_STREAMS 由解码端宣告,限制对端编码器可以让多少条流处于潜在阻塞状态,默认值为 0。编码器根据已确认的插入进度选择引用,也可改用静态表或字面值;额度耗尽时不能继续制造新的阻塞依赖。RFC 9204 §2.1.2
动态表容量由另一项 SETTINGS_QPACK_MAX_TABLE_CAPACITY 限制,也默认是 0。增加容量和允许阻塞的流数可能改善压缩率,但会增加内存和等待风险。每个方向的编码器表与对端解码器表必须一致,只是字段段无需全局按序到达。
4. 用 Alt-Svc 或 HTTPS DNS 记录发现服务
源站可在 HTTP/1.1 或 HTTP/2 响应中发送下面的字段,表示同一主机的 443 端口提供 HTTP/3,缓存有效期为 86400 秒:
Alt-Svc: h3=":443"; ma=86400Alt-Svc 属于 HTTP,不能用 dig 查询所谓的“Alt-Svc DNS 记录”。客户端可以缓存这个替代服务信息,但建立连接时仍须验证原始源站身份;换一个服务地址并不改变 URL 的源站或证书验证对象。RFC 7838 §2.1
DNS 的 SVCB/HTTPS 资源记录则可提供服务绑定、ALPN(例如 h3)、端口和地址提示。安装了支持 HTTPS 记录类型的 dig 后,可用下面的查询观察响应;示例域名不保证发布此记录:
dig example.com HTTPS检查 Answer 部分是否有 HTTPS 记录以及其中的 alpn。空答案不能证明服务不支持 HTTP/3,因为客户端也可能通过 Alt-Svc 发现服务。ipv4hint / ipv6hint 可用于提前尝试连接;客户端仍应查询目标名称的 A/AAAA,已有这些结果时优先使用它们。RFC 9460 §7.3
DNSSEC 负责验证 DNS 数据,DoH/DoT 保护到解析器的传输;二者都不能替代 HTTPS 的源站身份验证。
5. UDP 不通时,回退也要计入延迟
企业网络、防火墙、NAT 或限速规则可能阻断或降低 UDP/443 的可用性。除非应用明确只支持 QUIC,客户端通常还需要保留 TCP 上的 HTTP/2 或 HTTP/1.1 路径。
如果先等 QUIC 长时间失败,再串行建立 TCP,回退本身就会拖慢请求。并行竞速、缓存曾失败的替代服务,以及复用已有连接,都能减少等待,也会带来额外连接尝试。浏览器的具体时间阈值属于实现策略。
观测时同时记录尝试的协议、失败原因、回退耗时和最终协商协议。一次 HTTP 200 只能说明请求成功,不能说明 UDP 路径畅通。
6. 负载均衡必须保住连接归属
源 IP 或 UDP 端口可能因 NAT 重绑定和迁移而变化。只按五元组选择后端,可能把包送到没有连接密钥和流状态的服务器。常见方案包括:
- 在边缘代理或负载均衡器上终止 QUIC,再单独连接上游;
- 根据服务端签发的 CID 解出路由信息,或维护 CID 到后端的映射;
- 在后端间协调连接状态,但密钥、包号、流和计时器状态使其成本很高。
首次 Initial 的目标 CID 由客户端选择,负载均衡器还需要一套初始选路规则。连接建立后,服务端签发的多个 CID 即使轮换,也必须指向同一份状态。对任意 CID 直接做一致性哈希并不能自动保证这一点,后端集合改变时也要考虑旧连接归属。
QUIC-LB 草案给出生成可路由 CID 的算法。这里引用 draft-ietf-quic-load-balancers-21,它是 Internet-Draft;部署时需要核对各实现采用的草案版本和编码格式。QUIC-LB 草案
部署仍须处理 CID 编码的可关联性、密钥轮换、后端排空和 CID 签发/退役。无状态重置(stateless reset)可以让丢失状态的端点通知对端终止连接,但不会把连接恢复到另一台后端;重置令牌也必须受到保护。
7. 在 HTTP 层处理 0-RTT
代理在客户端 TLS 握手完成前转发请求时,必须带上 Early-Data: 1;已有这个字段时也不得删除。若不能确认上游理解它并正确处理 425,网关应等待客户端握手完成后再转发,或直接返回 425。
收到带 Early-Data: 1 的请求且无法安全处理时,服务端必须返回 425 Too Early。等当前这一跳的握手完成,也不能消除前一跳早期数据的重放风险。对早期数据发出的请求,客户端通常应自动重试,但重试不得再用早期数据。这个状态码用于处理早期请求,不应作为任意业务失败的通用重试信号。RFC 8470 §5.2
策略不能只看方法名:
- GET 可能消耗一次性链接、产生计费,或触发不符合方法语义的副作用;
- PUT 在 HTTP 语义上幂等,但重复处理仍可能增加审计或通知开销;
- 早期数据中的认证凭据可能在同一服务上下文中随请求重放;
- 分布式防重放缓存需要权衡一致性、可用性和跨实例共享成本。
对重要的状态修改请求,默认等待握手完成更容易控制风险。确实需要早期数据时,应明确幂等键、重放窗口、凭据范围和重复响应语义,且避免把“有幂等键”误认为所有副作用都已去重。
8. 用协议输出和日志验证运行情况
下面的命令适用于带 HTTP/3 后端的 curl;--http3-only 需要 curl 7.88.0 或更高版本。将 example.com 换成自己有权测试、已配置 HTTP/3 的主机。抓包还需要本机相应权限,并应在自己的测试环境中执行:
# 确认版本和 Features 行是否包含 HTTP3
curl --version
# 尝试 HTTP/3,同时允许回退;输出实际使用的版本
curl -v --http3 --max-time 15 -o /dev/null \
-w 'http_version=%{http_version}\n' https://example.com/
# 禁止回退,用来单独检查 QUIC 路径
curl -v --http3-only --max-time 15 -o /dev/null \
-w 'http_version=%{http_version}\n' https://example.com/
# 观察测试期间的 UDP/443;QUIC 也可以使用其他端口
tcpdump -ni any -c 20 'udp port 443'http_version=3 表示这次传输用了 HTTP/3;--http3 返回其他版本可能只是回退成功。--http3-only 失败时,应区分客户端缺少 HTTP/3 支持、DNS 或证书错误、服务端配置和 UDP 路径故障,记录 curl、后端库版本及详细错误。
这些命令验证连接和协议选择,不构成性能基准,也不保证出现 0-RTT。不同 curl 版本的竞速行为应查对应文档。curl HTTP/3 文档、curl --http3-only
QUIC 的应用载荷被加密,头部也有部分保护;只抓包无法直接读取 HTTP 字段。TCP 上的 HTTPS 同样隐藏应用内容,不能把 TCP 等同于明文 HTTP。排查需要结合:
- 应用访问日志与请求追踪;
- QUIC 指标:握手结果、0-RTT 接受/拒绝、PTO/丢包、RTT、拥塞窗口与 pacing、流量阻塞、迁移和路径验证;
- 实现支持的 qlog;
- 授权环境中的 TLS 密钥日志与 Wireshark,密钥日志按敏感凭据保管;
- 可选的 spin bit 等被动信号,不能假设所有实现或连接都启用。
9. 容量与拒绝服务边界
QUIC/TLS 握手会消耗服务端 CPU 和状态。部署时需要联合检查:
- 地址验证与 Retry 策略;
- 按 IP、前缀和全局实施的握手限速;
- 向未验证地址发送时的三倍字节限制;
- 连接、流、缓冲和 QPACK 表容量上限;
- 无状态重置令牌及密钥的保护;
- 证书链大小和握手数据的分包;
- UDP 接收缓冲、批量处理、GRO/GSO 和工作线程亲和性;
- TCP 上 HTTP/2 或 HTTP/1.1 的回退容量。
开放 UDP/443 之后,还应测试 NAT 超时、CID 路由、路径 MTU、限速和故障回退。可以先在测试网络阻断 UDP,检查 TCP 回退是否及时,再恢复 UDP 并确认最终协议确实回到 HTTP/3;保留相同请求和超时设置,才能比较两次结果。
10. 验收问题
- HTTP/3 帧、QUIC STREAM 帧和 QUIC 包的边界为什么不同?
- 已收到 5 次 QPACK 插入,却收到 Required Insert Count 为 6 的字段段,接下来要等待什么?
- Alt-Svc 与 HTTPS DNS 记录分别由哪个协议提供?空的 HTTPS 查询答案能否排除 HTTP/3?
- 首次 Initial、CID 轮换和后端排空分别给负载均衡带来什么要求?
--http3成功但输出版本 2,为什么不能证明 QUIC 路径可用?--http3-only失败时,应先收集哪些客户端和网络证据?- HTTP 425 应如何重试,为什么不能再次使用早期数据?