HTTP/2 的头部压缩依赖 HPACK 算法,而 HPACK 中的动态表是两端共同维护的上下文。Nginx 回源时如果与源站的动态表状态不一致,就会在日志里留下类似解压失败或头部非法的记录。这类问题有时和 RFC 9800 描述的动态表生命周期管理直接相关,本文从日志入手梳理排查路径。

一、HPACK 动态表与回源长连接是怎么耦合的
HPACK 把常用头部放到一张表里,直接用索引代替完整字段。静态表是规范预设的 61 项,动态表从索引 62 开始由连接两端动态添加。Nginx 回源时如果使用 HTTP/2,那么 Nginx 与源站之间的连接会复用,动态表会随着请求和响应不断增长。源站每次发送 HEADERS、PUSH_PROMISE 等帧时,可能携带动态表更新,Nginx 收到后必须同步更新自己的解码器。反过来 Nginx 发送请求头部时,源站也依赖同样的同步过程。
问题经常出在连接被复用但状态没有重置。例如 Nginx 使用连接池中的旧连接回源,而源站认为该连接已经收到 SETTINGS 或 GOAWAY,之前的动态表条目可能已经被驱逐。这时 Nginx 发出去的头部位索引还引用旧表项,源站解压失败;或者源站返回的头部引用了 Nginx 不存在的动态表索引,Nginx 在 error log 中记录解压失败。RFC 9800 对这类连接复用、表容量变化时的状态处理提出了更明确的要求,排查时可以作为判断依据。
二、Nginx 日志里哪些字段能定位 HPACK 异常
默认的 combined 格式不会暴露 HTTP/2 回源细节。回源 HPACK 解压失败通常不会直接计入 access log 的 status,而是进入 error log。Nginx error log 中常见的记录是 upstream sent invalid header while reading response header from upstream,或者 http2 frame parser 之类的消息。要看到更细的信息,需要把 error_log 级别调到 debug 或者至少 info,并配合自定义 access log 记录 upstream 连接协议、头部状态等。
可以定义一个新的 log_format,把 upstream 侧的 HTTP 版本、连接复用情况、请求和响应头部大小等信息打出来。下面是一段 Nginx 配置示例,回源强制使用 HTTP/2,同时输出 upstream_http2 变量和自定义头部状态。
http {
log_format hpck_debug '$remote_addr - [$time_local] "$request" '
'status=$status bytes=$body_bytes_sent '
'upstream_http2=$upstream_http2 '
'upstream_header_status=$upstream_header_status '
'upstream_connect_time=$upstream_connect_time';
access_log /var/log/nginx/hpck-access.log hpck_debug;
server {
listen 443 ssl http2;
server_name proxy.ipipp.com;
location / {
proxy_http_version 2.0;
proxy_pass https://origin.ipipp.com;
proxy_set_header Connection "";
access_log /var/log/nginx/hpck-access.log hpck_debug;
}
}
}
当 access log 中的 upstream_http2 显示为空或状态异常,同时 error log 出现 invalid header,基本就能把问题锁定在 HTTP/2 头部解压链路。然后再结合抓包确认帧级别的动态表更新是否完整。
三、RFC 9800 对动态表状态有哪些补充约束
RFC 9800 主要关注 HPACK 动态表的生命周期管理和 SETTINGS 变化后的状态恢复。传统 HPACK 实现中,如果一端发送 SETTINGS_HEADER_TABLE_SIZE 调整容量,另一端需要按照新容量驱逐表项,但许多实现并未严格遵守,导致两端表大小出现偏差。RFC 9800 强调连接建立、连接复用以及 SETTINGS 帧交换过程中,动态表必须被确定性地重置或保持,任何隐含的状态保留都应当明确化。
在 Nginx 回源场景中,源站可能通过 SETTINGS 把 header_table_size 调小,Nginx 如果忽略了这个调整,仍然引用已经超出新容量的索引,源站会直接关闭连接或返回错误。RFC 9800 给出的建议是接收方应立即按照新大小淘汰旧条目,且不能保留超出容量的引用。对于反向代理来说,这意味着 Nginx 与每个上游的连接都必须独立维护 HPACK 上下文,不能在连接被回收或重用时沿用旧状态。若日志中看到调用链中某一段总是在连接建立后第二个请求失败,很可能就是动态表状态沿用导致的。
另一个容易忽略的点是,HTTP/2 连接升级或从 h2c 切换到 h2 时,动态表可能被保留。RFC 9800 要求协议切换时明确清空动态表,除非两端协商了保留机制。Nginx 日志如果能记录切换前后的 upstream_http2 值变化,就能验证这一点。
四、回源 HPACK 压缩异常的修复与验证
定位到问题后,可以按顺序处理。先确认 Nginx 和源站的 HTTP/2 版本以及 SETTINGS 协商结果,尤其 header_table_size 是否一致。Nginx 从 1.25.1 开始支持上游 HTTP/2 的逐步完善,旧版本可能存在动态表处理缺陷。升级到稳定版通常能解决一部分索引越界问题。
如果源站无法升级,可以在 Nginx 侧减少动态表压力。例如取消回源长连接复用,通过在 location 中设置 proxy_http_version 2.0 的同时,利用 upstream keepalive 但把 keepalive_requests 调小,强制定期重建连接和表状态。另一个方案是降低回源请求头复杂度,减少使用自定义头部或大体积 Cookie,这样动态表条目增长慢,出错概率低。下面给出一个使用 keepalive 但限制复用次数的配置片段:
upstream origin_pool {
server origin.ipipp.com:443;
keepalive 16;
keepalive_requests 100;
}
server {
location / {
proxy_http_version 2.0;
proxy_pass https://origin_pool;
}
}
改完后继续观察 access log 中的 upstream_header_status,并用 tcpdump 抓取回源连接上的 HEADERS 和 SETTINGS 帧,确认动态表更新队列是否完整发出和确认。若 error log 不再出现 invalid dynamic table index 或解压失败类记录,说明 RFC 9800 相关的状态对齐已经生效。