当 Nginx 作为反向代理以 HTTP/2 向后端回源时,头部压缩不再是简单的文本传输,而是依赖 HPACK 动态表进行状态化压缩。动态表的索引变化会直接反映在抓包和日志工具记录的头部信息中,例如某些头部字段消失、顺序发生改变,甚至是同一个请求在不同连接上解码结果不同。这些问题本质上与 Nginx 配置无关,也不涉及 RFC 9380 所描述的椭圆曲线哈希。本文从回源场景出发,说明 HPACK 动态表的工作原理、对日志的影响,并厘清 RFC 编号的常见误解。

Nginx 回源 HTTP/2 中 HPACK 动态表的作用
Nginx 默认与上游通信使用 HTTP/1.1,这种模式下头部以明文形式发送,日志和抓包直接能看到完整的头部字段。但如果将 proxy_http_version 设置为 2.0,并且上游服务器支持 HTTP/2,那么 Nginx 与后端之间的请求头会经过 HPACK 压缩。HPACK 的压缩并不是类似 gzip 的独立压缩,而是一种状态化压缩:发送方和接收方各自维护一份动态表,表里存放已经出现过的头部名称和值。发送方可以用表索引来代替完整的头部字段,从而减少字节数。这意味着头部信息在两个层面存在:压缩前的语义层和压缩后的编码层。
动态表初始为空,随着请求的发送,某些头部会被插入。例如第一个请求带有自定义头 X-Request-ID: abc123,发送方会将其加入动态表,赋予一个索引号,后续请求如果包含相同的头名和值,就只发送这个索引号。动态表的大小由 SETTINGS_HEADER_TABLE_SIZE 协商决定,默认值通常是 4096 字节。Nginx 作为客户端回源时,这个参数可能与通用浏览器不同,需要在上游配置中确认。如果表空间不够,旧条目会被驱逐,新条目继续插入。这种更新是连接级别的,也就是说同一条 HTTP/2 连接上的不同请求共享动态表,而不同连接之间的表完全独立。
动态表对回源的主要价值在于连接复用时降低重复头部的开销。Nginx 的 keepalive 连接池保持与后端的长连接,后续请求可以复用动态表,从而减少带宽和 CPU 消耗。但代价是抓包和日志分析变得复杂。HPACK 编码后的报文里,头部块可能只有一串索引号和若干编码字段,如果日志系统没有在 Nginx 层解码,而是直接从网络包提取,就会看到这些原始索引而不是真正的头部。
日志中 HPACK 动态表引发的典型现象
在实际排障中,一个常见现象是:从 HTTP/2 帧抓包看,请求头似乎只有几个数字,或者某些头部字段完全消失。例如使用 Wireshark 直接查看 HTTP/2 HEADERS 帧时,如果 HPACK 解码上下文不完整,部分头部不会显示出来,而是以索引形式存在。这并不代表请求头真的缺失。Nginx 在处理这些请求时,能够根据动态表还原出完整的头部,因此它的 access log 可以正常记录 Host、User-Agent 等字段。但如果某个日志插件直接从网络抓包提取头部,就可能记录不完整,尤其是在同一个连接上后面的请求中,动态表已经被多次更新。
另一个典型问题是日志顺序与实际发送顺序不一致。HTTP/2 HPACK 编码允许不按原始顺序发送头部,解码后头部顺序也可能不同于文本协议。对于强依赖顺序的下游分析程序,这会导致误判。例如安全审计工具希望检查特定 Cookie 是否出现在请求中,使用 HTTP/1.1 时可以直接匹配文本,但 HTTP/2 下 Cookie 可能被索引化,字段值也可能被 Huffman 编码。正确的做法是在 Nginx 层进行解码后记录日志,而不是从网络上自行还原。
如果回源使用 TLS 加密,抓包还需要先解密 TLS 才能看到 HTTP/2 明文帧。TLS 解密通常需要浏览器或客户端的 SSLKEYLOGFILE,对于 Nginx 回源这种服务端到服务端的连接,配置难度更高。此时更可行的方案是使用 Nginx 的 debug 日志或开启 HTTP/2 相关调试,让 Nginx 输出解码后的头部信息。下面的命令可以配合抓包工具快速过滤 HTTP/2 流:
tshark -r capture.pcapng -Y "http2.type == 1" -T fields \ -e http2.headers
注意该命令依赖 tshark 对 HTTP/2 解码,如果动态表状态不完整,输出可能仍是索引。
RFC 9380 与 HPACK 的混淆从何而来
HPACK 的正式规范是 RFC 7541,它定义了头部压缩的静态表、动态表、索引类型和 Huffman 编码。而 RFC 9380 的标题是“Hashing to Elliptic Curves”,描述的是把任意字符串哈希到椭圆曲线上的通用方法。两者在协议栈中处于完全不同的层次:RFC 7541 服务于 HTTP/2 头部压缩,RFC 9380 服务于密码学中的椭圆曲线运算,例如某些 TLS 1.3 密码套件、身份基加密或盲签名系统。把 RFC 9380 当作 HPACK 的文档编号,通常是因为 RFC 编号记忆混乱,或者搜索时被同类的“HTTP/2”与“Hashing”关键词干扰。
虽然 RFC 9380 不直接定义 HPACK,但在 Nginx 回源 HTTP/2 的完整链路中,它可能间接出现。如果 Nginx 与后端之间使用 HTTPS 回源,TLS 握手阶段可能采用支持椭圆曲线密码的套件,而某些套件的参数生成参考了 RFC 9380 的哈希到曲线方法。但这不影响 HPACK 动态表的维护和头部日志记录,两者没有必要同时讨论。排障时如果发现头部乱码或索引异常,应优先检查 RFC 7541 相关的 HPACK 动态表大小配置,而不是去翻阅 RFC 9380。
最后给出一个 Nginx 回源 HTTP/2 的配置示例,帮助在测试环境复现动态表日志问题。该配置让 Nginx 以 HTTP/2 向后端 127.0.0.1:8443 回源,并启用连接复用。实际环境中需要保证后端已经正确支持 h2 或者 h2c。
upstream backend_h2 {
server 127.0.0.1:8443;
keepalive 16;
}
server {
listen 443 ssl;
http2 on;
server_name example.test;
location / {
proxy_pass https://backend_h2;
proxy_http_version 2.0;
proxy_set_header Connection "";
proxy_set_header Host $host;
}
}
配置完成后,可以对比 access log 中的头部记录和 tcpdump 抓包结果。日志中完整的头部说明 Nginx 已经完成 HPACK 解码;抓包中的索引形态说明动态表正在发挥作用。理解这一点,就能够在日志分析时避免被 RFC 编号和压缩编码误导。