HTTP/2协议引入服务器推送已经有些年头了,但真正在生产环境用好的案例并不多。在Chrome的net-internals日志中,你可能会看到http2_push、push_diary这样的内部标记,它们对应的是浏览器侧对推送流的接收与去重记录。而在服务端,Nginx通过ngx_http_v2_module模块实现了整套推送逻辑,同时还依赖HPACK算法完成头部压缩与解压。这篇文章就把Nginx的推送机制、push diary去重原理以及HPACK解压缩过程一次性讲透。

Nginx的服务器推送是怎么工作的
Nginx的HTTP/2服务器推送依赖ngx_http_v2_module,这个模块在Nginx以HTTP/2方式运行时自动生效,不需要额外编译开关(前提是编译时带了http_v2_module)。它的核心配置指令有两个:http2_push和http2_push_preload。前者直接在配置中声明要推送的资源,后者则通过识别响应头中的Link字段自动触发推送。
从协议层面看,推送的完整时序是这样的:客户端发起一个对主页面的请求,Nginx在返回HTML之前,先在一个空闲的流上发送PUSH_PROMISE帧,告知客户端“我准备在流ID为X的流上推送资源Y”,随后才真正在新的流上发送推送资源的响应数据。客户端收到PUSH_PROMISE后有两种选择:如果本地缓存里已经有这个资源,可以发送RST_STREAM帧取消推送;如果没有,就安静地接收。这套机制的价值在于省掉了浏览器解析HTML后发现资源引用、再发起请求的往返时间。
一个典型的Nginx配置示例如下:
server {
listen 443 ssl http2;
server_name ippipp.com;
# 方式一:显式声明推送资源
location = /index.html {
http2_push /style.css;
http2_push /app.js;
}
# 方式二:通过Link响应头自动推送
# http2_push_preload on;
# add_header Link "</style.css>; as=style; rel=preload";
}两种方式各有适用场景。显式配置直观可控,但资源路径一旦变化就要改配置;http2_push_preload方式更灵活,可以由后端应用动态生成Link头,Nginx只负责执行推送。需要注意的是,Link头的格式必须严格符合规范,as属性的值错误会导致Nginx拒绝推送。
push diary是什么,为什么需要去重
push diary(推送日志)并不是HTTP/2协议里的正式术语,它是实现层面的一个数据结构,浏览器和服务器各自维护一份。它的作用一句话概括:记录哪些资源已经被推送过或已经被客户端持有,避免重复推送浪费带宽。
具体来说,浏览器为每个HTTP/2连接维护一张push diary表,每收到一个PUSH_PROMISE帧,就把 promised stream 的URL哈希后记录进去。当浏览器自己主动请求某个资源时,会先查这张表,如果发现该资源正在被推送,就不再发起新请求,而是复用推送流的数据。反过来说,如果服务器推送了一个浏览器已经缓存的资源,浏览器会立刻用RST_STREAM拒绝,这时候推送的头部数据其实已经产生,带宽已经消耗,这就是服务器推送最大的风险点:推错了就是纯浪费。
Nginx侧同样有类似的去重逻辑。Nginx会为每个连接记录已经推送过的资源,避免在同一条连接上重复推送同一资源。此外还有一个容易被忽略的细节:Nginx在计算是否推送时,会参考请求头中的Accept、Accept-Encoding等字段。如果推送资源的内容编码与客户端声明的不匹配,推送会被跳过。所以有些时候配置了http2_push却看不到推送流量,排查方向应该放在请求头匹配上,而不是配置语法。
HPACK头部压缩与解压缩的原理
HTTP/2取消了纯文本的头部传输,改用HPACK算法压缩。HPACK的核心是两张表:静态表和动态表。静态表预先定义了61个最常见的头部字段和值组合,比如:method: GET在静态表中就是索引2,传输时只需要一个字节。动态表则是一个先进先出的队列,连接双方把最近使用过的头部字段存进去,后续重复出现时同样用索引代替完整内容。
关键点在于:动态表是双向同步的。服务端压缩时往自己的动态表里插入条目,接收方解压时必须执行完全相同的插入操作,双方的动态表才能保持一致。一旦某个帧丢失或乱序(HTTP/2基于TCP所以不会乱序,但如果中间有代理篡改了头部,就会出现不一致),后续所有解压都会失败,连接只能报COMPRESSION_ERROR并关闭。这也是为什么HTTP/2要求TLS部署时禁用不安全的中问盒配置,任何对头部帧的非法修改都会破坏压缩上下文。
Nginx中与HPACK相关的配置主要是http2_header_buffer_size(部分版本中为http2_max_field_size和http2_max_header_size),它们控制单个头部字段和整个头部块的大小上限。如果业务中有特别长的Cookie或Authorization头,默认值可能不够,表现为请求直接被拒绝,日志中出现相应错误码。调整这些值时要留意内存开销,因为每个连接都要分配对应的缓冲区。
服务器推送到底值不值得用
从实践反馈看,服务器推送的效果并不像当初设想的那么美好。原因主要有三点:一是服务器很难准确知道浏览器的缓存状态,推送已缓存资源的概率不低;二是HTTP/2推送只能对同源同连接的资源生效,跨域的CDN资源推不了;三是Chrome从106版本起已经禁用了HTTP/2推送,理由是实际收益低于维护成本,业界更推荐用rel=preload的103 Early Hints方案替代。
那么Nginx的推送配置还有意义吗?要看你的用户群体。如果流量主要来自仍支持推送的客户端,且你能通过Cookie或service worker准确判断客户端缺哪些资源,定向推送依然能带来首屏提升。否则更稳妥的做法是关闭推送,改用预加载提示。判断标准很简单:观察服务器日志中RST_STREAM的比例,如果被拒绝的推送超过三成,这个功能就是在负优化。
总结一下配置建议:保留ngx_http_v2_module但谨慎使用http2_push,头部缓冲区大小按业务实际情况调整,HPACK相关的错误重点排查中间代理是否改写了头部。理解push diary和HPACK动态表这两个底层机制,能帮你在排查HTTP/2问题时快速定位到底是推送策略的问题,还是压缩上下文损坏的问题。
NginxHTTP/2服务器推送HPACK解压缩修改时间:2026-09-05 00:26:41