HTTP/2协议在提升传输效率方面做了大量工作,头部压缩HPACK就是其中关键的一环。RFC9113作为HTTP/2的权威规范,详细定义了HPACK的编码规则,包括静态表、动态表以及哈夫曼编码三部分。不少运维和开发人员在配置Nginx作为反向代理回源时,会发现一个奇怪的现象:明明客户端发送的请求头很完整,抓包工具或者后端服务的日志里看到的字段却残缺不全,甚至只剩下一些索引数字。这并不是Nginx丢了数据,而是HPACK动态表在起作用。理解这套机制,是排查回源链路问题的前提。

HPACK到底压缩了什么:静态表与动态表的分工
HPACK的核心思想是减少头部字段在连接上的重复传输。它把常见的头部组织成两种结构。静态表是规范预先定义好的61个常见头部字段与取值的组合,比如索引1对应:authority,索引2对应:method: GET,索引16对应accept-encoding: gzip, deflate。这些内容写死在RFC9113的附录里,通信双方不需要协商就能直接用索引引用。
动态表则是每个连接独有的一块先入先出缓冲区,最大尺寸由SETTINGS帧中的SETTINGS_HEADER_TABLE_SIZE协商,默认值是4096字节。当一方发送了一个不在静态表中的头部字段,比如自定义的x-request-id,可以用「添加并引用」的指令把它写入动态表。下一次再发送同样的字段时,只需要一个字节的动态表索引即可,代价远低于传输完整字符串。
动态表还有一个容易被忽略的特性:逐出机制。当新条目加入导致总大小超过上限时,最老的条目会被挤出动态表,此后它对应的索引会整体前移。这意味着同一个索引号在同一条连接的不同时刻,可能指向完全不同的头部字段。抓包工具如果想正确解码HTTP/2流量的头部,必须完整跟踪这条连接从建立开始的每一个HEADERS帧和CONTINUATION帧,中途加入的抓包会出现解码错乱。
Nginx回源时头部是怎么被处理的
Nginx从1.9.5版本开始内置HTTP/2支持,但要注意区分两个方向:监听端口上的listen 443 ssl http2处理的是客户端到Nginx这一段;Nginx到上游的回源连接,则取决于proxy_pass指向的上游是否支持HTTP/2。默认情况下,Nginx回源走的是HTTP/1.1,即使客户端用HTTP/2访问也不例外。这也是很多人疑惑的地方:前端明明是h2,后端日志里却是HTTP/1.1的记录。
如果上游确实通过HTTP/2回源(例如使用了支持gRPC的grpc_pass,或某些定制模块),Nginx发出的请求头会经过HPACK编码。此时Nginx的$http_xxx日志变量取到的是解码后的完整值,日志层面一般不会缺失。真正容易出问题的场景是抓包分析:抓包工具只看到索引号而看不到字段名,或者因为动态表状态不同步而解码出乱码。
另一个常见误区是伪头部(pseudo-header)的处理。HTTP/2把请求行拆成了:method、:scheme、:authority、:path四个伪头部字段,它们以冒号开头,不属于传统HTTP头。如果日志格式里直接打印$http_authority,大概率是空的,因为Nginx在转发时已经把:authority转换成了Host头。排查时应优先确认Nginx层面已经完成了协议转换,而不是在日志格式里硬找伪头部。
如何正确抓包与配置日志定位回源问题
抓包工具的选择很关键。Wireshark从2.x版本起内置HTTP/2解析器,能够跟踪动态表状态并还源头部的原始文本。使用时需要让Wireshark从TCP握手或TLS密钥可用时就开始捕获,否则动态表状态缺失,解码结果不可信。如果流量是HTTPS,需要通过SSLKEYLOGFILE环境变量导出TLS会话密钥,再在Wireshark中加载,才能看到解密后的HEADERS帧内容。
在Nginx日志层面,建议扩展log_format,把协议版本和关键头部都记录下来,便于对比:
# 记录回源协议与头部透传情况
log_format upstream_trace '$remote_addr - [$time_local] "$request" '
'upstream=$upstream_addr proto=$server_protocol '
'status=$status urt=$upstream_response_time '
'xrid=$http_x_request_id host=$host';
access_log /var/log/nginx/access.log upstream_trace;
同时,回源头部透传要显式配置。proxy_set_header指令决定了哪些头部会被传递给上游,默认情况下Nginx会重设Host和Connection等字段。如果自定义头部在回源日志中消失,先检查该头部是否通过了Nginx的合法头部名校验,包含下划线的头部(如x_request_id)默认会被丢弃,需要设置underscores_in_headers on;才能保留。
动态表相关的性能与兼容性建议
HPACK动态表虽然省带宽,但也占内存。每条HTTP/2连接都要维护独立的动态表,长连接、高并发的场景下内存开销不可忽视。RFC9113允许发送方主动将表大小设为0来禁用动态表,某些嵌入式服务端或安全设备就会这样做。Nginx侧可以通过http2_header_buffer_size(旧版本中为http2_recv_buffer_size相关配置的演进,具体以当前版本文档为准)调整处理头部时的缓冲区,遇到超大Cookie或长URI的431错误时可以适当调大。
还有一个实际坑点:客户端与Nginx之间协商的SETTINGS_HEADER_TABLE_SIZE只影响这一跳,不会透传到回源连接。也就是说,前端禁用HPACK动态表不代表回源也没有压缩,两段连接的头部编码状态完全独立。排查问题时务必分段验证:先确认客户端到Nginx的头部完整,再单独抓Nginx到上游的流量。配合error_log的debug级别输出,基本可以覆盖绝大多数回源头部异常的场景。