导读:本期聚焦于韩兆瑞创作的《Nginx中如何用LRU最近最少使用策略优化HTTP/2 Server Push?》,敬请观看详情。一次性把页面依赖的所有静态资源都通过http2_push推送给客户端,首屏时间反而可能变长。原因在于HTTP/2 Server Push缺乏记忆能力,同一个资源在多次请求中被反复推送,浏览器若已缓存该资源便会发送RST_STREAM取消接收,服务器白做一次磁盘读取和带宽分配。引入推送日记这个结构后,可以用固定容量的缓存表记录近期已经推送过的URI,当容量写满时按LRU最近最少使用算法淘汰最久未使用的记录。Nginx本身没有内置这样的状态存储,但可以借助OpenResty的lua_shared_dict共享内存字典实现,该字典底层使用红黑树加LRU链表管理数据。本文给出完整的Nginx与Lua配置示例,解释如何在access阶段动态生成Link响应头触发推送,并讨论容量、过期时间、多进程一致性对性能的影响。

HTTP/2 Server Push允许服务器在客户端请求主文档之前主动推送关联资源,理论上能减少一次RTT。但实际部署中,静态配置http2_push往往导致同一资源在短时间内被反复推送,尤其是在AJAX请求或单页应用路由切换场景下。下文将分析这一问题的根源,并利用LRU最近最少使用算法构建一个有限容量的推送日记来缓解重复推送。

Nginx中如何用LRU最近最少使用策略优化HTTP/2 Server Push?

HTTP/2 Server Push在Nginx中的工作方式

启用HTTP/2后,Nginx可以通过http2_push指令在响应主文档时主动向客户端发送PUSH_PROMISE帧。这个帧告知客户端服务器即将推送哪些资源,客户端可以选择接受或拒绝。如果客户端已经缓存过该资源,通常会发送RST_STREAM帧取消推送,从而节省流量。但取消操作发生在服务器已经开始读取并准备发送数据之后,因此服务器端的磁盘IO、内存拷贝和带宽分配仍然发生了。

最基本的配置方式是在location上下文中直接列出要推送的资源,例如下面的静态配置。

server {
    listen 443 ssl http2;
    server_name ippipp.com;

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    location / {
        http2_push /css/main.css;
        http2_push /js/app.js;
    }
}

这种方式的缺点是推送列表是写死的,服务器无法判断客户端是否已经拥有这些资源。只要进入该location,推送就会发生。对于访问频繁的接口或动态请求,每次都会触发相同的推送动作。Nginx也支持通过Link响应头配合http2_push_preload指令实现动态推送,但依然无法记录哪些资源在最近一段时间内已经被推送过。

推送日记的意义与LRU算法引入

推送日记可以理解为一张记录近期已推送URI的表,用来回答一个问题:这个资源在最近的某个时间窗口内是否已经推送过?如果已经推送过,就跳过本次推送,避免重复PUSH_PROMISE帧带来的开销。由于服务器内存有限,不可能无限期保存所有推送记录,必须使用一种淘汰策略来管理这张表。LRU最近最少使用算法恰好符合这个场景:每次访问某个URI时把它移动到队首,当容量达到上限时淘汰队尾最久没有被访问的记录。

LRU的实现可以基于双向链表加哈希表,也可以借助现成的共享内存字典。OpenResty的lua_shared_dict提供了类似memcached的接口,并在内存不足时自动执行LRU淘汰。这个特性非常契合推送日记的需求:我们只需要关心最近的推送历史,久远的记录即使被淘汰也不会影响当前用户体验。

local push_diary = ngx.shared.push_diary
local uri = ngx.var.uri
if not push_diary:get(uri) then
    push_diary:set(uri, true, 60)
    return true
end
return false

上面的代码展示了用共享字典记录URI的基本逻辑,ttl设置为60秒。超过60秒后记录自动过期,即使没有触发LRU淘汰也会被清理。这种方式既控制了内存占用,又能避免短时间内同一连接的重复推送。需要注意的是,共享字典的LRU淘汰是针对整个字典的,不同location之间会竞争同一块内存空间。

基于OpenResty的动态推送与LRU实现

要让Nginx在运行时根据推送日记动态决定是否推送,需要把逻辑放在请求处理阶段。OpenResty允许在access_by_lua_block中检查共享字典,并根据结果设置Link响应头。配置http2_push_preload on之后,Nginx会解析响应头中的Link字段,对带有rel=preload的资源执行HTTP/2 Server Push。这样推送决策就完全由Lua脚本控制。

http {
    lua_shared_dict push_diary 10m;
    
    server {
        listen 443 ssl http2;
        server_name ippipp.com;
        
        ssl_certificate     /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;
        
        http2_push_preload on;
        
        location /app {
            access_by_lua_block {
                local diary = ngx.shared.push_diary
                local uri = ngx.var.uri
                local push_key = "app_" .. uri
                if not diary:get(push_key) then
                    ngx.header["Link"] = "</assets/app.js>; rel=preload; as=script"
                    diary:set(push_key, true, 30)
                end
            }
        }
    }
}

这段配置中,共享字典容量设置为10MB,每个worker进程共享同一块内存区域。access_by_lua_block会在请求进入location后、上游服务响应前执行。如果push_key不存在,就设置Link头并写入字典,ttl为30秒。30秒内相同URI的请求再次到来时,字典中存在记录,就不会重复推送。当字典容量写满时,lua_shared_dict会自动淘汰最久未访问的条目,保证内存不会无限增长。

这种实现的一个明显好处是推送策略可以按业务逻辑细分。例如可以为不同类型的资源设置不同的ttl,或者根据URI路径前缀决定是否推送。也可以把推送资源列表本身也放入共享字典,避免在Lua代码中硬编码。不过要注意,access阶段设置响应头后,后续的header_filter阶段不能覆盖Link头,否则推送不会生效。

参数调优与常见问题

共享字典的大小直接影响LRU淘汰的频率。设置过小会导致有效的推送记录被过早淘汰,重复推送问题重新出现;设置过大则会占用更多内存,而且在多worker进程下共享内存的锁竞争会变得更明显。通常可以从5MB开始测试,观察共享字典的命中率和内存占用,再逐步调整。ttl的选择需要结合静态资源的更新频率,如果资源更新频繁,过长的ttl会导致客户端错过新版本推送。

多进程环境下,所有worker访问同一块共享内存字典,内部通过互斥锁保证原子性。虽然锁的粒度较细,但在高并发场景下仍然可能成为瓶颈。如果推送日记的读写操作非常频繁,可以考虑使用多个字典分片,降低锁竞争。另外,lua_shared_dict的set操作是原子的,但get和set之间存在竞态窗口,高并发时可能有两个请求同时判断为未推送并同时设置Link头。对于推送场景,这种重复并不是致命问题,因为HTTP/2层会处理重复推送的取消逻辑,但若想严格避免,可以使用add方法替代get加set的组合。

还有一种替代方案是使用纯Nginx的map指令配合日志记录,在log阶段把推送信息写入文件,再通过外部程序定期解析并生成新的配置。但这种方式引入了文件IO和配置重载,实时性较差。相比之下,OpenResty的共享字典方案在实时性、代码复杂度和性能之间取得了更好的平衡。如果无法使用OpenResty,也可以考虑用Nginx的proxy_cache机制模拟LRU行为,但需要额外的缓存规则设计。

最终需要强调的是,HTTP/2 Server Push并不是越多越好。过度的推送会消耗客户端带宽,甚至影响主文档的加载。结合LRU最近最少使用策略后,推送日记能够帮助服务器做出更精准的推送决策,让HTTP/2的多路复用优势真正转化为首屏性能的提升。

Nginx HTTP/2 PushLRU缓存Server Push优化修改时间:2026-08-23 06:05:54

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。