基于 metabase + clickhouse 的流量分析

一、背景

博客的访问统计是基于51la实现,其只会统计“真人”的访问记录会过滤掉绝大部分的全站流量,因此如果想要分析全站的流量是否存在异常则需要更底层的实现。博客的基础架构是 nginx + hexo 的静态博客页面,因此全站流量分析的思路是:

  1. 采集 nginx 的访问日志
  2. 同步到分析性数据库
  3. 搭建实时报表

结合博主的技术栈,设计出如下架构

基础流量架构

二、框架解析

本节解释为什么要选择 vector + clickhouse + metabase

2.1 vector

vector 是一个轻量级、超快用于构建可观测数据管道的工具,这里用作日志采集的 ETL工具。相较于 flume 或 filebeat + logstash 有以下优点

  1. 使用 rust 编写,无 gc 运行效率极高且部署简单无需额外的环境(flume 或 ELK 都需要 jre)
  2. vector 支持 44 种 source、15 种 transform 和 59 种 sink

同时要考虑实际情况,博主公网服务器的配置仅为 2c4g,vector 是一个性价比极高的选择

2.2 clickhouse

市面上流量的 AP 数据库大概就是:clickhouse、doris(starrocks) 或 duckdb。这个实验有如下特点:

  1. 实时
  2. 单机

从单机的架构来看,clickhouse 仅需自身服务不像 doris 至少还需要 fe+be。同时 clickhouse 也是博主最熟悉的 AP 数据库

duckdb 似乎也是不错的选择,但是作为内嵌的数据库对外提供服务的能力博主还需要去研究一下

2.3 metabase

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

vector-demo

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 在一台

3.3 metabase

使用 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 连接mb2ck

点击数据库-添加数据库,填写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 -t
nginx -s reload

观察 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 cnt
from access_logs
group by 1
);

数据量在 5 条/s,access_logs按天分区是可以满足今日的实时数据分析,无需额外基于access_logs进一步构建物化视图。因此直接在 metabase 中搭建报表,下面演示分时段 pv 统计的搭建过程

点击创建问题,按照下图选择即可

mb-demo

  • 数据:报表的数据源,对应 sql 的 from
  • 过滤:选择今日,对应 sql 的 where
  • 汇总:选择行数,对应 sql 的聚合字段这里是 count()
  • 通过:选择时间字段,因为是分时段统计同时选择时间单位为分钟,对应 sql 的 group by
  • 排序:选择时间字段,为了让可视化可以连续显示,对应 sql 的 order

点击可视化后 mb 会自动判断最适合的展示图表,也可以对样式进行微调

mb-show

后续所有的指标都类似,当然如果指标过于复杂无法通过低代码进行拖拉拽可以新建 sql 实现可视化。当指标足够多时创建面板将指标拖入即可生成一个流量分析的实时面板,面板可配置自动刷新以及生成分享链接

mb-share

五、后续规划

  1. 数据更多有价值的指标
  2. ip 映射地理位置