Skip to content

Nginx 日志采集与分析:Vector + ClickHouse + Grafana 全流程实战

本文介绍如何通过 Nginx + Vector + ClickHouse + Grafana 搭建一套高性能的 Nginx 日志采集、存储与可视化分析平台。架构上由 Nginx 生成 JSON 格式的访问日志,Vector 实时采集并解析、补充 GeoIP 与 UA 信息,写入 ClickHouse 进行存储与聚合分析,最终由 Grafana 呈现可视化看板。

整体架构

Nginx (生成 JSON 格式日志)

Vector (采集、解析、GeoIP、UA 解析)

ClickHouse (存储与分析)

Grafana (可视化看板)

一、Nginx 配置

Nginx 安装步骤省略

1.1 自定义 JSON 日志格式

Nginx 默认的 combined 日志格式不易被结构化解析,因此我们自定义 main 格式,将每一行访问日志输出为一条 JSON 字符串。日志中包含毫秒级时间戳、服务器/客户端 IP、域名、URL、响应耗时、状态码、UA 等关键字段。

1.2 配置 nginx.conf

bash
vim /home/application/nginx/conf/nginx.conf

http {} 块中增加 maplog_format 配置(建议放在已有的 http {} 顶部位置,避免覆盖其他站点使用的格式):

nginx
map "$time_iso8601 # $msec" $time_iso8601_ms { "~(^[^+]+)(\+[0-9:]+) # \d+\.(\d+)$" $1.$3$2; }
log_format main
    '{"timestamp":"$time_iso8601_ms",'
    '"server_ip":"$server_addr",'
    '"remote_ip":"$remote_addr",'
    '"xff":"$http_x_forwarded_for",'
    '"remote_user":"$remote_user",'
    '"domain":"$host",'
    '"url":"$request_uri",'
    '"referer":"$http_referer",'
    '"upstreamtime":"$upstream_response_time",'
    '"responsetime":"$request_time",'
    '"request_method":"$request_method",'
    '"status":"$status",'
    '"response_length":"$bytes_sent",'
    '"request_length":"$request_length",'
    '"protocol":"$server_protocol",'
    '"upstreamhost":"$upstream_addr",'
    '"http_user_agent":"$http_user_agent"'
    '}';
access_log  /home/application/nginx/logs/access.log  main;

说明

  • $time_iso8601_ms 是通过 map 指令拼接出来的时间戳(带毫秒、含时区),后续 Vector 会按此格式解析。
  • $http_x_forwarded_for 多个代理 IP 时是逗号分隔字符串,Vector 会进一步取第一个作为真实客户端 IP。

1.3 重载配置

bash
# 检查配置语法
nginx -t

# 重载配置
nginx -s reload

# 或者使用 systemd
systemctl reload nginx

重载后访问一次站点,确认 /home/application/nginx/logs/access.log 中已经输出 JSON 格式的日志。


二、安装 Docker

Docker 安装步骤省略


三、ClickHouse 部署

3.1 创建部署目录与 docker-compose

bash
mkdir -p /home/application/Database/clickhouse/{data,log}
vim /home/application/Database/clickhouse/docker-compose.yml

docker-compose.yml 内容:

  • 需定义 clickhouse 数据库的密码
yaml
services:
  clickhouse:
    #image: clickhouse:24.8.14
    image: docker.cnb.cool/srebro/docker-images-chrom/clickhouse:24.8.14_amd64
    container_name: clickhouse
    restart: always
    environment:
      TZ: Asia/Shanghai
      CLICKHOUSE_USER: 'default'
      CLICKHOUSE_PASSWORD: 'xxxx'
      CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: '1'
    networks:
      - srebro
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - /home/application/Database/clickhouse/log:/var/log/clickhouse-server
      - /home/application/Database/clickhouse/data:/var/lib/clickhouse
      - /etc/localtime:/etc/localtime:ro

networks:
  srebro:
    external: true

3.2 启动 ClickHouse

bash
cd /home/application/Database/clickhouse
docker-compose up -d

3.3 创建数据库与表

进入 ClickHouse 客户端

bash
docker container exec -it clickhouse clickhouse-client --user default --password xxxx

执行创建语句

sql
CREATE DATABASE IF NOT EXISTS nginxlogs ENGINE=Atomic;

CREATE TABLE nginxlogs.nginx_access
(
    `timestamp` DateTime64(3, 'Asia/Shanghai'),
    `server_ip` String,
    `domain` String,
    `request_method` String,
    `status` Int32,
    `top_path` String,
    `path` String,
    `query` String,
    `protocol` String,
    `referer` String,
    `upstreamhost` String,
    `responsetime` Float32,
    `upstreamtime` Float32,
    `duration` Float32,
    `request_length` Int32,
    `response_length` Int32,
    `client_ip` String,
    `client_latitude` Float32,
    `client_longitude` Float32,
    `remote_user` String,
    `remote_ip` String,
    `xff` String,
    `client_city` String,
    `client_region` String,
    `client_country` String,
    `http_user_agent` String,
    `client_browser_family` String,
    `client_browser_major` String,
    `client_os_family` String,
    `client_os_major` String,
    `client_device_brand` String,
    `client_device_model` String,
    `createdtime` DateTime64(3, 'Asia/Shanghai')
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
PRIMARY KEY (timestamp,
 server_ip,
 status,
 top_path,
 domain,
 upstreamhost,
 client_ip,
 remote_user,
 request_method,
 protocol,
 responsetime,
 upstreamtime,
 duration,
 request_length,
 response_length,
 path,
 referer,
 client_city,
 client_region,
 client_country,
 client_browser_family,
 client_browser_major,
 client_os_family,
 client_os_major,
 client_device_brand,
 client_device_model
)
TTL toDateTime(timestamp) + toIntervalDay(30)
SETTINGS index_granularity = 8192;

设计要点

  • 引擎:MergeTree,适合大规模日志写入与聚合查询。
  • 分区:按天 toYYYYMMDD(timestamp),便于按天清理和查询。
  • TTL:30 天自动过期,避免日志无限增长。
  • 主键:使用多个常用过滤字段组合,兼顾写入和典型查询。

四、部署 Vector 采集日志

Vector 是一款高性能、可观测性数据管道(由 Datadog/Timber 开发)。这里使用它读取 Nginx 日志,解析 JSON、提取字段、补全 GeoIP 与 UA,然后写入 ClickHouse。

4.1 Vector 部署

创建部署目录与 docker-compose

bash
#创建Vector工作目录
mkdir -p /home/application/vector


#下载最新的GeoLite2库文件
cd /home/application/vector/
wget -O GeoLite2-City.mmdb https://github.com/P3TERX/GeoLite.mmdb/releases/download/2026.06.07/GeoLite2-City.mmdb


vim /home/application/vector/docker-compose.yml

docker-compose.yaml 内容:

yaml
services:
  vector:
    #image: timberio/vector:0.56.0-debian
    image: docker.cnb.cool/srebro/docker-images-chrom/timberio-vector:0.56.0-debian_amd64
    container_name: vector
    hostname: vector
    restart: always
    entrypoint: vector --config-dir /etc/vector/conf 
    ports:
      - 8686:8686
    networks:
      - srebro
    volumes:
      - /home/application/nginx/logs:/nginx_logs  # 这是需要采集的日志的路径需要挂载到容器内
      - /home/application/vector/GeoLite2-City.mmdb:/etc/vector/GeoLite2-City.mmdb
      - /home/application/vector/conf:/etc/vector/conf
      - /etc/localtime:/etc/localtime
networks:
  srebro:
    external: true

注意

  • /var/log/nginx 是需要采集的 Nginx 日志目录,请根据实际环境修改。
  • access_vector_error.log 用来记录解析失败的日志,便于排错。
  • GeoLite2-City.mmdb 是 MaxMind 提供的 GeoIP 离线库,用于客户端 IP 城市定位。

4.2 Vector 配置

主配置文件

bash
#创建Vector配置文件目录
mkdir -p /home/application/vector/conf

cat <<-EOF > /home/application/vector/conf/vector.yaml
timezone: "Asia/Shanghai"
api:
  enabled: true
  address: "0.0.0.0:8686"
EOF

Nginx 访问日志解析配置

bash
cat <<-EOF > /home/application/vector/conf/nginx-access.yaml
sources:
  01_file_nginx_access:
    type: file
    include:
      - /nginx_logs/access.log  # 容器内 Nginx 访问日志路径

transforms:
  02_parse_nginx_access:
    drop_on_error: true
    reroute_dropped: true
    type: remap
    inputs:
      - 01_file_nginx_access
    source: |
      .message = string!(.message)
      if contains(.message,"\\x") { .message = replace(.message, "\\x", "\\\\x") }
      . = parse_json!(.message)
      .createdtime = to_unix_timestamp(now(), unit: "milliseconds")
      .timestamp = to_unix_timestamp(parse_timestamp!(.timestamp , format: "%+"), unit: "milliseconds")
      .url_list = split!(.url, "?", 2)
      .path = .url_list[0]
      .query = .url_list[1]
      .path_list = split!(.path, "/", 3)
      if length(.path_list) > 2 {.top_path = join!(["/", .path_list[1]])} else {.top_path = "/"}
      .duration = round(((to_float(.responsetime) ?? 0) - (to_float(.upstreamtime) ?? 0)) ?? 0,3)
      if .xff == "-" { .xff = .remote_ip }
      .client_ip = split!(.xff, ",", 2)[0]
      .ua = parse_user_agent!(.http_user_agent , mode: "enriched")
      .client_browser_family = .ua.browser.family
      .client_browser_major = .ua.browser.major
      .client_os_family = .ua.os.family
      .client_os_major = .ua.os.major
      .client_device_brand = .ua.device.brand
      .client_device_model = .ua.device.model
      .geoip = get_enrichment_table_record("geoip_table", {"ip": .client_ip}) ?? {"city_name":"unknown","region_name":"unknown","country_name":"unknown"}
      .client_city = .geoip.city_name
      .client_region = .geoip.region_name
      .client_country = .geoip.country_name
      .client_latitude = .geoip.latitude
      .client_longitude = .geoip.longitude
      del(.path_list)
      del(.url_list)
      del(.ua)
      del(.geoip)
      del(.url)

sinks:
  03_ck_nginx_access:
    type: clickhouse
    inputs:
      - 02_parse_nginx_access
    endpoint: http://<clickhouse_host>:8123   # ClickHouse HTTP 接口
    database: nginxlogs                         # ClickHouse 库
    table: nginx_access                         # ClickHouse 表
    auth:
      strategy: basic
      user: default
      password: xxxxxxxx                           # ClickHouse 密码
    compression: gzip

enrichment_tables:
  geoip_table:
    path: "/etc/vector/GeoLite2-City.mmdb"
    type: geoip
    locale: "zh-CN"
EOF

关键点说明

  • 01_file_nginx_access 监听文件,Vector 会自动按行读取并保存 offset,重启不会丢数据。
  • 02_parse_nginx_access 使用 VRL 解析 JSON、拆分 URL、计算 duration(自身处理耗时)、补全 UA 与 GeoIP。
  • drop_on_error: true + reroute_dropped: true 让解析失败的日志输出到 04_out_nginx_dropped,不会污染主流程。
  • 03_ck_nginx_access 通过 HTTP 接口将结构化日志写入 ClickHouse,使用 gzip 压缩降低带宽。
  • geoip_table 是 enrichment table,在 VRL 中通过 get_enrichment_table_record 引用。

4.3 运行 Vector

bash
cd /home/application/vector
docker compose up -d
docker logs -f vector

Vector 启动成功后。稍等片刻即可在 ClickHouse 中查到数据:

sql
SELECT count() FROM nginxlogs.nginx_access;

五、Grafana 配置

Grafana安装步骤省略,本节介绍如何安装 ClickHouse 数据源插件、配置数据源以及导入可视化看板。

5.1 安装 ClickHouse 插件

Grafana 官方仓库的 grafana/grafana 镜像默认没有集成 ClickHouse 数据源,需要安装第三方插件 grafana-clickhouse-datasource

bash
# 进入 Grafana 容器
docker container exec -it grafana bash

# 安装 ClickHouse 数据源插件
grafana cli plugins install grafana-clickhouse-datasource

# 退出容器并重启 Grafana
exit
docker restart grafana

注意:部分较新的 Grafana 版本会要求插件带签名,需使用 grafana cli plugins install <plugin> --pluginUrl <url> 或将插件放进 GF_PATHS_PLUGINS 指定目录;如安装失败请检查网络/镜像源。

5.2 增加数据源

登录 Grafana,配置数据源,添加新的数据源

image-20260610155249835

主要填写以下内容:

  • Server addresshttp://<clickhouse_host>:9000
  • Auth:Basic auth
    • Userdefault
    • Password<clickhouse_password>
  • Default databasenginxlogs 默认数据库,一定要填写!!!

image-20260610155359083

填写完成后点击 Save & test,出现绿色 Data source is working 提示即代表连通。

5.3 导入看板

我们使用准备好的 ClickHouse + Nginx 请求日志分析看板进行导入:

  1. 进入 DashboardsImport
  2. 导入仪表板,id 为:22037
  3. 选择目标数据源(上面新增的 ClickHouse 数据源)。
  4. 点击 Import 完成导入。

image-20260610155753767

5.4 看板预览

该看板基于 ClickHouse + Vector 的 NGINX 请求日志分析看板,包括 请求与耗时分析、异常请求分析、用户分析、地理位置分布图、指定接口分析、请求日志明细。尤其在异常请求分析方面,总结多年异常请求分析经验,从各个角度设计了大量异常请求的分析图表。

image-20260610155927487


image-20260610160009293

参考文档

最近更新

采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 运维小弟