HTTP/2 Server Push 是一个看起来很美好、实际落地却充满争议的特性。Nginx 从 1.13.9 版本开始支持 http2_push 指令,可以主动把 CSS、JS 等资源推送到客户端。但推送出去的资源到底有没有被浏览器真正使用?有多少推送被 RST_STREAM 取消了?这些问题如果没有数据支撑,优化就无从谈起。本文给出一条完整的链路:从 Nginx 记录 push 相关日志,到解析入库 TimescaleDB,再到按时序维度统计命中率。

一、让 Nginx 记录 HTTP/2 Push 的行为数据
Nginx 原生日志里并没有一个直接叫 push_status 的字段,但可以通过 $http2 变量判断连接是否跑在 HTTP/2 上,结合访问日志中的 $request、$status、$time_iso8601 等变量,把推送资源和主请求关联起来。更关键的是,Nginx 的 push 模块在客户端发送 RST_STREAM 拒绝推送时,会在错误日志中留下痕迹,我们可以同时采集 access log 和 error log,用统一的请求标识做拼接。
一个实用的做法是自定义日志格式,把推送标记打进日志。例如通过 map 把需要推送的 URI 前缀识别出来,然后在日志中输出一个额外字段,方便后续解析时区分主文档请求和被推送的子资源请求。
http {
map $uri $is_push_resource {
default 0;
~*^/static/.*\.(css|js)$ 1;
}
log_format push_log '$time_iso8601|$remote_addr|$http2|'
'$request_method|$uri|$status|'
'$body_bytes_sent|$request_time|$is_push_resource';
access_log /var/log/nginx/push_access.log push_log;
}这样每条日志都带有管道符分隔的固定字段,比默认的 combined 格式好解析得多。同时建议在 error log 中开启 info 级别,Nginx 会在推送被拒时输出相应的流错误信息,这是判断 push 是否被客户端取消的重要信号来源。
二、采集与解析:把日志变成结构化的时序数据
日志落盘之后,需要一个采集器把它送进 TimescaleDB。常见选择有两种:用 Filebeat 或 Vector 把日志转发到 Kafka 再由消费者写入,或者直接写一个简单的 Python 消费脚本轮询日志文件。对于规模不大的集群,一个基于 tail -f 思路的 Python 脚本就足够了,实现简单且方便自定义清洗逻辑。
import psycopg2
from datetime import datetime
def parse_line(line):
parts = line.strip().split('|')
if len(parts) != 9:
return None
ts = datetime.fromisoformat(parts[0])
return {
'ts': ts,
'client_ip': parts[1],
'http2': parts[2] or '-',
'method': parts[3],
'uri': parts[4].split(' ')[1] if ' ' in parts[4] else parts[4],
'status': int(parts[5]),
'bytes_sent': int(parts[6]),
'request_time': float(parts[7]),
'is_push': int(parts[8]),
}
def insert(conn, rec):
sql = """INSERT INTO push_metrics
(ts, client_ip, http2, uri, status, bytes_sent, request_time, is_push)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"""
with conn.cursor() as cur:
cur.execute(sql, (rec['ts'], rec['client_ip'], rec['http2'],
rec['uri'], rec['status'], rec['bytes_sent'],
rec['request_time'], rec['is_push']))
conn.commit()这里有两个细节值得注意。一是时间戳必须用 $time_iso8601 而不是 $time_local,前者带时区信息,可以直接被 fromisoformat 解析,避免容器环境下时区错乱。二是批量写入时建议攒够 500 条或每秒提交一次,单条 insert 会对 TimescaleDB 造成不必要的连接开销。
三、TimescaleDB 建表、hypertable 与连续聚合
TimescaleDB 的核心优势是针对时序数据的自动分片和保留策略。把 push_metrics 表转成 hypertable 后,数据会按时间自动切分成 chunk,查询近期数据时只需扫描少量分片,写入性能也比普通 PostgreSQL 表高出一个量级。
CREATE TABLE push_metrics (
ts TIMESTAMPTZ NOT NULL,
client_ip INET,
http2 TEXT,
uri TEXT,
status SMALLINT,
bytes_sent BIGINT,
request_time REAL,
is_push SMALLINT DEFAULT 0
);
SELECT create_hypertable('push_metrics', 'ts',
chunk_time_interval => INTERVAL '1 day');
-- 7 天后自动清理原始数据,节省磁盘
SELECT add_retention_policy('push_metrics', INTERVAL '7 days');
-- 按 5 分钟粒度做连续聚合,统计推送量与命中率
CREATE MATERIALIZED VIEW push_metrics_5m
WITH (timescaledb.continuous) AS
SELECT
time_bucket('5 minutes', ts) AS bucket,
uri,
count(*) FILTER (WHERE is_push = 1) AS push_count,
count(*) FILTER (WHERE is_push = 1 AND status = 200) AS push_hit,
avg(request_time) AS avg_rt
FROM push_metrics
GROUP BY bucket, uri
WITH NO DATA;
SELECT add_continuous_aggregate_policy('push_metrics_5m',
start_offset => INTERVAL '1 hour',
end_offset => INTERVAL '5 minutes',
schedule_interval => INTERVAL '5 minutes');FILTER 子句在这里非常好用,一条聚合查询就能同时算出推送总量和有效命中量,两者相除就是推送命中率。连续聚合的查询结果已经物化,Grafana 直接对着 push_metrics_5m 建面板即可,不需要每次都扫描原始日志表。
保留策略的设置要和聚合策略配合好:原始数据只留 7 天,但聚合视图默认不受 retention 影响,可以单独为聚合表设置更长的保留期,比如 90 天,这样既能看实时数据,也能回溯几个月的趋势变化。
四、从数据看 Push 的真实效果与常见坑
数据积累一段时间后,往往会得出和直觉相反的结论。Chrome 和 Firefox 早已支持 Cache-Digest 思路下的推送取消机制,浏览器如果本地已有缓存,会立刻发送 RST_STREAM 拒绝推送,此时带宽已经浪费在推送头部数据上了。如果日志中推送取消比例长期偏高,说明推送清单过于激进,应当缩小 http2_push 的资源范围,或者改用 Link 响应头按需触发。
另一个常见的坑是 HTTP/2 Push 在 Chrome 中的支持已被移除,推送出去的帧会被直接忽略。因此在做数据看板时,建议按 User-Agent 维度再做一层拆分,统计不同浏览器内核下的命中率差异,避免被整体平均数误导。如果数据最终证明 Push 在你的场景收益有限,那么把这套监控链路直接复用到 HTTP/3 或 preload 分析上,投资也不会浪费。
整体来看,Nginx 负责产生带上下文的日志,采集脚本负责清洗,TimescaleDB 负责存储与时序聚合,三者各司其职,链路清晰且组件都可替换。真正的价值不在工具本身,而在于让每一次推送决策都有数据可依,把协议层的黑盒变成可以持续优化的白盒。
NginxTimescaleDBhttp2_push修改时间:2026-09-14 01:38:50