HTTP/2 Server Push曾经是Nginx里备受关注的性能优化特性,它允许服务端在响应HTML之前主动把CSS、JS等关键资源推送给浏览器。但不少人在实测中发现一个奇怪现象:同样的页面,第一次访问时资源被推下来了,刷新几次之后推送却消失了,过一会儿又恢复了。这个现象的根源,在于Nginx内部维护的一个推送记录结构——push diary,以及它容量满了之后的淘汰策略。理解这套机制,是正确使用http2_push指令、避免带宽浪费的前提。

push diary到底是什么,它为什么存在
push diary可以理解为Nginx为每个HTTP/2连接维护的一份推送历史账本。它存在的目的只有一个:防止对同一个资源重复推送。HTTP/2协议本身不会替服务端做这件事,如果浏览器已经缓存了某个CSS文件,服务端仍然一股脑地推送,客户端只能发送RST_STREAM帧拒绝,这不仅浪费了已经发出的带宽,还占用了并发流配额,严重时反而比不开Push更慢。
在Nginx的实现中,每当服务端通过http2_push指令推送一个资源,就会把这个资源的标识(URL经过哈希处理后的指纹)记录到diary里。下一次同一个连接上再遇到推送请求时,Nginx会先查一下这本账:如果指纹已经存在,就跳过推送。这本质上是一种基于哈希指纹的去重机制,和浏览器端的Cache Digest思路一脉相承,只是它记录的是服务端视角的推送历史,而不是客户端的缓存状态。
需要注意的是,diary是连接级别的,不是全局的。一个HTTP/2连接对应一份diary,连接关闭后记录随之消失。所以同一个用户换一条TCP连接(比如网络切换、Nginx端http2_recv_timeout超时断开),推送又会重新开始,这也是为什么测试时现象看起来时好时坏。
容量上限与随机淘汰策略的工作原理
diary不可能无限大,Nginx为它设定了固定的容量上限,这个上限由编译时的常量控制,大约能容纳64条记录,运行期无法通过配置指令调整。容量这么小是合理的:每个条目只是几十字节的哈希指纹,而且一个连接生命周期内真正会被推送的资源通常不多。
关键问题来了:当diary写满之后又有新资源需要记录时,选哪个旧条目让位?Nginx采用的是随机淘汰策略,也就是说,不按插入时间淘汰最旧的,也不统计访问频率淘汰最冷的,而是随机挑一个槽位直接覆盖。这种做法实现极其简单,不需要维护链表或时间戳,内存开销几乎为零,且在条目均匀分布的假设下,每个指纹被淘汰的概率相等,长期来看不会出现系统性的偏差。
但随机淘汰的代价也很明显:它不保证公平。假设你的页面推送了80个资源,而diary只能存64条,那么任意一次新写入都可能覆盖掉一个仍然有效的指纹,导致该资源在下一次请求时被重复推送。最坏情况下,热资源的指纹反复被覆盖,重复推送率会明显高于LRU策略。不过在实践中,单页推送资源数远超64个的场景本身就该反思——推送太多资源说明策略有问题,Nginx官方也一直建议只推送一两个最关键的资源。
还有哈希碰撞的问题。指纹是定长哈希,不同URL理论上可能碰撞,碰撞会让Nginx误以为已经推送过而跳过推送。概率虽低,但如果你观察到某个资源偶尔不推送,除了淘汰,碰撞也是可能的原因之一。
配置示例与实测验证方法
先给出一个典型的Nginx配置。http2_push可以写在location块里,也可以通过响应头Link来动态触发:
server {
listen 443 ssl http2;
server_name example.ipipp.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# 限制并发推送数,防止推送风暴
http2_max_concurrent_pushes 10;
location / {
root /var/www/html;
index index.html;
# 静态写法:无条件推送关键CSS
http2_push /css/critical.css;
}
# 动态写法:后端返回 Link 头时自动推送
location /app/ {
proxy_pass http://127.0.0.1:8080;
http2_push_preload on;
}
}其中http2_push_preload on会让Nginx识别后端响应中的Link: </app.css>; as=style; rel=preload头并转换为推送。动态方式更灵活,推荐由应用层决定推送什么,Nginx只负责执行。
验证推送是否生效,最直接的工具是Chrome的chrome://net-export或者开发者工具的Network面板(勾选Protocol列,看Push状态),也可以用curl调试:
# curl 需要编译时带 nghttp2 支持
curl -sI --http2 https://example.ipipp.com/ \
-H 'Accept: */*' -w '%{http_version}\n'
# 观察推送流,可配合 nghttp 客户端
nghttp -ans https://example.ipipp.com/如果想观察diary的淘汰效果,可以写一个脚本对同一连接连续发起几十次请求(HTTP/2连接复用是前提),统计推送触发的次数。你会发现在资源数超过diary容量后,重复推送开始出现,且出现时机没有规律,这正是随机淘汰的特征。
常见踩坑点与演进方向
第一个坑是盲目推送。push diary只在单连接内去重,浏览器缓存命中时客户端仍会拒绝推送,服务端是感知不到的。所以推送决策应该尽量依赖客户端提示,比如结合cookie判断是否首访,只对新用户推送critical.css,否则RST_STREAM的开销会吃掉你省下的请求延迟。
第二个坑是推送的资源本身很大。HTTP/2的推盛会占用带宽,如果CSS有几百KB,推送反而阻塞了HTML的多路复用,这是协议层面的队头干扰。经验值是推送资源控制在几十KB以内,且数量不超过两三个。
更重要的一点是:Nginx从1.25.1版本起官方移除了HTTP/2 Server Push支持,http2_push等指令已经废弃。官方给出的理由是Chrome等主流浏览器早已禁用了对Push的客户端支持,实际收益趋近于零。如果你在维护老版本Nginx上的Push配置,理解diary机制仍有价值;但新项目应改用Link: rel=preload响应头或103 Early Hints,它们不依赖连接级去重,与浏览器缓存天然协同,是当前更稳妥的首屏资源提示方案。
NginxHTTP/2 Server Pushpush diary修改时间:2026-09-15 06:50:35