第07章 HTTP/2、gRPC 与 HTTP/3(QUIC) 代理的挑战
学习目标
- 理解 HTTP/2 的多路复用为何让代理"不能再简单 splice",必须理解帧层
- 搞懂代理 gRPC 的三个坑:trailers、长连接复用、流式不能缓冲
- 理解 HTTP/3 = QUIC 给代理出的新题:UDP、连接迁移、加密程度、MASQUE
- 会用
curl --http2/--http3、openssl -alpn观察协议协商
前置知识
- 第04章 HTTP 代理协议、第05章 TLS 代理(ALPN)
- 第02章 网络层级(L4 splice vs L7 解析)
原理
从 HTTP/1.1 说起:代理为什么曾经很简单
HTTP/1.1 是文本协议、一条连接一次一个请求(pipelining 实际已死)。所以 L7 代理处理它很直白:读一个请求 → 转发 → 读一个响应 → 转回。一条 TCP 连接就对应一条请求流,第21章 那种"解析+转发"模型够用。
HTTP/2 和 HTTP/3 打破了"一连接一请求"的假设,代理的复杂度陡增。
HTTP/2 给代理的三道题
HTTP/2 是二进制、多路复用协议:一条 TCP 连接上并发跑多个 stream,每个 stream 是一个独立的请求/响应,数据被切成 frame 交错传输。
一条 TCP 连接
┌──────────────────────────────────┐
│ [stream1帧][stream3帧][stream1帧] │ 多个请求的帧交错在一条连接上
│ [stream5帧][stream1帧][stream3帧] │
└──────────────────────────────────┘
题①:不能再盲转字节。 L4 splice 看到的是一堆交错的二进制帧,无法按请求路由。L7 的 HTTP/2 代理必须解析帧层,把帧重组成一个个 stream,才能按请求做路由/改写。这就是为什么 HTTP/2 反向代理(Nginx、Envoy)比 HTTP/1 代理重得多。
题②:HPACK 头压缩要重算。 HTTP/2 用 HPACK 压缩头部,且压缩状态在一条连接内是有状态的。代理一旦改了头,就得在自己这一侧重新维护 HPACK 上下文、重新编码——不能简单透传压缩后的字节。
题③:连接复用语义变了。 客户端↔代理可能是 h2(多路复用),代理↔后端可能是 h1(多条连接)。代理要做 h2↔h1 的"翻译":把一条 h2 连接上的 N 个 stream,映射到后端的 N 条 h1 连接(或一条 h2)。这种协议降级/升级是反向代理的常见配置。
Client ══ h2(1条连接, N个stream) ══▶ [代理解帧] ── h1(N条连接) ──▶ 后端
gRPC = HTTP/2 + 三个额外要求
gRPC 跑在 HTTP/2 之上,但对代理有 HTTP/2 之外的硬要求:
- 必须支持 trailers(尾部头):gRPC 的状态码
grpc-status放在 响应的 trailer(响应体之后),不是普通响应头。代理若不转发 trailers,客户端永远收不到调用结果状态 → 表现为 gRPC 调用挂起或报错。 - 不能缓冲、必须流式:gRPC 有双向流式(client/server streaming)。代理若像处理普通响应那样"缓冲完整个 body 再转"(第03章 的 buffering),流式 RPC 就会卡死。必须配
proxy_buffering off类选项。 - 长连接复用:gRPC 在一条 h2 连接上持续跑多个调用。代理若过早关连接或不正确复用,性能崩塌。
结论:代理 gRPC 要选原生支持 h2 + trailers + 流式的代理。Envoy 是 gRPC 代理的事实标准(第15章),Nginx 需
grpc_pass且版本够新,老配置常踩 trailers 的坑。
HTTP/3 = QUIC:代理的范式转变
HTTP/3 把传输层从 TCP 换成了 QUIC(跑在 UDP 上 + 内置 TLS 1.3)。这对代理是范式级冲击:
变化①:从 TCP 到 UDP。 代理/负载均衡几十年的积累都是围绕 TCP 的(连接跟踪、splice、四元组哈希)。QUIC 是 UDP,这些大多要重写。很多 L4 LB 对 QUIC 的支持仍不成熟。
变化②:多路复用下沉到 QUIC,无队头阻塞。 HTTP/2 的多路复用在 TCP 之上,一个丢包阻塞所有 stream(TCP 队头阻塞)。QUIC 在传输层就做了独立 stream,丢包只影响单个 stream。代理要理解 QUIC stream 才能做 L7。
变化③:连接迁移(Connection Migration)。 QUIC 连接由 Connection ID 标识,而非 IP+端口四元组。客户端从 WiFi 切到 4G、IP 变了,连接不断——靠 Connection ID 续上。这要求负载均衡器按 Connection ID 路由,不能再按四元组哈希,否则迁移后的包会被分到错误的后端 → 连接中断。
客户端 WiFi(IP_A) ──QUIC, CID=abc──▶ [LB 按 CID=abc 路由] ──▶ 后端X
客户端 切4G(IP_B) ──QUIC, CID=abc──▶ [LB 必须仍按 CID 路由到] ──▶ 后端X ✅ 不断连
(若按四元组哈希 → 路由到后端Y → 断!)
变化④:加密程度更高。 QUIC 把更多控制信息也加密了,代理能"偷看"的更少,SNI 嗅探类技巧进一步受限(叠加 第05章 的 ECH)。
变化⑤:代理 UDP/QUIC 的新标准 MASQUE。 传统 HTTP 代理用 CONNECT 隧道 TCP;要隧道 UDP/QUIC,需要新机制——MASQUE 工作组的 CONNECT-UDP(RFC 9298)让 HTTP 代理能转发 UDP 数据报,是"用 HTTP/3 代理 HTTP/3"的基础。
协议协商:ALPN 与 Alt-Svc
- ALPN:TLS 握手时,客户端在 ClientHello 里列出支持的协议(
h2、http/1.1),服务器/代理选一个。代理做 TLS 终止时,它和客户端 ALPN 协商的结果,决定了客户端这侧用 h1 还是 h2——与后端那侧可以不同。 - Alt-Svc:HTTP/3 不能在 TLS(TCP)里用 ALPN 协商(因为 h3 在 UDP 上)。服务器先用 h1/h2 响应,附带
Alt-Svc: h3=":443"头,告诉客户端"我也支持 h3,下次走 UDP 443",客户端据此升级。代理要正确转发或改写Alt-Svc。
一张表:三代协议对代理的要求
| 维度 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC/UDP |
| 多路复用 | ❌(一连接一请求) | ✅(TCP 之上,有队头阻塞) | ✅(QUIC 层,无队头阻塞) |
| 代理可否 splice | ✅(隧道时) | ❌ 须解帧 | ❌ 须解 QUIC |
| 连接标识 | 四元组 | 四元组 | Connection ID |
| 头压缩 | 无 | HPACK | QPACK |
| 代理难度 | 低 | 中 | 高 |
| 代理 UDP 的隧道 | — | — | MASQUE/CONNECT-UDP |
️ 实现 / 命令
实验一:看 ALPN 协商结果
# 看服务器/代理通过 ALPN 选了什么协议
openssl s_client -connect example.com:443 -alpn h2,http/1.1 </dev/null 2>/dev/null \
| grep -i "ALPN"
# ALPN protocol: h2 ← 协商成了 HTTP/2
实验二:强制 curl 走 h2 / h3 经过代理
# HTTP/2 经代理(CONNECT 隧道后端到端 h2,代理只盲转——透传模式没问题)
curl --http2 -x http://127.0.0.1:3128 https://example.com/ -s -o /dev/null -w "%{http_version}\n"
# 2
# HTTP/3(需 curl 编译了 HTTP3 支持;走 UDP,传统 HTTP 代理无法 CONNECT 隧道它)
curl --http3 https://example.com/ -s -o /dev/null -w "%{http_version}\n"
# 3
关键观察:HTTP 正向代理用
CONNECT透传 h2/h3 是可以的(盲转密文,第05章);但要在 L7理解并路由 h2/h3,代理就必须原生支持对应协议栈。
实验三:Envoy 做 gRPC 反向代理(最小配置片段)
# Envoy 对 gRPC(h2 + trailers + 流式)原生友好
http_filters:
- name: envoy.filters.http.router
route_config:
virtual_hosts:
- name: grpc_svc
domains: ["*"]
routes:
- match: { prefix: "/", grpc: {} } # 显式匹配 gRPC
route:
cluster: grpc_backend
timeout: 0s # 流式 RPC 不能设响应超时
clusters:
- name: grpc_backend
http2_protocol_options: {} # 关键:对后端也用 h2
http2_protocol_options: {} 让 Envoy 对后端讲 h2,timeout: 0s 避免流式被超时切断——这两条正是 gRPC 代理的命门。
实验四:用 grpcurl 验证 trailers 是否被正确转发
# 经代理调 gRPC,若 trailers 丢失会卡住或报 "missing grpc-status"
grpcurl -plaintext -proxy http://127.0.0.1:8080 grpc.example.com:443 list
# 正常返回服务列表 = trailers 通了
排错
| 现象 | 根因 | 解决 |
|---|---|---|
gRPC 调用挂起 / missing grpc-status | 代理没转发 trailers | 换支持 trailers 的代理(Envoy)或升级 Nginx + grpc_pass |
| 流式 gRPC 卡死、收不到流 | 代理缓冲了响应体 | 关缓冲:proxy_buffering off / Envoy 默认流式 |
| h2 客户端经代理变成 h1 | 代理只支持 h1 或 ALPN 没配 h2 | 代理侧开启 h2、配 ALPN |
| HTTP/3 经传统代理不通 | h3 在 UDP,CONNECT 隧道不了 | 用支持 MASQUE 的代理,或回落 h2 |
| QUIC 连接迁移后断连 | LB 按四元组哈希而非 Connection ID | LB 启用 QUIC-aware(按 CID 路由) |
Alt-Svc 不生效、客户端不升级 h3 | 代理吞掉了 Alt-Svc 头 | 确保代理透传/正确改写 Alt-Svc |
本章小结
- HTTP/2 的多路复用让代理不能再盲转,必须解帧、重组 stream、重算 HPACK;还要做 h2↔h1 翻译。
- gRPC 在 h2 上加了三条硬要求:转发 trailers、流式不缓冲、长连接复用,Envoy 是事实标准。
- HTTP/3 = QUIC(UDP+TLS1.3):UDP 化、无队头阻塞、按 Connection ID 路由(连接迁移)、加密更彻底、需 MASQUE 隧道 UDP。
- ALPN 协商 h1/h2,Alt-Svc 通告 h3;代理两侧协议可不同。
下一章 第08章,我们回到客户端侧:浏览器/系统/程序到底怎么"知道"该用哪个代理——PAC、WPAD、系统代理与 NO_PROXY 的全部门道与坑。