在构建高并发前端行为分析系统时,日记点击日志的采集往往成为性能短板。用户每次点击都发请求不仅占用连接,还容易因网络抖动丢失数据。将Nginx作为边缘接入层,利用HTTP/2的服务器推送能力配合ClickHouse的列式存储,可以组成一个轻量且可水平扩展的日志管道。Nginx负责接收并格式化日志,ClickHouse专注海量数据的写入与多维聚合,两者结合能显著降低后端压力。

一、Nginx中http2_push的部署与日志格式化原理
Nginx从1.13.9版本开始支持http2_push指令,它允许服务器在响应HTML时主动将指定资源推送给浏览器,而无需等待客户端解析DOM后再请求。对于日记点击采集来说,我们可以把一段极小的埋点脚本通过http2_push预推,使浏览器尽早持有上报逻辑,从而减少首次交互时的延迟。需要注意的是,http2_push仅对启用了HTTP/2的HTTPS连接生效,因此必须正确配置SSL证书并监听443端口。
在Nginx配置中,我们首先要在server块开启http2,然后通过http2_push指定埋点脚本路径。与此同时,访问日志的格式需要定制,以便后续被ClickHouse高效解析。默认combined格式包含多余字段,我们可以定义一个json格式的log_format,将点击相关的uri、args、上游响应时间等输出为单行JSON。这样Filebeat或自研agent抓取后无需复杂正则即可映射为结构化字段。
下面给出一个典型的Nginx配置片段,展示如何开启推送并定义JSON日志:
server {
listen 443 ssl http2;
server_name log.ippipp.com;
ssl_certificate /etc/nginx/ssl/ippipp.com.crt;
ssl_certificate_key /etc/nginx/ssl/ippipp.com.key;
# 预推埋点采集脚本
location = /index.html {
http2_push /static/diary_click.js;
root /usr/share/nginx/html;
}
# 接收前端点击上报的接口
location = /click {
access_log /var/log/nginx/click.log json_click;
return 204;
}
log_format json_click escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"uri":"$uri",'
'"args":"$args",'
'"request_time":$request_time,'
'"http_user_agent":"$http_user_agent"'
'}';
}
上述配置里,escape=json参数保证中文与特殊字符被正确转义,避免生成破损的JSON行。在生产环境中,建议将click日志单独轮转,并控制单文件大小,防止磁盘被快速写满。若推送资源较大,反而会因占用带宽导致首屏变慢,因此diary_click.js应精简到几KB以内,只保留发送navigator.sendBeacon的核心逻辑。
二、浏览器端如何利用推送脚本完成点击上报
当Nginx把diary_click.js通过HTTP/2推送到浏览器后,该脚本会立即执行并监听文档的点击事件。由于推送资源已经存在于浏览器缓存中,脚本无需额外网络往返,这使得即便在弱网环境下,点击事件也能被快速绑定。脚本内部通常使用sendBeacon将点击数据异步发往Nginx的/click接口,因为sendBeacon不阻塞页面卸载,特别适合日记类应用的退出前上报。
一个常见的误区是开发者在推送脚本里又发起了同步XHR,这会抵消http2_push带来的性能收益。正确做法是在脚本中仅做事件委托,把用户点击的target信息、时间戳序列化为URL参数,然后调用navigator.sendBeacon('/click?'+params)。Nginx收到请求后直接返回204,访问日志就会记下这次点击的结构化信息。对于不支持sendBeacon的旧浏览器,可降级为new Image().src方式,但仍应保持请求极小。
以下示例展示推送脚本的核心内容,注意其中对DOM的监听与参数构造:
(function () {
function reportClick(e) {
var t = e.target;
var params = 'cat=diary&ts=' + Date.now();
params += '&x=' + (e.clientX || 0) + '&y=' + (e.clientY || 0);
if (t && t.id) {
params += '&el=' + encodeURIComponent(t.id);
}
if (navigator.sendBeacon) {
navigator.sendBeacon('/click?' + params);
} else {
var img = new Image();
img.src = '/click?' + params;
}
}
if (document.addEventListener) {
document.addEventListener('click', reportClick, true);
}
})();
这段脚本通过捕获阶段监听点击,能覆盖绝大多数交互元素。由于它是被服务器推送的,用户打开首页时便已就绪,不会出现在页面底部加载器才绑定事件的空窗期。在实测中,这种方案比传统尾部嵌入脚本的点击丢失率下降约四成,尤其对移动端 diary 应用的返回键点击捕获有明显改善。
三、ClickHouse表设计与日志消费写入策略
ClickHouse处理点击日志的核心在于表引擎的选择与分区键设定。对于append-only的日记点击数据,使用MergeTree家族最为合适。我们可以按天分区,用toYYYYMMDD(time)作为partition key,并指定uri或el为跳数索引列,加速特定元素的查询。由于Nginx日志是JSON文本,建议使用Kafka引擎或File引擎配合物化视图做ETL,而不是直接让ClickHouse去读文本文件。
字段类型方面,request_time应定义为Float32而非String,否则无法做平均耗时分析;remote_addr可用IPv4类型节省空间。若前端传来的args中包含动态参数,可先以String存入原始列,再通过物化视图用queryString函数抽取需要的键值,写入窄表供报表使用。这种原始宽表加派生窄表的模式,兼顾了灵活性与查询性能。
下面是创建基础表的示例,展示了如何声明分区与索引:
CREATE TABLE diary_click_raw (
time DateTime,
remote_addr IPv4,
uri String,
args String,
request_time Float32,
http_user_agent String
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(time)
ORDER BY (uri, time)
TTL time + INTERVAL 90 DAY;
写入端可以使用Vector或自研Go程序,实时追Nginx日志文件,将每行JSON解析后批量插入。ClickHouse的async_insert特性可以把多次小写入合并,进一步降低磁盘IOPS。对于需要按元素统计点击量的场景,再建一张物化视图,将el字段聚合计数,前端报表直接查视图即可获得亚秒级响应。整个链路从Nginx推送、浏览器上报到ClickHouse分析,能够在单台入门级服务器上支撑每日数亿级日记点击事件的稳定处理。
Nginxhttp2_pushClickHouse修改时间:2026-08-13 23:48:34