2957 字
约 9 分钟
18
S02-X07 · HTTP 版本演进:从 1.1 到 HTTP/3

一个页面首次打开走几次连接、一条连接上并行几个请求、丢一个包之后还有多少请求跟着等——这三件事分别由 HTTP 版本、连接模型与传输层决定。HTTP/1.1、HTTP/2、HTTP/3 的差别不是"快慢档位",而是每一版都在拆掉上一版的一处结构性瓶颈。这条因果链理清了,才知道自己的链路该停在哪一版。


1. 每个版本解决前一版的什么问题

版本 上一版的痛点 本版的解法 带来的新代价
HTTP/1.0 每个请求一条 TCP,握手与慢启动反复付 持久连接成为默认(Connection: keep-alive) 空闲连接长期占用,服务端要管超时
HTTP/1.1 请求只能一个接一个发出 管线化(pipelining)+ Host 头 + 分块传输 响应必须按请求顺序返回,应用层仍串行
HTTP/2 并发靠开多条 TCP,握手成本高、头部重复传输 单连接多路复用 + HPACK 头部压缩 所有流共享一个 TCP 字节流,丢包互相牵连
HTTP/3 TCP 的字节流有序语义造成的跨流阻塞 把可靠传输搬到 QUIC(基于 UDP),按流独立重传 需要 UDP 可达的链路,中间设备兼容性问题

关键判断:换版本解决的是"并发调度"问题,不是"带宽"问题。带宽受限的链路换到 HTTP/3 也不会有质变。


2. HTTP/1.1:队头阻塞发生在应用层

管线化是 HTTP/1.1 定义的能力:客户端不必等前一个响应就能连续发出下一个请求。但 RFC 7230 规定服务器必须按请求到达的顺序返回响应,于是第一个响应慢,后面的响应全在客户端排队。这层阻塞属于 HTTP 消息语义层的串行,与 TCP 无关——调内核参数对它无效。

$ curl -sv --http1.1 https://TARGET/ -o /dev/null 2>&1 \
    | grep -E "Connected|Re-using|ALPN"

浏览器选择绕开而不是修复:对同一域名开若干条并发 TCP 连接换并行度(条数按各浏览器实现核对,典型量级是个位数)。副作用有两个——域名分片与首条连接被慢资源占死。管线化在现代浏览器里默认关闭。

阻塞层 HTTP/1.1 的表现 调参能解决吗
应用层(消息顺序) 前一个响应慢,后续请求等待 不能,只能加连接或换版本
连接层(TCP 重传) 丢包时该连接上一切等待 不能,TCP 语义决定
服务器并发(worker/backlog) 新连接排队或被拒 能,调 worker 数与 backlog

3. HTTP/2:流、帧、HPACK,以及推送的退场

HTTP/2 把连接切成帧,帧头带 stream id,属于不同流的帧可以在同一连接上交替发送。接收端按 stream id 重组,于是请求之间不再互相排队——应用层的队头阻塞被消除。

概念 作用 关键约束
流 Stream 一次请求/响应对应的逻辑通道 id 单向递增,客户端发起奇数号
帧 Frame 传输最小单位(HEADERS / DATA / SETTINGS / WINDOW_UPDATE / RST_STREAM / PING) 受 SETTINGS_MAX_FRAME_SIZE 限制
HPACK 头部压缩:静态表 + 动态表 + 霍夫曼编码 动态表是有状态的,同一连接内必须按序解码
流控 连接级与流级双层窗口 只约束 DATA 帧,不约束 HEADERS

HPACK 的动态表让头部压缩跨请求生效,代价是同一连接内 HEADERS 帧必须顺序解码——h2 里一个不太被提及的串行点,量级远小于 1.1 的消息级串行。

服务端推送在 HTTP/2 里定义过,但主流浏览器已陆续移除实现。原因不是难做,而是服务端无法准确预测客户端缓存状态:推过去的东西可能已有,白占带宽。替代组合是 103 Early Hints 加 Link: rel=preload。

# nginx 1.25.1 起,http2 从 listen 参数提升为独立指令
server {
    listen 443 ssl;
    http2 on;
    server_name DOMAIN;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate     /etc/nginx/cert/APP.crt;
    ssl_certificate_key /etc/nginx/cert/APP.key;
}

4. TCP 层队头阻塞:HTTP/3 的直接动因

多路复用把多个请求塞进一条 TCP 连接,这在丢包场景下变成缺点。TCP 保证字节流严格有序交付:任一段丢失,内核会把其后已到达的段留在接收缓冲区不交给应用。对 HTTP/2 而言这些段属于所有流,于是全部流一起等重传。

# 构造丢包环境,观察 h2 多路复用的退化
$ tc qdisc add dev eth0 root netem loss 1%
$ curl -sv --http2 https://TARGET/big.js -o /dev/null 2>&1 | grep -c -i retrans
$ tc qdisc del dev eth0 root

所以 HTTP/2 的收益有条件:丢包率低时明显,丢包率上升时可能还不如多连接的 HTTP/1.1。这正是 HTTP/3 的理由——它不是"更快的 HTTP",而是把可靠传输从内核 TCP 搬出来,改成按流独立重传。

现象 HTTP/2 over TCP HTTP/3 over QUIC
丢一个包 所有流一起等待重传 只有该流的数据等待
连接建立 TCP 握手 + TLS 握手(合计 2~3 RTT) 传输与 TLS 握手合并(新连接 1 RTT,复用可 0 RTT)
换网络(WiFi→蜂窝) 四元组变化,连接必须重建 连接 ID 不变,连接可迁移
中间设备可见性 明文头部与握手 除少数字段外全加密

5. QUIC 的四个关键变化

变化 具体做法 换来的能力
基于 UDP 在用户态实现可靠传输、流控、拥塞控制 升级不受内核与中间设备僵化影响
连接 ID 不再用四元组标识连接 连接迁移,切网不断流
1-RTT / 0-RTT TLS 1.3 握手并入传输握手 复用连接时首个往返即可携带数据
TLS 1.3 内建 加密是协议本体的一部分 头部也被加密,可观测性下降

UDP 的现实约束要单独记住:部分运营商与企业网络会对 UDP 443 限速或直接丢弃。此时 HTTP/3 不报错,而是静默回退到 HTTP/2,表现就是"配了没生效"。


6. 0-RTT 的重放风险与适用边界

0-RTT 允许客户端在第一个往返就带上应用数据,代价是这些数据不具备前向安全性,也没有抗重放保证——录下来就能重发。

请求类型 是否适合 0-RTT 理由
幂等 GET(取静态资源、查列表) 适合 重放一次结果相同
带副作用的 POST(下单、扣款、发消息) 不适合 重放等于重复执行
强一致查询 不适合 可能读到旧连接状态下的结果
内网低价值探活 适合 失败重试代价低

服务端必须自己做幂等(幂等键 + 去重表),不能把这件事交给传输层。


7. 各版本的启用与降级链路

# 同端口上 HTTP/1.1 与 HTTP/2 由 ALPN 协商,HTTP/3 另开 UDP
server {
    listen 443 quic reuseport;
    listen 443 ssl;
    http2 on;
    ssl_early_data on;                     # 允许 0-RTT
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}

# 后端(Tomcat 蓝绿其一)走明文 HTTP/1.1,回源段不折腾 h2c
location /api/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

浏览器的降级顺序固定:读 Alt-Svc 缓存 → 尝试 HTTP/3 → UDP 不通或握手超时回落 HTTP/2 → 再回落 HTTP/1.1。所以"第一次访问不走 h3"是设计如此,不是配错了。

Spring Boot 侧:

# 由底层容器(Tomcat 的 Http2Protocol)承担,需要 TLS 才能协商 h2
server.http2.enabled=true
server.ssl.enabled=true
server.port=8443
组件 开启方式 备注
nginx 1.25.1+ http2 on; 独立指令 旧的 listen ... http2 写法已弃用并告警
nginx 1.25 之前 listen ... ssl http2; 必须同时有 ssl
Spring Boot 3.x(Tomcat) server.http2.enabled=true 明文 h2c 需额外 up-grade 配置,实践里少用
curl 验证 curl -sv --http2 看 ALPN, server accepted to use h2
浏览器验证 DevTools 的 Protocol 列 显示 h2 / h3,这是唯一可信的结论来源

8. 什么时候反而不该上 HTTP/2

场景 建议 原因
内网批量拉取大量小文件 保持 HTTP/1.1 帧头与 HPACK 状态机对极小响应是净开销
需要逐条抓包看请求 保持 HTTP/1.1 h2 头部被压缩、多流交织,可读性差
链路中间有老旧 HTTP 解析设备 保持 HTTP/1.1 部分代理无法正确转发 h2 帧
公网多静态资源的页面 上 h2/h3 并发与头部压缩收益最明显的地方

9. 常见坑

坑 现象 正解
以为上了 h2 就一定快 小站点测不出差异 收益来自并发与头部压缩,串行场景无感
上了 h2 仍做域名分片 连接变多、HPACK 动态表被拆散 资源合并到同一域名,让多路复用真正起作用
UDP 被限导致 h3 静默回退 "配了没效果" 看 DevTools 的 Protocol 列,不看配置
http2 on; 写进老版本 nginx unknown directive "http2" 1.25.1 之前用 listen 443 ssl http2;
只有 h2 没有 Alt-Svc 浏览器永不尝试 h3 补响应头,并确认 UDP 443 可达
0-RTT 用在写接口 偶发重复提交 0-RTT 只留给幂等请求,写接口上幂等键
回源也强行开 h2c 容器兼容性与调试成本上升 回源保持 HTTP/1.1,收益在前端接入层

10. 边界:哪些问题压根不该从版本入手

问题 该看的东西 不该做
页面整体慢 资源数量、体积、瀑布图的串行段 先换版本
偶发超时 抓包看重传率与 RTT 抖动 归因到 HTTP 版本
首屏慢但复访快 缓存策略与 Cache-Control 归因到多路复用
回源延迟高 后端接口耗时、连接池 给回源段强上 h3
是否上 h3 用户网络中 UDP 443 的实际可用率 因为"新"就上

一句话:并发瓶颈找 h2,丢包链路找 h3,头部体积看 HPACK 与会话复用,剩下的多半不在版本上。


11. 自测

  1. HTTP/1.1 的队头阻塞发生在哪一层?为什么调内核参数解决不了?
  2. HPACK 的动态表为什么要求同一连接内的 HEADERS 帧按序解码?
  3. HTTP/2 的多路复用为什么会在丢包时退化成"所有流一起等"?
  4. QUIC 的连接 ID 解决了什么问题,它和四元组的关系是什么?
  5. 0-RTT 为什么只能用于幂等请求?服务端还需要做什么补充?
  6. nginx 1.25.1 之后 http2 指令的位置变了,旧写法会有什么表现?
S02-X07 · HTTP 版本演进:从 1.1 到 HTTP/3
https://www.clxhxhhr.top/posts/5899/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。