☰
ClickHouse日志分析平台实战:从表设计到查询优化与运维避坑
2026/9/24 22:43:42 网站建设 项目流程

1. 为什么我用ClickHouse重做日志分析:先聊聊背景和选型

做后端运维和平台开发这么多年,日志分析这块我前前后后折腾过不少方案。早期用过ELK那一套,Elasticsearch确实好用,模糊搜索、聚合统计都很顺手,但到了数据量涨起来之后,成本压力就非常明显了——ES的内存和磁盘开销相当“豪华”,三台机器扛了半年,堆内存频频告警,索引分片一多,写入性能也跟着抖。后来也试过Loki,轻量是真轻量,但它的索引能力太弱,想按某个字段快速过滤、按时间范围做复杂聚合,基本不太行,更多是“看日志”而不是“分析日志”。

真正让我下定决心切到ClickHouse,是因为一个具体的业务场景。我们当时要做一个统一日志平台,接入来源包括Nginx访问日志、后端服务打印的业务日志、Tomcat的catalina.out,还有Windows服务器上的系统日志和应用日志。每天新增的日志量大概在80亿到120亿条之间,高峰期写入速度要求每秒至少50万条。查询侧的需求也很明确:按时间范围、服务名、机器IP、日志级别这几个维度做组合过滤,然后对请求量、错误率、响应时间分位数做分钟级的聚合统计。

说实话,这个量级和查询模式,ES已经有点吃力了,Loki更是直接出局。ClickHouse的列式存储、极致的压缩比、向量化执行引擎,简直就是为这种“高吞吐写入+宽表聚合分析”的活量身定做的。我用一台16核64G的机器做POC测试,导入10亿行Nginx日志,存储占用不到90GB,这个压缩比在同量级数据下ES至少得翻三倍。聚合查询方面,按分钟分组算平均值和分位数,大部分查询响应都在200毫秒以内,这性能表现确实惊艳。

这篇文章就围绕我实际落地ClickHouse做日志分析平台的完整过程来写,从部署环境、表结构设计、数据入库链路,到查询优化、可视化对接,再到运维过程中碰到的各种坑,一次性讲清楚。如果你也在纠结“日志太多了怎么办”“ES太贵了能不能换”,那这篇文章应该能给你一些参考价值。

提示:ClickHouse适合的是“大规模、结构化程度较高、以聚合分析为主”的日志场景。如果你的日志格式极度不规则,或者你主要做全文检索(比如“搜日志里包含某个关键字的原始内容”),那ClickHouse不一定是最优选,这点后面会详细说。

2. 环境准备与集群部署:我建议从单机验证开始

很多人一上来就奔着集群去了,觉得分布式才显得“专业”。我的建议恰恰相反——先老老实实把单机版跑通,把表结构、写入链路、查询语句这些核心逻辑验证好,再考虑扩集群。原因很简单:ClickHouse单机的性能已经足够强,大部分中等规模的日志平台(日增百亿条以内)单机甚至双副本就能扛住,一上来就搞集群,只会把问题复杂度拉高好几倍。

2.1 操作系统与安装方式的选择

我实际部署的环境有两套,一套测试环境是Ubuntu,另一套生产环境用的是Rocky Linux 9,这也符合大多数公司的现状——测试和生产往往不是一个系统。ClickHouse官方对这两个系统都提供了良好的支持,安装方式也很简单,我习惯用官方RPM/DEB源来装,方便后续用systemctl管理。

先说一下Rocky Linux 9上的安装步骤:

# 添加官方仓库 sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://packages.clickhouse.com/rpm/clickhouse.repo # 安装服务端和客户端 sudo yum install -y clickhouse-server clickhouse-client # 启动并设置开机自启 sudo systemctl enable clickhouse-server sudo systemctl start clickhouse-server # 验证服务状态 sudo systemctl status clickhouse-server

Ubuntu上用DEB包,思路一致:

# 添加官方仓库 sudo apt-get install -y apt-transport-https ca-certificates dirmngr sudo apt-key adv --keyserver keyserver.ubuntu.com --recv E0C56BD4 echo "deb https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list sudo apt-get update # 安装服务端和客户端 sudo apt-get install -y clickhouse-server clickhouse-client # 启动并设置开机自启 sudo systemctl enable clickhouse-server sudo systemctl start clickhouse-server

关于“最新版下载”这个事,多说一句。ClickHouse的版本迭代非常快,我建议安装当前最新的稳定版(stable),而不是latest版本。latest一般是预发布版本,虽然功能新,但稳定性风险高,生产环境没必要当小白鼠。

2.2 关键配置项:内存、并发和存储路径

安装完之后,默认配置文件在/etc/clickhouse-server/config.xml,我通常会重点调整几个参数。

<!-- 允许通过网络访问,默认只监听本机 --> <listen_host>0.0.0.0</listen_host> <!-- 并发线程数,一般设置为CPU核数,不要盲目调大 --> <max_thread_pool_size>16</max_thread_pool_size> <!-- 单次查询最大内存限制,防止大查询拖垮整个服务 --> <max_memory_usage>10000000000</max_memory_usage> <!-- 数据存储路径,建议单独挂载数据盘,不要和系统盘共用 --> <path>/data/clickhouse/</path> <!-- 临时数据路径,用于排序和聚合的中间结果 --> <tmp_path>/data/clickhouse/tmp/</tmp_path>

内存这块我踩过一次坑。早期我总觉得“内存越大,给ClickHouse的越多越好”,结果把max_memory_usage调得特别高,导致一个聚合大查询直接把内存吃满,其他所有查询全部超时。后来我学乖了,这个值一般控制在物理内存的50%-60%比较稳妥,剩下的留给系统缓存和其他组件。

存储路径这块要特别注意,ClickHouse对磁盘IO的要求很高。如果条件允许,数据目录放到独立的NVMe SSD上,写入性能和查询响应都会有质的提升。机械硬盘不建议用于生产日志分析场景,IO会成为明显瓶颈。另外,tmp_path也建议放到SSD上,排序和聚合的中间数据落盘时快很多。

2.3 从单机到多副本:什么时候需要上集群?

ClickHouse的集群架构分为两层:分片(shard)和副本(replica)。分片解决的是数据量水平扩展的问题,比如单机磁盘快满了,就把数据分散到多台机器上;副本解决的是高可用问题,一个分片的数据存多份,某台机器挂了不影响查询。

根据我的实践经验,判断是否需要上集群,可以从两个维度来评估:

  • 数据量维度:单机磁盘存储已经超过总容量的60%,且增长趋势不减,这时候考虑分片扩展。
  • 可用性维度:日志平台是核心监控设施,不允许宕机窗口,至少需要配置双副本。

如果你是双副本架构,ClickHouse内置的ReplicatedMergeTree引擎会基于ZooKeeper或ClickHouse Keeper来协调数据同步。我配置集群时用的三套ClickHouse节点加三套ClickHouse Keeper(轻量级替代ZooKeeper的方案),架构还算清爽。不过说实话,初期单机加定期冷备也能撑住大多数场景,等到规模真上来了再平滑扩展也不迟。

3. 日志数据模型设计:表引擎选型是成败关键

日志分析场景下,表结构设计的重要性怎么强调都不为过。用ClickHouse写SQL和MySQL差别不大,但如果不懂它的存储引擎原理,很容易设计出一张“看起来没问题、用起来卡到爆”的表。

3.1 MergeTree家族:从基础到专用,因地制宜

日志分析最常用的就是MergeTree家族,我按实际使用频率给它们排个序:

  • MergeTree:最基础的表引擎,支持主键、分区、数据TTL,适合通用的日志存储分析场景。如果你的日志数据不需要副本,用这个就够了。
  • ReplacingMergeTree:适合需要对同一主键去重的场景。比如你从消息队列里读取日志,可能因为重复投递导致同一条日志被写入了多次,用这个引擎可以在后台合并时按版本号去重。
  • ReplicatedMergeTree:带副本机制的引擎,底层是MergeTree,通过ZooKeeper/Keeper同步多个副本的数据。
  • SummingMergeTree:适合预先聚合的场景。比如你只需要按分钟统计每个服务的请求量、错误量,可以提前把明细数据聚合成汇总行,大大降低存储成本和查询耗时。

我实际用的最多的是ReplicatedMergeTree,因为生产环境要高可用。但这里有个需要注意的点:如果你的集群只有单节点,或者暂时不考虑副本,直接用MergeTree,不要给表加副本机制,否则会引入Keeper依赖和额外的同步开销。

3.2 日志表结构设计:字段、分区与排序键

日志数据有个特点——写多读少、按时间聚合、按维度过滤。表设计要围绕这个特点来。我们以Nginx访问日志为例,这是我设计的表结构:

CREATE TABLE default.nginx_access_log ( `log_time` DateTime64(3) COMMENT '日志时间', `host` String COMMENT '客户端IP', `request_method` String COMMENT '请求方法', `request_uri` String COMMENT '请求URI', `request_protocol` String COMMENT '请求协议', `status` UInt16 COMMENT 'HTTP状态码', `body_bytes_sent` UInt64 COMMENT '响应体大小', `request_time` Float32 COMMENT '请求耗时(秒)', `upstream_response_time` Float32 COMMENT '上游响应耗时(秒)', `user_agent` String COMMENT 'User-Agent', `referer` String COMMENT '来源页面', `server_name` String COMMENT '域名或服务名', `remote_port` UInt16 COMMENT '客户端端口', `timestamp` DateTime64(3) DEFAULT now64(3) COMMENT '写入时间' ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/nginx_access_log', '{replica}') PARTITION BY toYYYYMMDD(log_time) ORDER BY (log_time, host, server_name, status) TTL toDateTime(log_time) + INTERVAL 30 DAY SETTINGS index_granularity = 8192;

简单解释几个关键设计点:

分区键用toYYYYMMDD(log_time),也就是按天分区。这样做的直接好处是,如果只需要查询最近7天的数据,ClickHouse可以快速跳过其他分区的数据,只扫描这7个分区的文件,查询效率大幅提升。但分区粒度也不能太细,如果按小时分区,分区文件数量会暴增,后台合并任务会看不过来,反而拖慢性能。按天是大多数日志场景的黄金平衡点。

排序键是(log_time, host, server_name, status),这个设计很多人容易忽略。ClickHouse的排序键直接影响数据的物理存储顺序,查询时如果能利用排序键做二分查找,效率会高得多。我们的查询模式通常是“先定位到某个时间范围,再按机器IP、服务名、状态码过滤”,所以排序键的顺序也按这个查询频率来排。

TTL设置30天,过期数据自动删除,省去手动清理的麻烦。日志平台一般保留30天明细,更久远的数据要么转存对象存储,要么只保留聚合结果。

3.3 分区粒度估算:不要盲目按天

上面说按天分区是默认选择,但具体情况还是要做点数学题的。分区的合适大小大概在100MB到500MB之间。如果分区太小,比如单天数据量只有几十MB,会产生大量小分区文件,合并线程忙不过来,查询也要打开很多文件;如果分区太大,比如单天几个GB,那么查询一天的数据就要扫描大量数据,跳过无用数据的效果就差了。

估算方法很简单:统计每天的原始日志行数,估算单行日志的大小,然后算一下单天的数据量。

假设单行Nginx日志平均500字节,一天5000万行,一天的数据量=5000万 * 500B = 25GB,这个体量按天分区完全没问题。再比如一天只有10万行,单天数据量不到5MB,按天分区就太碎了,这时候可以考虑按周甚至按月分区。

3.4 Windows和Tomcat日志的建模差异

热搜词里也提到了Windows日志分析和Tomcat日志分析,这两个我都实际做过,建模思路略有不同,顺便展开说说。

Windows日志一般分为系统日志、应用程序日志、安全日志,通过Logstash或Fluent Bit采集后,字段比较固定,比如EventID、Level、ProviderName、Message等。建模的关键点是EventID和Level这两个字段一定要单独存成数值类型,不要和Message混在一起,这样过滤和统计会快很多。Message字段是全文内容,不需要单独建全文索引,ClickHouse用ilike做模糊匹配已经够用。

Tomcat日志有两种典型格式:catalina.out和access log。catalina.out是运行日志,就是Java进程打印的堆栈、异常、业务日志,格式很不规整,我的建议是整体存成一个message字段,配合@timestamp、日志级别、服务名几个维度字段即可。Tomcat的access log就很规整,通常有remote_ip、request_uri、status、bytes_sent、request_time这些字段,设计表结构和Nginx基本一个套路。

4. 数据写入链路:从采集到ClickHouse的完整管道

表结构设计好了,接下来要解决“日志怎么进到ClickHouse”的问题。这里选择太多,但核心要把握一个原则:不管中间用什么组件,最终写入ClickHouse的SQL一定要是批量插入,每一批建议在1万行以上,否则性能完全发挥不出来。

4.1 常见采集方案对比:Logstash vs Fluent Bit vs Vector

我在不同阶段用过不同的采集链路,逐一说说实际感受。

Logstash:最重但最强。输入输出插件极多,过滤能力强悍。缺点是内存占用大、配置复杂。我们早期用它采集,20个采集通道轻轻松松吃掉2个G内存,后来数据量大了不得不换。

Fluent Bit:轻量级采集器,内存占用常年在10-20MB之间,适合部署在每台业务机器上。C语言编写,性能很好,插件生态虽然不如Logstash丰富,但覆盖Nginx、Tomcat、Windows事件日志这些常见场景绰绰有余。我现在的主力采集器就是它。

Vector:Rust编写的采集器,性能和内存表现都不错,最舒服的是它的配置语法非常清晰,管道处理逻辑写起来像写代码一样直观。如果你团队的采集器运维能力比较强,Vector是个好选择。

我的推荐方案是:Fluent Bit做边缘采集,Kafka做消息缓冲,ClickHouse官方提供的clickhouse-kafka引擎或自研消费程序负责最终入库。Kafka这层非常重要,它能削峰填谷,防止ClickHouse因为瞬时写入洪峰导致合并任务堆积或者查询变慢。

4.2 核心入库方式:批量写与异步写的取舍

ClickHouse有几个写入通道,各有适用场景:

  • HTTP接口:官方提供的http://host:8123接口,支持INSERT INTO table FORMAT JSONEachRow之类的格式,适合程序直接写入。
  • clickhouse-client命令行:适合离线导入、批量初始化。
  • JDBC驱动:适合Java系程序写入。
  • Kafka引擎表:直接把Kafka topic映射成一张ClickHouse表,通过物化视图把数据写入真正的明细表,配置起来最省事,也是我目前生产环境用的方案。

用Kafka引擎表入库有一个好处——天然支持批量消费,一个批次默认拉取几千条再写入,不像HTTP接口需要自己攒批。下面是Kafka引擎表的配置示例:

-- 先创建Kafka引擎表,对应Kafka中的原始消息 CREATE TABLE default.nginx_access_log_kafka ( `message` String ) ENGINE = Kafka() SETTINGS kafka_broker_list = 'kafka1:9092,kafka2:9092', kafka_topic_list = 'nginx_access_log', kafka_group_name = 'clickhouse_nginx_consumer', kafka_format = 'JSONAsString', kafka_max_block_size = 65536; -- 通过物化视图将Kafka表的数据解析后写入真正的明细表 CREATE MATERIALIZED VIEW default.nginx_access_log_mv TO default.nginx_access_log AS SELECT JSONExtractString(message, 'log_time') AS log_time, JSONExtractString(message, 'host') AS host, JSONExtractString(message, 'request_method') AS request_method, ... FROM default.nginx_access_log_kafka;

这个方案的好处是写入路径短、无需额外的消费者代码,只要Kafka里的数据格式稳定,ClickHouse就能持续消费并写入。需要注意一点,Kafka引擎表的消费是自动的,如果Kafka里的数据量远大于ClickHouse的处理能力,消费延迟会越来越大。这时候就需要消费者性能监控了。

如果不用Kafka,直接走HTTP接口,我一般会用Python写好批量写入脚本,控制批次大小:

import requests import json def batch_insert_to_clickhouse(rows: list[dict], table: str): """ 将一批日志行批量写入ClickHouse rows: 字典列表 """ # 拼接成JSONEachRow格式 payload = '\n'.join(json.dumps(row, ensure_ascii=False) for row in rows) # 这里用gzip压缩,日志文本重复度高,压缩率很可观 resp = requests.post( f'http://clickhouse:8123/', params={'query': f'INSERT INTO {table} FORMAT JSONEachRow'}, data=payload.encode('utf-8'), headers={'Content-Encoding': 'gzip'}, timeout=30 ) if resp.status_code != 200: raise Exception(f'Insert failed: {resp.text}')

批量大小我一般压到1万到5万行一批,太大容易导致单次请求内存暴涨,太小则写入频率太高、效率上不去。

4.3 数据解析的几种路径:SQL函数解析在入库前还是入库后?

日志原始格式多种多样,有JSON的、有键值对的、有任意文本的。解析逻辑放在哪一层,是一个需要琢磨的问题。

我的原则是:格式越规整,解析越靠前;格式越随意,解析越靠后。

如果是服务端主动推送的JSON日志,建议在Fluent Bit或Logstash阶段就解析好字段,以JSON格式发到Kafka,ClickHouse侧用JSONEachRow直接写入,解析开销最小。如果是Tomcat的catalina.out这种非结构化文本,建议原始文本先入库,查询时再用ClickHouse的字符串函数按需解析。这种方式存储上会多一点开销,但胜在灵活,后续想怎么拆字段都行,不会因为入库时解析错了还得重新回放数据。

ClickHouse的解析函数确实能打,常用的JSONExtractString、splitByChar、extractAllGroupsHorizontal之类的,性能都不错。但需要注意的是,每次查询都要解析一遍,如果数据量大,查询耗时会显著增加。对于高频查询的字段,最好还是入库时就拆好,不要依赖查询时现算。

4.4 写入性能调优的几个关键参数

写入性能始终是日志平台的重点。除了批量插入之外,还有几个参数我也踩过不少坑,整理出来供参考:

  • max_insert_block_size:默认是1048576行,但实际使用中插入块过大可能会导致内存暴涨。我一般设为65万行左右。
  • max_threads:写线程数,默认是CPU核数,没必要调很高,写入性能瓶颈通常在网络和磁盘IO上。
  • index_granularity:默认8192行,意思是每8192行建一个索引标记。日志表如果单行很长,可以适当调小到4096,因为大字段会放大索引文件体积。如果单行很短,则可以调大到16384,减少索引文件数量。
  • parts_to_throw_insert:当数据分区碎片数量过多时,ClickHouse会拒绝新数据写入,防止文件数量无限膨胀。如果高峰期写入快、分区多,适当调大这个值可以避免写入被拒,但也有风险,不建议盲目调。

5. 查询优化实战:让日志分析快起来的几个关键技巧

部署好了、数据也进去了,接下来就是你每天都要面对的日常——写查询、看报表、排查问题。日志分析场景中的查询,和传统OLTP查询有着本质区别,它追求的是扫描大数据集时的高吞吐和低延迟,而不是单条记录的点查。因此,查询优化思路也要跟着变。

5.1 聚合统计的正确姿势:提前用物化视图

日志分析中“实时聚合”是最高频的需求。比如:实时统计最近5分钟每个服务的请求量、错误率、P99耗时。如果每次查询都直接扫描原始明细表做group by,数据量一大就撑不住。

我的做法是:用物化视图把汇总结果实时计算出来,查询时直接查汇总表。ClickHouse物化视图就像是一个“预计算器”,数据写入明细表时,它会自动把聚合结果更新到另外一张表里。

以“按分钟统计请求量、错误量、平均响应时间”为例:

-- 创建聚合结果表 CREATE TABLE default.nginx_access_log_agg_minute ( `log_time` DateTime COMMENT '统计时间(分钟粒度)', `server_name` String COMMENT '服务名', `status_group` String COMMENT '状态码分组:2xx/3xx/4xx/5xx', `request_count` UInt64 COMMENT '请求量', `error_count` UInt64 COMMENT '错误量(4xx+5xx)', `avg_request_time` Float64 COMMENT '平均响应时间', `p95_request_time` Float64 COMMENT 'P95响应时间', `p99_request_time` Float64 COMMENT 'P99响应时间' ) ENGINE = SummingMergeTree() PARTITION BY toYYYYMMDD(log_time) ORDER BY (log_time, server_name, status_group); -- 创建物化视图,实时计算并写入聚合表 CREATE MATERIALIZED VIEW default.nginx_access_log_agg_minute_mv TO default.nginx_access_log_agg_minute AS SELECT toStartOfMinute(log_time) AS log_time, server_name, multiIf(status >= 500, '5xx', status >= 400, '4xx', status >= 300, '3xx', '2xx') AS status_group, count() AS request_count, countIf(status >= 400) AS error_count, avg(request_time) AS avg_request_time, quantile(0.95)(request_time) AS p95_request_time, quantile(0.99)(request_time) AS p99_request_time FROM default.nginx_access_log GROUP BY log_time, server_name, status_group;

这套方案好处是明细数据的聚合是增量计算的,查询时只需扫聚合表里很少的数据,毫秒级返回不是问题。当然代价是占用额外的存储空间和写入CPU成本,但日志场景下这个代价完全值得。

5.2 常见查询模板:按时间范围过滤、状态码统计、Top N接口耗时

聚合统计查汇总表,详细排查还是要查明细表。下面是我日常最常用的几个查询模板,几乎覆盖了日志分析的绝大多数场景。

查询某段时间内所有服务请求量和错误率:

SELECT server_name, count() AS total_count, countIf(status >= 400) AS error_count, round(error_count / total_count * 100, 2) AS error_rate_percent FROM default.nginx_access_log WHERE log_time >= now() - INTERVAL 15 MINUTE AND log_time < now() GROUP BY server_name ORDER BY total_count DESC LIMIT 20;

查询最近1小时最慢的20个接口:

SELECT request_method, request_uri, server_name, quantile(0.99)(request_time) AS p99_time FROM default.nginx_access_log WHERE log_time >= now() - INTERVAL 1 HOUR GROUP BY request_method, request_uri, server_name ORDER BY p99_time DESC LIMIT 20;

按IP维度统计访问频次,识别异常来源:

SELECT host AS client_ip, count() AS request_count, uniqCombined(server_name) AS unique_services, max(log_time) AS last_access_time FROM default.nginx_access_log WHERE log_time >= now() - INTERVAL 1 DAY GROUP BY client_ip ORDER BY request_count DESC LIMIT 50;

这里有几个函数值得关注。uniqCombined是ClickHouse专门为大数据量去重统计设计的,比count(DISTINCT)快得多,误差也在可接受范围内。quantile系列函数用于分位数计算,日志分析里的P95、P99靠它来实现。

5.3 全文检索怎么做:ilike和match的取舍

很多从ES迁移过来的同学,最不适应的就是全文检索能力。ClickHouse没有倒排索引(新版本其实已经加了实验性支持),但用对函数,很多场景依然能应付。

短文本模糊搜索,优先用ilike:

SELECT count() FROM default.nginx_access_log WHERE request_uri ILIKE '%/api/v1/order%' AND log_time >= now() - INTERVAL 10 MINUTE;

ilike是大小写不敏感的模糊匹配,ClickHouse对它做了优化,在排序键前缀匹配的情况下能走索引加速。如果模糊匹配的字段是request_uri这种高频查询字段,可以考虑把它加入排序键的靠前位置,效果会明显。

复杂正则匹配,用match,但要注意性能:

SELECT count() FROM default.nginx_access_log WHERE match(request_uri, '^/api/v[0-9]+/order/\\d+$') AND log_time >= now() - INTERVAL 10 MINUTE;

match走的是正则引擎,性能开销远大于ilike,在大数据集上做全表正则扫描,CPU一下就打满了。我一般只在ilike满足不了需求的场景才用正则。

如果你想做真正的全文检索,比如搜索Message字段里的任意关键词,ClickHouse其实也提供了基于ngram的倒排索引,需要在建表时显式声明:

INDEX idx_message message TYPE ngrambf_v1(4, 100000, 0, 0) GRANULARITY 4

这个索引适合“包含某个词”的高频查询,原理是提前生成ngram布隆过滤器,查询时先过滤大部分不相关的数据块,再对剩余数据做精确匹配。效果比不上ES的全文索引,但在日志关键词过滤场景下,性能提升还是相当明显的。

6. 可视化与告警:日志平台最后一块拼图

日志分析不只是写SQL查数,最终还是要展示给别人看、要能在异常时第一时间通知到人。这一块我用的方案是Grafana加Alertmanager,原因很简单:社区插件成熟、配置灵活、和ClickHouse数据源对接顺畅。

6.1 Grafana对接ClickHouse的配置要点

Grafana对接ClickHouse,推荐用官方插件clickhouse-datasource。安装好插件、配置好数据源之后,最关键的是查询语言的写法。

Grafana的ClickHouse数据源支持两种查询模式:SQL模式和Table模式。我一般用SQL模式,直接写聚合SQL,然后绑定时间字段。需要注意的一个坑是,Grafana的模板变量和ClickHouse的语法要配合好,例如:

SELECT $timeSeries AS t, count() AS request_count FROM default.nginx_access_log WHERE log_time >= $timeFilter AND server_name IN ($server_names) GROUP BY t ORDER BY t;

这里的$timeSeries、$timeFilter、$server_names都是Grafana的宏变量,会被自动替换为实际的时间范围和选中的服务名。设置好之后,就能在Dashboards上看到按时间序列动态刷新的流量曲线和状态码分布了。

6.2 常用监控面板搭建思路

我搭建的日志监控面板包含这样几个核心图表,每个都对应一类实际问题:

  • QPS和错误率总览:按分钟展示总请求量、各服务请求量、4xx/5xx错误率,一眼看出整体健康状况。
  • 接口Top N耗时:按P99耗时排序,展示最慢的接口,定位性能瓶颈。
  • IP访问Top榜:按请求量排序的客户端IP,辅助发现异常访问和攻击行为。
  • 状态码饼图:展示当前时间窗口内2xx/3xx/4xx/5xx状态码占比,快速判断流量质量。
  • Tomcat/Windows系统日志级别分布:展示error/warn/info级别分布,Java堆栈异常和系统错误一目了然。

6.3 告警规则怎么配才不“狼来了”

告警配置最大的坑就是“告警风暴”。配置太松,出了问题没人看;配置太紧,天天被噪音打扰,最终大家选择性忽略。我的经验是分层级配置:

  • 紧急告警(页面/电话):比如5xx错误率连续5分钟超过5%、P99响应时间连续5分钟超过2秒、某个服务写入量突然降为0。
  • 警告告警(IM群):比如错误率超过2%持续10分钟、请求量较基线突降30%等。
  • 信息告警(日报汇总):每天汇总各项指标,不实时打扰。

Grafana Alerting的规则配置比较直观,选择查询、设置阈值、配置通知渠道即可。建议大家一定要设定“持续多长时间才触发”这个条件,避免瞬时抖动引起误报。

7. 运维踩坑实录:那些官方文档没写明白的坑

这部分是全文最有价值的部分,也是我踩坑踩得最多的地方。每一个问题都是真实遇到、花了不少时间排查才解决的,希望能帮你少走弯路。

7.1 重启报错:failed to flush system log already exists

这个报错是热搜词里提到的,说明遇到的人不少。我第一次遇到是在一次版本升级后的重启操作中,当时看到这个报错一脸懵:

DB::Exception: Cannot flush system log, because flush failed: Code: 57. DB::Exception: Table default.system.query_log already exists.

查了一圈,原因是ClickHouse的系统日志表(query_log、query_thread_log等)在重启恢复时,尝试重建一个已经存在的同名表对象,导致冲突。这个问题的根源通常是系统表元数据损坏或版本升级后残留数据不一致。

我的处理办法是:

-- 在另一个未受影响的节点或从其他客户端连接,先尝试删除系统日志表对应的物理文件 -- 实际上更安全的做法是备份数据文件后,执行: DETACH TABLE system.query_log; ATTACH TABLE system.query_log;

如果DETACH/ATTACH操作不成功,就直接找系统表对应的目录(通常在/data/clickhouse/data/system/下),备份后删除query_log相关目录,再重启服务。重启后ClickHouse会自动重建这些系统表,问题就解决了。

不过这里要提醒一下,操作前一定要做好数据目录的备份,系统表虽然不存业务数据,但误删可能导致元数据不一致,引发更大的问题。

7.2 磁盘空间告急:日志TTL为什么不生效?

我们上线初期遇到过一个诡异问题:明明表里配置了TTL,磁盘空间却仍然持续上涨,一度把数据盘给撑满了。排查之后发现,TTL只在数据合并(merge)之后才真正删除过期分区里的数据,而当时因为写入量太大、合并任务一直被推迟,导致过期数据迟迟未被物理清除。

解决方法是手动触发合并,或者降低后台合并任务的优先级门槛:

-- 强制触发分区合并,清理过期数据 OPTIMIZE TABLE default.nginx_access_log FINAL;

OPTIMIZE TABLE ... FINAL是强制合并所有分区,把过期的TTL数据物理删除。需要注意的是,这个操作比较重,如果表很大的话会占用不少CPU和IO,最好在业务低峰期执行,不要频繁使用。

另一个经验是:TTL清理有滞后性,磁盘规划时要留足余量。我一般会预留30%左右的空闲空间用于应对TTL滞后和合并临时文件的空间开销。

7.3 Too many parts 异常的应对策略

存储到一定量之后,你可能会在日志里看到类似这样的错误:

DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts.

这个报错表示某个分区下的小文件数量(parts)超过阈值了,ClickHouse拒绝新的写入,以防文件数量继续膨胀。原因通常是写入频率太高、或者分区选择不当(比如按小时分区但每个小时数据量少),导致产生大量小parts,而合并的速度跟不上插入速度。

我的处理措施有几步:

  1. 降低写入频率:把批量从每批1万行提升到每批5万行,减少part数量。
  2. 调整分区粒度:如果按小时分区,改为按天分区,减少分区总数。
  3. 调大合并线程数:在配置文件里调整background_pool_size参数,增加后台合并任务的并发能力。
  4. 临时措施:调高parts_to_throw_insert阈值,至少保证数据能写进去,但这是临时方案,治标不治本。

最终根子上的解法还是控制分区数量和批量插入行数,让part生成速度慢于合并速度,这个平衡关系要配合监控来调。

7.4 高峰期查询超时:内存不够和CPU被打满怎么办

日志平台上线一个月后,问题开始暴露在查询侧。每天10:00和14:00的流量高峰,dashboard加载很慢,有的聚合查询直接超时。排查后发现,有些BI同事写的SQL没有加时间范围条件,直接对全表数据跑了一次深度聚合,把CPU和内存都打满了。

我的解决方式是从两个层面入手。

第一,在ClickHouse用户配置中限制单次查询的资源消耗,将大查询隔离到独立的资源池:

<profiles> <default> <max_memory_usage>8000000000</max_memory_usage> <max_execution_time>60</max_execution_time> </default> <readonly> <max_memory_usage>2000000000</max_memory_usage> <max_execution_time>20</max_execution_time> </readonly> </profiles>

第二,通过Grafana的查询编辑规范,强制要求所有可视化查询必须绑定时间范围,从源头上避免全表扫描。配合ClickHouse的system.query_log表,定期分析慢查询,把高频次、高耗时的SQL整理出来逐条优化。

7.5 多节点集群数据倾斜:分片键选择要慎重

上集群之后,我们又遇到一个头疼的问题:三个分片的磁盘使用率不一样,其中一个已经70%,另外两个才30%。查了一圈,问题出在分片键的选择上。

我们之前用rand()作为分片键,理论上数据是均匀分配的,但实际场景中,某个大客户的服务日志量特别大,rand()虽然能保证行数均匀,却不能保证“数据大小”均匀——大字段日志会集中在某些行里。

解决办法是把分片键改为cityHash64(server_name),按服务名做哈希分片,这样同一个服务的日志会落到同一个分片,数据量的大小分布就会均衡很多。当然了,这样也会带来一个新问题:单个服务日志量暴增时,对应的分片会首先变热。这个就需要根据业务情况权衡,没有任何一种分片策略是万能的。

8. 日志分析场景的横向对比:ClickHouse、ES和Loki怎么选

关于选型,最后用一点篇幅做个总结性对比。很多朋友问“ClickHouse是不是能完全替代ES”,我个人观点是:在日志分析这个细分领域,ClickHouse确实能替代大部分ES场景,但并不能在所有场景下完全替代。

先从几个维度做个客观对比:

维度ClickHouseElasticsearchLoki
存储引擎列式存储,压缩比高倒排索引+DocValues,存储开销大对象存储+索引,轻量设计
写入性能极高,批量写入可达百万行/秒中等,受限于索引构建开销高,依赖对象存储
聚合分析极强,向量化执行引擎中等,聚合深了容易OOM较弱,主要面向日志检索
全文检索较弱,支持有限极强,核心能力所在标签过滤较强,正文检索较弱
运维成本较低,单机能力突出较高,集群和内存调优复杂低
适用位置结构化日志、指标分析非结构化全文检索、业务搜索轻量日志查看、云原生环境

从实用性角度来说,如果你需要“全文搜原始日志内容”的场景非常多,比如“搜索所有日志里包含某个用户ID或错误堆栈的原始内容”,那ES依然是更好的选择,倒排索引在这种场景下的优势是ClickHouse目前无法取代的。

但如果你更关心的是“有多少请求、错误率多高、哪些接口最慢、哪个IP在频繁访问”——也就是所有基于结构化字段的聚合统计分析,ClickHouse是明显更优的选型。性能和成本都是碾压级的优势。

Loki则更偏向“只需要能看日志、不需要复杂分析”的轻量场景,尤其适合云原生和Kubernetes环境下,成本要求敏感、有对象存储可用的情况。

所以我最终的选型建议是:

  • 日志量小、查询简单:直接用Loki或Elasticsearch,怎么方便怎么来。
  • 日志量大、分析需求为主:ClickHouse,尤其是本文介绍的表结构和查询方案。
  • 日志量大且全文搜索需求很强:可以考虑ClickHouse+ES混合方案,热数据进ES供搜索,全量数据进ClickHouse供聚合分析。虽然复杂一点,但各有侧重、各得其所。

我是一个实用主义者,选型只信一条:用最合适的工具解决当前最重要的问题。ClickHouse在日志分析领域的生态还在不断完善,但它作为“大数据量下的日志聚合分析引擎”这个定位,已经非常成熟可靠了。如果你正在日志分析的技术选型上犹豫,不妨先用本文的方案搭一套POC,拿自己真实的日志数据跑一跑,数值会替你做出最后的决定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询