实验项目 基于 metabase + clickhouse 的流量分析 王一川 2025-06-08 2025-06-09 一、背景 博客 的访问统计是基于51la 实现,其只会统计“真人”的访问记录会过滤掉绝大部分的全站流量,因此如果想要分析全站的流量是否存在异常则需要更底层的实现。博客的基础架构是 nginx + hexo 的静态博客页面,因此全站流量分析的思路是:
采集 nginx 的访问日志 同步到分析性数据库 搭建实时报表 结合博主的技术栈,设计出如下架构
二、框架解析 本节解释为什么要选择 vector + clickhouse + metabase
2.1 vector vector 是一个轻量级、超快用于构建可观测数据管道的工具,这里用作日志采集的 ETL工具。相较于 flume 或 filebeat + logstash 有以下优点
使用 rust 编写,无 gc 运行效率极高且部署简单无需额外的环境(flume 或 ELK 都需要 jre) vector 支持 44 种 source、15 种 transform 和 59 种 sink 同时要考虑实际情况,博主公网服务器的配置仅为 2c4g,vector 是一个性价比极高的选择
2.2 clickhouse 市面上流量的 AP 数据库大概就是:clickhouse、doris(starrocks) 或 duckdb。这个实验有如下特点:
实时 单机 从单机的架构来看,clickhouse 仅需自身服务不像 doris 至少还需要 fe+be。同时 clickhouse 也是博主最熟悉的 AP 数据库
duckdb 似乎也是不错的选择,但是作为内嵌的数据库对外提供服务的能力博主还需要去研究一下
BI 方面最开始选择的是 superset,但 superset 匿名访问 dashboard 的配置过于繁琐,且部署文档欠缺。因此选择 metabase
三、框架搭建 3.1 vector 一行命令直接安装
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev | bash
部署在 nginx 服务器中
如果是 root 用户执行安装命令,vector 的 home 路径为:/root/.vector
;为了后续方便执行可以添加到环境变量
VECTOR_HOME=/root/.vector PATH=$PATH:$VECTOR_HOME/bin
vector 默认给出了一个演示案例,执行
vector -c $VECTOR_HOME/config/vector.yaml
3.2 clickhouse 使用 docker 进行部署
services: clickhouse: image: bitnami/clickhouse:23.8 environment: - ALLOW_EMPTY_PASSWORD=false - CLICKHOUSE_ADMIN_USER=root - CLICKHOUSE_ADMIN_PASSWORD=<替换成自己的密码> - TZ=Asia/Shanghai ports: - '8123:8123' - '9000:9000' volumes: - clickhouse_data:/bitnami/clickhouse volumes: clickhouse_data: driver: local
建议部署在分析服务器,如果公网服务器配置足够也可与 nginx 在一台
使用 docker 进行部署
services: metabase: image: metabase/metabase:latest container_name: metabase hostname: metabase volumes: - metabase_data:/dev/random:ro environment: - JAVA_TIMEZONE=Asia/Shanghai ports: - 8088 :3000 volumes: metabase_data: driver: local
配置 clickhouse 连接
点击数据库-添加数据库,填写clickhouse 连接信息即可。如果和博主一样 metabbase(下称 mb) 与 clickhouse(下称 ck) 均使用 docker 部署且使用博主的 compose 文件,可以直接填写服务器的 ip(非 127.0.0.1),如果熟悉 docker 的同学可以将两个 compose 文件合并成一个这样 mb 与 ck 就在一个网段上可以直接填写 service_name 作为 ip。
为了让 mb 可以即使获取 ck 的表结构可以打开
Scan all databases 为简单的探索重新运行查询 选择同步和扫描的时间 建议部署在分析服务器,如果公网服务器配置足够也可与 nginx 在一台
3.4 其它细节 博主的 clickhou、metabase 部署在自建的服务器中,借助公网 IP 进行穿透。使用 docker 部署的资源占用如下
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS cc0bf19b1d95 metabase 1.17% 1.105GiB / 15.12GiB 7.31% 25.4MB / 522MB 0B / 1.94GB 149 e3c8555282bb clickhouse-clickhouse-1 38.35% 544.5MiB / 15.12GiB 3.52% 48.4MB / 64.3MB 0B / 144GB 408
四、实验搭建 4.1 日志采集 nginx 默认的访问日志使用空格分割,为了方便后续的解析这里修改为 json 格式。修改默认的配置文件nginx.conf
并在http
模块中添加名为access_json
日志格式
log_format access_json '{"@timestamp ":"$time_iso8601 ",' '"host":"$server_addr ",' '"clientip":"$remote_addr ",' '"size":$body_bytes_sent ,' '"responsetime":$request_time ,' '"upstreamtime":"$upstream_response_time ",' '"upstreamhost":"$upstream_addr ",' '"http_host":"$host ",' '"uri":"$uri ",' '"domain":"$host ",' '"remote_user":"$remote_user ",' '"request":"$request ",' '"xff":"$http_x_forwarded_for ",' '"referer":"$http_referer ",' '"tcp_xff":"$proxy_protocol_addr ",' '"http_user_agent":"$http_user_agent ",' '"status":"$status "}' ;
并修改access_log
日志应用上面配置的格式
access_log /var/log/nginx/access.log access_json;
校验配置文件无误后重新加载
观察 nginx 的日志是否符合要求
tail -F /var/log/nginx/access.log
下面使用 vector 将日志采集到 clickhouse 中,在 clickhouse 创建nginx_access_logs
用于接收原始日志
create table nginx_access_logs( message String ) engine = MergeTree order by tuple()
这里的message
是 vector 默认映射到 clickhouse 的字段,初接触 vector 同学不建议修改这个字段
下面编写 vector 配置文件实时捕获 nginx 的访问日志并写入nginx_access_logs
中
sources: nginx_access_log: type: file include: - /var/log/nginx/access.log sinks: out: inputs: - nginx_access_log type: clickhouse endpoint: http://127.0.0.1:8123 database: ueba table: nginx_access_logs auth: strategy: basic user: root password: <替换成自己的 ck 密码>
启动日志采集服务
nohup vector -c nginx_access_log_to_clickhouse.yaml >> vector.log &
vector 的 file source 默认支持断点续传将 offset 实时写入本地文件中,路径为$data_dir/<source_name>
其中data_dir
是 vector 的全局变量也可以在环境变量配置,默认/var/lib/vector/
会写入下面数据
{ "version" : "1" , "checkpoints" : [ { "fingerprint" : { "first_lines_checksum" : 11589993358280241074 } , "position" : 26119026 , "modified" : "2025-06-08T05:57:56.154979458Z" } ]
其中position
即为当前读取的文件偏移量。
4.2 解析日志 不出意外 clickhouse 的nginx_access_logs
表已经接收到数据了,根据 nginx 的日志格式创建access_logs
表用于接收解析后的结构数据
create table access_logs( timestamp DateTime64(3 ) comment '访问时间' , host Nullable(String), client_ip IPv4, size UInt64, response_time Nullable(Float64), upstream_time Nullable(Float64), upstream_host Nullable(String), http_host Nullable(String), uri Nullable(String), domain Nullable(String), remote_user Nullable(String), request Nullable(String), method Nullable(String), xff Nullable(String), referer Nullable(String), tcp_xff Nullable(String), http_user_agent Nullable(String), status UInt16 ) engine = MergeTree partition by toYYYYMMDD(timestamp )order by (timestamp , client_ip)
创建物化视图自动解析数据
CREATE MATERIALIZED VIEW mv_access_logs TO access_logs AS SELECT timestamp , host, client_ip, size, response_time, upstream_time, if(upstream_host = '-' , NULL , upstream_host) AS upstream_host, http_host, uri, domain, if(remote_user = '-' , NULL , remote_user) AS remote_user, request, method , if(xff = '-' , NULL , xff) AS xff, if(referer = '-' , NULL , referer) AS referer, if(tcp_xff = '-' , NULL , tcp_xff) AS tcp_xff, http_user_agent, status FROM ( SELECT parseDateTime64BestEffortOrNull(JSONExtractString(message, '@timestamp' )) AS timestamp , JSONExtractString(message, 'host' ) AS host, toIPv4OrNull(JSONExtractString(message, 'clientip' )) AS client_ip, JSONExtractUInt(message, 'size' ) AS size, toFloat64OrNull(JSONExtractString(message, 'responsetime' )) AS response_time, toFloat64OrNull(JSONExtractString(message, 'upstreamtime' )) AS upstream_time, JSONExtractString(message, 'upstreamhost' ) AS upstream_host, JSONExtractString(message, 'http_host' ) AS http_host, JSONExtractString(message, 'uri' ) AS uri, JSONExtractString(message, 'domain' ) AS domain, JSONExtractString(message, 'remote_user' ) AS remote_user, JSONExtractString(message, 'request' ) AS request, splitByString(' ' , JSONExtractString(message, 'request' ))[1 ] AS method , JSONExtractString(message, 'xff' ) AS xff, JSONExtractString(message, 'referer' ) AS referer, JSONExtractString(message, 'tcp_xff' ) AS tcp_xff, JSONExtractString(message, 'http_user_agent' ) AS http_user_agent, toUInt16OrNull(JSONExtractString(message, 'status' )) AS status FROM nginx_access_logs ) AS t WHERE timestamp IS NOT NULL ;
至此便打通了实时的日志采集链路。
4.3 搭建报表 评估一下每秒的日志条数
select avg (cnt)from (select toYYYYMMDDhhmmss(timestamp ), count (1 ) as cntfrom access_logsgroup by 1 );
数据量在 5 条/s,access_logs
按天分区是可以满足今日的实时数据分析,无需额外基于access_logs
进一步构建物化视图。因此直接在 metabase 中搭建报表,下面演示分时段 pv 统计的搭建过程
点击创建问题,按照下图选择即可
数据:报表的数据源,对应 sql 的 from 过滤:选择今日,对应 sql 的 where 汇总:选择行数,对应 sql 的聚合字段这里是 count() 通过:选择时间字段,因为是分时段统计同时选择时间单位为分钟,对应 sql 的 group by 排序:选择时间字段,为了让可视化可以连续显示,对应 sql 的 order 点击可视化后 mb 会自动判断最适合的展示图表,也可以对样式进行微调
后续所有的指标都类似,当然如果指标过于复杂无法通过低代码进行拖拉拽可以新建 sql 实现可视化。当指标足够多时创建面板将指标拖入即可生成一个流量分析的实时面板,面板可配置自动刷新以及生成分享链接
五、后续规划 数据更多有价值的指标 ip 映射地理位置