日志分析是几乎所有业务系统都绕不开的需求,无论是接口访问统计、异常排查,还是用户行为分析,背后都依赖一套可靠的日志采集与查询体系。传统方案里,大家习惯把日志写进文件再用ELK处理,或者直接塞进MySQL做统计。这两种方式在日志量小的场景下没问题,但一旦日增日志达到千万行甚至亿级,写入延迟和聚合查询的性能就会成为明显瓶颈。这篇文章介绍一种更轻量的组合:用Nginx接收并转发日志,用ClickHouse做存储和查询,搭建一套低成本、高性能的日志分析引擎。

一、整体架构设计思路
这套方案的核心思想是职责分离。Nginx擅长处理高并发连接,适合做日志的接入层,业务方只需要把日志通过HTTP请求或syslog协议发到Nginx,Nginx再把日志转发给后端的ClickHouse。ClickHouse则是列式存储的OLAP数据库,单机就能支撑每秒几十万行的写入,对聚合类查询做了深度优化,特别适合按时间范围统计PV、UV、错误率这类场景。
整个链路可以描述为:业务服务把日志发送到Nginx暴露的接收端口,Nginx通过proxy_pass将请求转发到ClickHouse的HTTP接口,ClickHouse执行INSERT语句把日志写入目标表。相比直接让业务服务连ClickHouse的JDBC或Native协议,这种方式有几个好处:一是Nginx天然具备限流、负载均衡能力,可以在日志洪峰时保护ClickHouse;二是业务方不需要引入额外的客户端依赖,一个HTTP POST就能搞定;三是Nginx可以配置缓冲,日志瞬时突增时先落盘排队,避免后端被打挂。
当然也有取舍。如果对日志可靠性要求极高,一条都不能丢,那么中间还需要引入Kafka做削峰和持久化。但对于大多数统计类日志场景,Nginx加ClickHouse的两层结构已经够用,部署和维护成本都低很多。
二、ClickHouse表结构设计与分区策略
日志表的设计直接决定了后续查询性能。ClickHouse最关键的特性是分区裁剪和排序键,合理的分区能让查询只扫描必要的数据块。对于日志场景,通常按天分区,排序键选择查询中最常用的过滤字段,比如时间加上业务维度。
下面是一个典型的访问日志表结构:
CREATE TABLE nginx_log.access_log
(
event_time DateTime,
date Date MATERIALIZED toDate(event_time),
client_ip IPv4,
method LowCardinality(String),
path String,
status UInt16,
bytes_sent UInt64,
request_time Float32,
user_agent String,
referer String
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, path, status)
TTL event_time + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;几个设计要点值得展开说明。首先是LowCardinality(String),method这种字段只有GET、POST等十几个取值,使用低基数类型能显著减少存储和提升查询速度。其次是PARTITION BY toYYYYMMDD,按天分区配合TTL可以实现日志的自动过期清理,不需要再写定时任务删数据。最后是排序键,把event_time放在第一位,因为绝大多数查询都带时间范围条件,ClickHouse会利用主索引跳过不相关的数据块。
需要避免的一个坑是分区粒度过细。有人喜欢按小时分区,觉得粒度越细查询越快,实际上分区数量过多会导致后台merge压力剧增,严重时会出现too many parts报错。日志场景按天分区是经过大量实践验证的平衡点。
三、Nginx侧的接收与转发配置
Nginx这边要做的事情是:开一个专门的server接收日志请求,设置合理的缓冲,然后转发给ClickHouse的HTTP端口8123。ClickHouse的HTTP接口支持直接执行SQL,因此可以把日志插入转换为一条INSERT语句。
一个可用的配置示例如下:
upstream clickhouse_http {
server 127.0.0.1:8123;
keepalive 32;
}
server {
listen 9800;
server_name log-collector;
location /ingest {
# 请求体限制,防止单条日志过大
client_max_body_size 2m;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://clickhouse_http;
# 关键:重写请求为ClickHouse可执行的INSERT语句
proxy_set_header X-CH-Database nginx_log;
rewrite ^/ingest$ /?query=INSERT%20INTO%20access_log%20FORMAT%20JSONEachRow last;
# 简单防护:只接受POST
if ($request_method !~ ^(POST)$) {
return 405;
}
}
}业务方发送日志时,只需要向http://log-collector:9800/ingest发送POST请求,body是JSONEachRow格式的数据,例如{"event_time":"2024-05-10 10:23:11","client_ip":"192.168.1.5","method":"GET","path":"/api/user","status":200,"bytes_sent":1024,"request_time":0.03,"user_agent":"curl/8.0","referer":""}。多条日志可以放在同一个请求里逐行排列,这样能大幅减少请求次数。
如果业务方走的是syslog协议,Nginx从1.7.1版本开始支持access_log syslog:server=...方向,但接收syslog需要借助第三方模块或改用rsyslog中转,实现上不如HTTP方式直观。因此更推荐统一用HTTP接入,客户端实现简单,也方便加认证头。
四、批量写入优化与查询实践
ClickHouse最忌讳的就是小批量高频写入。每次INSERT都会生成一个新的数据分片,后台需要异步merge,写入频率过高会直接把系统拖垮。经验值是每秒不超过一到两次INSERT,每次批量几百到几千行。业务侧的做法是在客户端攒一批日志,比如积满500条或超过1秒就统一发送。如果客户端不好改,也可以在Nginx后面加一个轻量的缓冲服务,或者上文中提到的Kafka方案。
数据落库之后,查询体验就是ClickHouse的主场了。统计某天每小时的请求量和错误率,MySQL上可能要跑几十秒的语句,这里毫秒级就能返回:
SELECT
toStartOfHour(event_time) AS hour,
count() AS total_requests,
countIf(status >= 500) AS errors,
round(errors / total_requests * 100, 2) AS error_rate
FROM nginx_log.access_log
WHERE date = '2024-05-10'
GROUP BY hour
ORDER BY hour;再比如查慢请求TOP 10的接口路径:
SELECT
path,
count() AS cnt,
round(avg(request_time), 3) AS avg_time,
round(max(request_time), 3) AS max_time
FROM nginx_log.access_log
WHERE date = '2024-05-10' AND request_time > 0.5
GROUP BY path
ORDER BY avg_time DESC
LIMIT 10;查询时务必带上分区过滤条件,也就是date或event_time的范围,否则ClickHouse会做全表扫描。对于需要频繁执行的固定统计,建议配合物化视图预聚合,把明细查询转化为对汇总表的查询,性能还能再提升一个量级。
五、压测对比与踩坑经验
在一台16核32G的普通服务器上实测,这套架构单机写入吞吐稳定在每秒20万到40万行,日均十亿级别的日志完全撑得住。同样的数据量下,MySQL的写入会成为瓶颈,聚合查询普遍在10秒以上;ELK方案虽然查询灵活,但资源消耗大约是ClickHouse的三到四倍,主要开销在JVM和倒排索引上。ClickHouse的列式压缩对日志这种重复度高的数据非常友好,同样的原始日志,存储空间大约只有文本文件的十分之一。
踩坑方面有三点值得提醒。第一,ClickHouse不适合高频单条更新和删除,日志场景是append-only没问题,但如果有修改需求就要在设计上规避。第二,要注意max_insert_block_size和后台merge的监控,出现too many parts基本就是写入频率太高,先检查客户端有没有做批量。第三,Nginx转发到ClickHouse时记得限制来源和加认证,ClickHouse的HTTP接口默认能执行任意SQL,暴露在内网也要做好users.xml里的只读权限配置,避免误操作删表。
总的来说,Nginx加ClickHouse这套组合胜在简单直接,两层结构没有复杂的中间件,运维心智负担小。如果日志规模进一步扩大到多条链路、多机房,再考虑演进为Kafka加ClickHouse的架构即可,表结构层面基本不用变动,迁移成本很低。
NginxClickHouse日志分析修改时间:2026-09-12 14:52:49