广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何设计Nginx的http2_push_diary_cleanup清理策略避免推送记录膨胀?

HTTP/2 Server Push 允许服务器在客户端请求主资源时,主动把关联的 CSS、JavaScript 或字体文件一并推送过去,省去后续往返。但这个机制也带来一个容易被忽视的问题:Nginx 需要记录哪些资源已经推送过,避免同一连接内重复推送;这些记录在有些实现中被称为 push diary。如果清理不及时,diary 会随着并发连接数和推送次数线性增长,最终反映在共享内存占用和 CPU 扫描开销上。下面从清理策略的角度来拆解。

如何设计Nginx的http2_push_diary_cleanup清理策略避免推送记录膨胀?

一、push diary 的产生与存储形态

在 Nginx 中启用 HTTP/2 推送通常依靠 http2_push 指令或者响应头里的 Link 预加载字段。比如在配置里写 http2_push /css/main.css;,Nginx 就会在主请求响应前把该 CSS 文件推给客户端。为了知道哪些资源已经推送过,避免在同一个 HTTP/2 连接里重复推送,内部会维护一份推送状态记录。这份记录可以理解为 push diary,它可能以共享内存中的哈希表形式存在,也可能以磁盘上的状态文件形式保存。

共享内存实现的优点是查询快,但大小通常受限于 http2_push_diary_size 这类参数,一旦写满就需要淘汰旧条目;而磁盘文件实现则更直观,但会带来额外的 IO 开销。无论哪种存储形态,如果清理策略缺失,记录都会变得臃肿。例如一个高流量站点每天可能有数十万次推送,每个条目哪怕只占几十字节,累积一个月也会达到几百 MB 的量级,这在内存紧张的场景下会直接影响 Nginx worker 进程的稳定性。

更重要的是,diary 中的过期条目还会干扰新连接的推送决策。比如某个资源已经不再被页面引用,但旧记录仍然存在,Nginx 可能继续尝试推送该资源,白白浪费带宽。因此,清理策略不仅要解决空间问题,还要保证推送列表的时效性。

二、基于 logrotate 的滚动清理策略

对于磁盘上的 push diary 文件,最简单的清理方式是接入 logrotate。假设 Nginx 将每次推送事件写入 /var/log/nginx/http2_push.log,我们可以为这个文件单独配置轮转规则。与常规的 access log 不同,push diary 通常不需要长期保留,保留 3 到 7 天就足够排查问题。

下面是一段 logrotate 配置示例:

/var/log/nginx/http2_push.log {
    daily
    rotate 3
    missingok
    notifempty
    compress
    delaycompress
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

这段配置会让 http2_push.log 每天轮转一次,只保留最近 3 份压缩后的日志。设置 notifempty 可以避免空文件被反复轮转,compress 和 delaycompress 则能减少磁盘占用。轮转后通过 kill -USR1 通知 Nginx 重新打开日志文件,这里使用了反引号而不是单引号,注意在部分 shell 环境下的兼容性。

如果 push diary 并非写入独立日志文件,而是记录在 access log 的自定义字段中,那么就需要在 log_format 里单独定义 push_diary 变量,再把 access log 的轮转策略和 push diary 分开处理。否则混在一起会增加日志分析难度,也会让清理策略变得笨重。

三、用 shell 脚本实现按大小和时间的双重清理

对于共享内存或状态文件中的 push diary,logrotate 就无能为力了,这时需要配合一个定时执行的清理脚本。脚本可以同时检查状态文件的大小和最后修改时间,只要满足任一条件就触发清理或重置。比如当状态文件超过 50 MB,或者超过 7 天没有更新,就清空其中过期的条目。

下面是一个示例脚本,它可以用来清理指定目录下的 *.diary 文件:

#!/bin/bash
DIARY_DIR=/var/cache/nginx/http2_push
MAX_SIZE=50M
MAX_AGE=7
# 按大小清理
find "$DIARY_DIR" -type f -name "*.diary" -size +$MAX_SIZE -delete
# 按时间清理
find "$DIARY_DIR" -type f -name "*.diary" -mtime +$MAX_AGE -delete
# 清理空目录
find "$DIARY_DIR" -type d -empty -delete

脚本里的 -size +$MAX_SIZE 表示只匹配大于 50 MB 的文件,-mtime +$MAX_AGE 表示最后修改时间超过 7 天。把这两条命令放进 crontab,比如每天凌晨 3 点执行一次,就能防止 diary 文件无限增长。需要注意的是,直接 -delete 操作不会经过回收站,如果文件正在被 Nginx 写入,删除后可能造成 inode 残留,最好先通过 lsof 检查占用情况,或者给 Nginx 发信号让它暂停写入。

另外,清理脚本还可以增加一个安全阈值:当可用内存低于设定值时,主动通知 Nginx 清理共享内存中的 diary。这需要 Nginx 暴露相应的控制接口,例如通过 ngx_http_v2_module 的共享内存管理功能,但原生模块并不提供直接清理指令,通常需要配合第三方模块或自定义编译。

四、清理策略对性能的影响与监控建议

清理策略并不是越频繁越好。如果每次请求都检查 diary 文件的大小或时间,会增加额外的系统调用;如果清理动作本身需要暂停 Nginx worker 或者锁定共享内存,那么在清理瞬间可能会造成请求延迟抖动。理想的做法是异步清理,比如在 worker 进程空闲时执行,或者把清理任务交给独立的低优先级进程。

监控方面,建议至少关注三个指标:diary 文件大小、清理耗时、以及清理触发频率。可以用 prometheus 配合 node_exporter 采集文件大小,用脚本执行时间记录清理耗时。如果发现清理耗时超过了几百毫秒,或者每天触发次数异常增多,就需要重新评估阈值设置。

最后还要注意,清理策略必须和推送列表的更新机制联动。如果只是简单删除旧记录,但 Nginx 内部仍然认为这些资源需要推送,那么清理反而会破坏缓存命中率。解决方法是在清理前先重建推送列表,或者使用带版本号的 diary 文件,让 Nginx 自动忽略旧版本记录。这样既能控制空间,又能保证 HTTP/2 Server Push 的准确性和效率。

广告位 · 文章底部(后台-模块管理替换此区域)