在排查Nginx回源链路问题时,很多人习惯用tcpdump抓包看HTTP/2回源流量,结果发现Wireshark里解出来的请求头是一堆无法阅读的十六进制字节,完全不像HTTP/1.1那样直观。其实这些字节并不是加密数据,而是HPACK头部压缩算法的输出。RFC7541把这套算法规定为HTTP/2的标准头部编码方式,而在HTTP/3时代,其演进版本QPACK又被收录为RFC9204,RFC9910则属于HTTP/3核心协议文档之一,理解HPACK的工作方式对读懂这些后续标准同样有帮助。

HPACK到底压缩了什么:静态表、动态表与哈夫曼编码
HPACK的核心思路是把重复出现的头部字段用整数索引替代。它包含一张61项的静态表,比如:method: GET是索引2,:path: /是索引4,host相关字段也各有编号。如果一个头部字段和静态表中的某一项完全一致,编码方只需发送一个字节的索引值即可,接收方查表还原,压缩率极高。抓包时你看到的形如0x82 0x87的字节,本质就是静态表索引2和索引7的变长整数编码。
静态表覆盖不了的字段,比如Nginx回源时带的X-Forwarded-For、X-Real-IP这类自定义头,或者具体域名不属于静态表里的示例值,就会进入动态表。动态表是连接级别的FIFO结构:每发送一个新头部,就插入表头位置并获得一个新的索引(从62开始编号),当动态表占用字节数超过上限时,从表尾逐出最旧的条目。这就带来一个关键特性:同一个头部字段第二次出现时可能只需一个索引字节,但如果两条流交错发送,双方对动态表状态的推导必须严格一致,一旦出现编码错误就会导致整个连接的状态错乱。
第三层压缩是哈夫曼编码。头部字段名和值中的字符串如果哈夫曼编码后更短,就会被打上H标志并用哈夫曼码流传输。抓包中那些既不是索引也查不到明文的字符串,大概率就是哈夫曼编码的结果。Wireshark自带的HTTP/2解析器可以自动完成这三层解码,前提是它能从连接建立的第一个SETTINGS帧开始跟踪,中途抓包就会因为丢失动态表上下文而无法还原,这是乱码问题最常见的成因。
Nginx回源场景下的实践配置与抓包定位
先说一个容易被误解的点:Nginx的listen ... http2只影响客户端到Nginx这一跳,Nginx回源到上游默认用的是HTTP/1.1。要让回源走HTTP/2,从1.13.5版本起需要显式配置:
upstream backend_h2 {
server 10.0.0.100:8443;
# 保持长连接,避免反复建连带来的动态表重建
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.ipipp.com;
location / {
proxy_pass https://backend_h2;
proxy_http_version 1.1; # 回源仍为HTTP/1.1(默认)
proxy_set_header Connection "";
# 若上游支持,可改用下面的方式走HTTP/2回源
# 需要 nginx 编译时带上 --with-http_v2_module
# proxy_ssl_protocols TLSv1.2 TLSv1.3;
# proxy_ssl_server_name on;
}
}Nginx对上游走HTTP/2长期依赖第三方模块(如ngx_http_v2_proxy相关的补丁或gRPC场景下的grpc_pass),直到较新版本才逐步完善原生支持。如果你用grpc_pass回源gRPC服务,走的同样是HTTP/2,此时抓包看到的就是标准HPACK编码的头部。无论哪种方式,抓包都要从连接建立开始,完整捕获TCP三次握手、TLS握手以及HTTP/2的SETTINGS帧,动态表的初始状态正是从SETTINGS_HEADER_TABLE_SIZE协商出来的。
一个实用技巧是调整动态表大小来辅助分析。HPACK允许发送方主动缩小动态表上限(通过动态表大小更新指令),把SETTINGS_HEADER_TABLE_SIZE设为0后,所有头部都必须明文传输,代价是失去压缩收益。在测试环境这样配置,抓包立刻恢复可读性,非常适合对比验证。生产环境则不建议这么做,因为回源链路上的请求头往往携带大量固定字段,HPACK能显著降低带宽占用。
从HPACK到QPACK:RFC9910视角下的演进与排障思维
HPACK有一个设计约束:头部压缩严格依赖传输层的顺序性,因为TCP保证字节有序,收发双方对动态表状态的推导天然一致。但HTTP/3基于QUIC,QUIC的多路复用允许流乱序到达,如果沿用HPACK,一个流的丢包会阻塞所有流对动态表的更新理解。于是QPACK在RFC9204中被设计出来,引入编码流和解码流两条单向流,配合插入计数和阻塞控制,让头部解码不再被乱序流卡死。而RFC9910(HTTP/3的正式标准文档)与这套机制配合,共同构成了下一代协议栈的头部处理基础。
理解这条演进脉络对排障很有价值。遇到Nginx回源HTTP/2流量解析异常时,建议按顺序排查:第一,确认抓包起点是否在连接建立之前,缺失动态表上下文是乱码首因;第二,检查TLS密钥是否可用,没有密钥时Wireshark连TLS解密都做不了,更谈不上HPACK解码;第三,对比curl --http2直连上游与经过Nginx回源的头部差异,确认是否是代理层追加的字段导致动态表行为变化。下面这段命令可以在测试环境快速验证:
# 完整抓包,从连接建立开始,包含TLS握手 tcpdump -i eth0 -w h2.pcap host 10.0.0.100 and port 8443 # 让 curl 输出HTTP/2帧级别的调试信息 curl -v --http2 -o /dev/null https://10.0.0.100:8443/ # Wireshark中指定TLS密钥日志文件后解码 # 设置环境变量 SSLKEYLOGFILE=/path/keys.log
最后补充一点:如果你观察到的乱码出现在TLS记录层内部且无论如何都解不开,那问题不在HPACK,而是缺少会话密钥。HPACK的乱码只出现在已成功解密的HTTP/2帧中,两者要区分开。把TLS解密、HPACK解码、动态表上下文这三层依次确认,Nginx回源链路上的二进制流量基本都能还原成可读形态,后续无论是排查回源失败还是分析头部透传问题,都会轻松很多。
Nginx回源HTTP/2 HPACKRFC9910修改时间:2026-09-07 09:36:54