Nginx作为反向代理回源时,日志与抓包中经常能观察到HTTP/2的身影。与HTTP/1.1时代不同,HTTP/2引入了HPACK这一专用的头部压缩算法,其中动态表是理解整个机制的关键。很多人把它和gzip、brotli混为一谈,实际上它们解决的是完全不同层面的问题。本文从原理、对比和Nginx实战三个角度展开分析。

HPACK压缩的底层原理:静态表、动态表与哈夫曼编码
HPACK(RFC 7541)由三部分组成。第一部分是静态表,它是协议内置的固定索引表,包含61个最常见的头字段与常见取值,比如索引2对应:method: GET,索引16对应accept-encoding: gzip, deflate。如果请求头恰好命中静态表,传输时只需要一个字节甚至半个字节。
第二部分是动态表,这是本文的重点。动态表是每条HTTP/2连接两端各自维护的先进先出索引结构。第一次传输某个头部(例如几百字节的User-Agent或长Cookie)时,发送方把它插入动态表并分配索引;后续同一连接上再次出现相同头部,只需发送索引号,接收方根据自己维护的同一份表还原完整内容。动态表默认上限4096字节,可通过SETTINGS帧协商调整。典型的编码过程如下:
# HPACK头部块示意(十六进制) # 0xBE = 10111110,最高位1表示使用动态表索引 # 索引62,对应动态表中第一次插入的 User-Agent 条目 0xBE # 命中动态表,无需传输完整值 # 未命中时发送增量索引指令并插入动态表: # 0x40 | len(name) | name | len(value) | value
第三部分是哈夫曼编码,对未命中索引的字段名和值做字符级压缩,平均节省15%左右。三者叠加后,HTTP/1.1下约700到800字节的请求头,在HTTP/2长连接的第二次请求后往往能压到几十字节。需要特别注意,动态表是有状态的,连接一旦断开即清空,这也解释了为什么短连接下HPACK收益有限,只有长连接复用才能最大化效果。
与gzip、brotli的对比:压缩对象和适用边界完全不同
gzip和brotli是面向响应正文的通用压缩算法,HPACK是面向二进制帧头的专用压缩,两者不在同一层级,不能互相替代。下表从多维度对比:
| 维度 | HPACK | gzip | brotli |
|---|---|---|---|
| 压缩对象 | HTTP/2头部 | 响应正文 | 响应正文 |
| 状态依赖 | 连接内动态表 | 无状态流式 | 无状态流式 |
| 压缩率 | 重复头部可省90%以上 | 中等 | 比gzip高15%到25% |
| CPU开销 | 极低,查表为主 | 中等 | 压缩时高于gzip |
| 安全风险 | 曾受CRIME类时序攻击影响 | BREACH风险 | 同样需防BREASH类攻击 |
| 协商方式 | HTTP/2自动启用 | Accept-Encoding | Accept-Encoding: br |
从压缩率看,brotli内置针对Web文本的预置字典,对HTML、CSS、JS这类小文件表现明显优于gzip;HPACK针对高频重复的头部,收益随连接复用次数递增。从安全角度看,任何基于历史数据的压缩与加密通道组合,都可能被CRIME、BREACH类攻击利用,因此HPACK禁止跨连接共享状态,HTTP/2也明确禁用了TLS层压缩。
另一个容易混淆的点是QPACK。HTTP/3底层换成QUIC后无法保证头部块与数据流的严格顺序,HPACK不再适用,派生出的QPACK增加了确认机制,允许乱序编码。简单记忆:HTTP/2用HPACK,HTTP/3用QPACK。
Nginx回源场景下的配置实践与问题排查
Nginx与上游默认通过HTTP/1.1通信(proxy_http_version 1.1),此时回源流量不涉及HPACK。当使用grpc_pass或作为gRPC代理时,回源走HTTP/2,HPACK自动生效,无法手动开关。配置示例:
upstream backend {
server 127.0.0.1:8080;
# 长连接让动态表持续积累收益
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
listen 443 ssl http2;
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json;
# brotli需单独编译动态模块
# brotli on;
# brotli_comp_level 5;
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 避免双重压缩:上游已压缩的内容不要再处理
proxy_set_header Accept-Encoding "";
}
}排查回源日志有几个技巧。第一,用log_format记录$http2与$upstream_addr等变量,确认客户端侧与回源侧各自使用的协议版本。第二,抓包分析时,Wireshark需要TLS会话密钥才能解出HTTP/2帧,设置SSLKEYLOGFILE环境变量后即可直接查看HEADERS帧的HPACK编码明细,包括动态表插入和索引命中情况。第三,如果回源头部体积异常大,优先检查proxy_set_header是否透传了客户端的巨型Cookie,在Nginx层裁剪后再转发往往比依赖压缩更直接。
还有一个常见误区:为了让回源享受压缩,有人强行给上游带上Accept-Encoding: gzip,结果上游返回压缩后的二进制正文,Nginx无法再做sub_filter替换。正确做法是根据是否需要修改响应体来决定透传或清空该头部;需要二次处理时,应让上游返回未压缩内容,由Nginx统一压缩,并结合gzip_vary on让CDN正确缓存不同编码版本。
总结一下:HPACK是HTTP/2内建的头部压缩,收益来自连接复用与动态表学习;gzip和brotli作用于响应正文,靠Accept-Encoding协商。三层机制各司其职,理解它们的状态特性、压缩边界与安全隐患,才能在Nginx回源链路上把性能调到合理水平。