导读:本期聚焦于守望者创作的《Nginx HTTP/2推送机制详解:http2_push_diary_iterator迭代器如何实现推送去重》,敬请观看详情。为什么同一个页面刷新多次后,Nginx就不再重复推送资源了?这背后靠的是HTTP/2推送日记机制,而遍历这份日记的核心正是http2_push_diary_iterator迭代器。本文从ngx_http_v2_module模块的源码入手,分析推送日记的哈希存储结构、迭代器的初始化流程与逐项遍历逻辑,并对比简化版与完整版两种实现差异,同时给出基于指针对齐与除法取模的两种哈希计算方式。理解这套机制,能帮助你在配置http2_push指令时避免重复推送带来的带宽浪费,也能在排查推送不生效问题时快速定位到日记检索环节。

HTTP/2 Server Push是Nginx早就支持的特性,通过http2_pushhttp2_push_preload指令,服务端可以在浏览器请求HTML时主动把CSS、JS等资源推下去,省掉一次往返。但推送不是无条件的:如果客户端已经缓存了资源,或者本次连接里已经推过同样的资源,再推一次就是浪费带宽。Nginx用一个叫push diary(推送日记)的结构记录已经推送过的资源指纹,而遍历这份日记、逐项比对的工具,就是http2_push_diary_iterator。这个迭代器定义在src/http/v2/ngx_http_v2.c中,属于ngx_http_v2_module模块的核心数据结构之一。

Nginx HTTP/2推送机制详解:http2_push_diary_iterator迭代器如何实现推送去重

推送日记为什么需要专门的迭代器

先看Nginx源码中迭代器的定义:

typedef struct {
    ngx_http_v2_push_t              *push;
    ngx_queue_t                     *queue;
    ngx_queue_t                      sentinel;
} ngx_http_v2_push_diary_iterator_t;

结构很简洁,三个字段:push指向推送资源对象,queue是当前遍历到的队列节点,sentinel是本地构造的哨兵节点。Nginx的push diary本质上是一个循环双向链表,配合一个64位的位图(ngx_http_v2_node_t里的size与桶数组)实现指纹的快速登记。

之所以不直接用下标去遍历数组,是因为diary的桶数量是可变的。Nginx会根据配置项http2_max_concurrent_pushes以及日记容量的对数值动态分配桶,初始容量从16个条目起步,按2的幂倍增。用队列迭代器而非裸指针数组遍历,可以天然兼容扩容过程中节点被重新链接的情况,遍历逻辑不需要关心桶的数量和分布。

另外一点容易被忽视:迭代器内部的sentinel字段是一个栈上的哨兵。当遍历结束时,队列会被接到这个哨兵上,形成一个临时的空队列,这样后续代码可以用ngx_queue_empty判断遍历是否完成,避免再额外维护一个计数器。这是Nginx源码里很典型的链表哨兵技巧。

迭代器的工作流程与指纹比对逻辑

当Nginx决定推送一个资源时,流程大致是:先根据资源URI计算出哈希指纹,然后初始化迭代器扫描diary,检查该指纹是否已存在。相关源码节选如下:

/* 计算32位指纹(简化示意) */
static uint32_t
ngx_http_v2_push_diary_hash(ngx_str_t *path)
{
    uint32_t  hash;
    /* 经典的Murmur-ish字符串哈希 */
    for (hash = 0; path->len; path->len--) {
        hash = *path->data++ + (hash << 6) + (hash << 16) - hash;
    }
    return hash;
}

/* 初始化迭代器并逐项比对 */
iterator.push = NULL;
ngx_queue_init(&iterator.sentinel);

for (q = ngx_queue_head(&h2c->push_diary);
     q != ngx_queue_sentinel(&h2c->push_diary);
     q = ngx_queue_next(q))
{
    node = ngx_queue_data(q, ngx_http_v2_node_t, queue);
    if (node->id == fingerprint) {
        /* 已推送过,命中,停止遍历 */
        iterator.push = node;
        break;
    }
}

这段代码体现了迭代器的核心价值:遍历是惰性的。每次调用ngx_http_v2_push_diary_next只前进一个节点,一旦指纹命中就立即停止,不必扫完整个链表。对于一个长连接内推送了几十个资源的场景,这种提前退出能明显减少CPU开销。

指纹的计算方式有两种实现。标准Nginx发行版采用指针按位与的方式取模:hash & (entries - 1),利用容量必然是2的幂这一约束,把除法换成位运算。而Cloudfork等衍生版本以及LiteSpeed的实现里,用的是对质数取模的方式:hash % table_size。位运算版本更快,但要求扩容时必须重新哈希所有条目;取模版本在扩容时可以延迟迁移,代价是每次查找多一次除法指令。Nginx选择了前者,配合迭代器在扩容时重建链表,整体性能更优。

实践配置与常见问题排查

理解了diary的去重逻辑,配置推送时的一些怪现象就好解释了。典型配置如下:

server {
    listen 443 ssl http2;
    server_name example ipipp.com;

    # 显式推送
    http2_push /style.css;
    http2_push /app.js;

    # 或者根据Link头自动推送
    http2_push_preload on;
}

最常见的疑问是:为什么抓包看到第一次请求推了资源,第二次刷新就不推了?答案正是push diary在起作用。diary记录的是连接级别的指纹,只要这条HTTP/2连接没断,同样的URI指纹会被迭代器检测到并跳过推送。浏览器复用连接时表现尤其明显。要验证推送行为,建议用nghttp工具配合-v参数,可以清楚看到PUSH_PROMISE帧的发出情况。

另一个坑是http2_push参数里路径写成了绝对URL或带了查询字符串。diary的指纹是基于URI字符串逐字节计算的,/style.css/style.css?v=2会被视为两个不同资源,导致重复推送。此外,如果客户端在SETTINGS帧中把SETTINGS_ENABLE_PUSH设为0,Nginx会在更早的环节直接关闭推送,迭代器根本不会执行,这一点在排查时要先确认,别一上来就怀疑diary逻辑。

从源码角度再补充一点:diary的容量上限由http2_recv_buffer_size等配置间接影响,且Nginx对diary做了64条目的软性约束(超过后会按LRU思路淘汰最老的指纹)。迭代器在淘汰场景下同样适用,因为淘汰本质上是把队头节点摘链,迭代器的queue指针在被摘节点的前一个位置,不会造成悬空访问。理解这套机制后,无论是优化推送策略还是排查推送异常,都能快速定位到正确的环节。

Nginxhttp2_push_diary_iteratorHTTP/2 Server Push修改时间:2026-09-06 04:04:37

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