当一个React项目的线上监控积累了数十亿条日志后,ELK Stack(Elasticsearch、Logstash、Kibana)的性能瓶颈会逐渐暴露出来:聚合查询变慢、索引膨胀导致磁盘吃紧、JVM内存压力让集群频繁出现Full GC。不少团队在权衡之后选择迁移到ClickHouse,通常能在查询延迟和存储成本上获得数倍甚至数十倍的改善。本文结合一次真实的迁移经历,聊聊整个改造过程中的思路、方案和踩过的坑。

为什么要放弃ELK Stack:先搞清楚性能瓶颈在哪
ELK的核心是Elasticsearch,它本质是一个搜索引擎,基于Lucene的倒排索引结构,为关键词全文检索做了大量优化。这套结构在日志检索的早期非常够用,但日志分析的场景有一个特点:写入量极大,查询则以时间范围过滤加聚合统计为主,真正的高频词搜索反而不多。
这就产生了矛盾。倒排索引为每一条日志的每个分词都建立索引,写入放大非常明显。以我们一个中型React电商项目为例,每天产生约20GB原始日志,在Elasticsearch中占用约45GB磁盘空间,而且随着字段基数增加,索引体积还会继续膨胀。更麻烦的是聚合查询,比如统计某个页面过去24小时按小时分组的JS错误数量,这类查询在Elasticsearch中需要遍历大量倒排表并做Doc Values聚合,数据量上来后响应时间轻松突破10秒。
此外Elasticsearch是JVM系应用,堆内存管理是个持续性的运维负担。集群规模扩大后,为了保证稳定性通常要做冷热分层、索引生命周期管理(ILM),整套方案的复杂度不低。团队维护成本上升的同时,查询性能却没有随着硬件投入线性增长,这就是我们下决心迁移的直接原因。
ClickHouse凭什么快:列式存储与日志查询的天然契合
ClickHouse是面向列的存储引擎,数据按列落盘,查询时只读取需要的列。日志分析中典型的查询往往只涉及少数几个字段,比如时间戳、日志级别、服务名、错误消息,列式存储可以跳过大量无关数据,I/O量直接下降一个数量级。
再配合几个关键特性,ClickHouse在日志场景的优势会被进一步放大。首先是压缩能力,列数据的相似度高,采用LZ4或ZSTD压缩后,同样的20GB日志在ClickHouse中通常只占4到8GB,存储成本约为Elasticsearch的五分之一。其次是向量化执行引擎,聚合计算全程SIMD加速,count、group by这类操作的速度远超逐行处理的模式。最后是物化视图,可以在写入时自动做预聚合,把最重的查询变成对结果表的简单扫描。
需要注意ClickHouse的短板也很明确:分布式JOIN性能一般,高并发点查不是它的强项,更新删除是异步的Mutation操作。对日志分析来说,这些恰好都不是核心诉求,日志基本是append-only写入,查询以扫描聚合为主,所以匹配度很高。
迁移实战:表结构设计与采集链路改造
设计适合日志的MergeTree表结构
表结构设计是迁移中最关键的一步。日志表按天分区,按时间排序,并针对常用过滤条件添加跳数索引(Skip Index),可以在扫描层面再砍掉大量无效读取。下面是我们实际使用的建表语句:
CREATE TABLE logs.frontend_events
(
event_time DateTime,
event_date Date DEFAULT toDate(event_time),
level LowCardinality(String),
service LowCardinality(String),
page_url String,
error_msg String,
user_id String,
duration_ms UInt32,
message String CODEC(ZSTD(3)),
INDEX idx_msg error_msg TYPE bloom_filter() GRANULARITY 4,
INDEX idx_user user_id TYPE minmax GRANULARITY 4
)
ENGINE = MergeTree()
PARTITION BY event_date
ORDER BY (event_date, service, event_time)
TTL event_date + INTERVAL 30 DAY;
几个设计要点值得展开。LowCardinality针对level、service这类低基数字段能显著减少存储并加速分组。bloom_filter索引适合error_msg这种需要模糊匹配的字符串。TTL子句让过期日志自动清理,替代了原来ELK中ILM的角色。ORDER BY把service放在时间前面,是因为我们大部分查询都会先按服务过滤,这样排序键能更充分利用主键稀疏索引。
采集链路从Logstash改为直接写入
原来前端React应用上报的日志经过网关收集,再由Logstash解析后写入Elasticsearch。迁移后链路简化为:前端通过beacon或fetch上报到日志网关,网关批量攒够一批后通过HTTP接口直接写入ClickHouse。如果担心ClickHouse写入压力,可以在中间加一层Kafka做缓冲,ClickHouse用Kafka表引擎消费,整条链路的组件数量反而比ELK时代更少。
有一个细节要注意:ClickHouse推荐小批量写入,单次insert建议至少几千行,否则会产生大量小part影响合并性能。网关侧一定要做攒批逻辑,比如每5秒或每5000条触发一次写入。
React日志看板对接:查询改写与前端优化
查询侧的改写工作量不小。Kibana的Lucene查询语法不能用了,需要把常用查询翻译成SQL。好在ClickHouse的SQL非常完整,翻译难度不大。例如原来统计错误趋势的查询,改写后如下:
SELECT
toStartOfHour(event_time) AS hour,
count() AS error_count,
uniqExact(user_id) AS affected_users
FROM logs.frontend_events
WHERE event_date >= today() - 1
AND level = 'error'
AND service = 'web-react-app'
GROUP BY hour
ORDER BY hour;
查询条件中显式带上event_date是关键技巧。虽然ClickHouse会做主键裁剪,但显式的日期条件能让分区裁剪更精准。另外uniqExact在数据量大时会比较慢,如果不需要精确去重,换成uniq可以快好几倍。
前端React看板我们直接用现有的管理后台改造,通过一个Node.js中间层提供REST接口,内部调用clickhouse-client执行SQL。为了避免大查询拖垮前端体验,中间层做了两件事:一是对所有查询强制加时间范围上限,超过7天的聚合必须走预聚合表;二是利用ClickHouse的max_execution_time设置兜底超时,防止慢查询堆积。
迁移效果与踩坑总结
迁移完成后我们做了一轮对比压测,结果相当可观。存储方面,30天日志从Elasticsearch的约1.3TB降到ClickHouse的260GB左右。查询方面,原来Kibana中8到15秒的聚合面板,在ClickHouse中普遍降到300毫秒到1秒;最重的按用户去重统计从超时降到2秒内返回。集群规模也从6台ES节点缩减为3台ClickHouse节点,机器配置要求还更低。
踩过的坑也记录一下。第一,刚迁移时按单条insert写入,很快出现too many parts报错,改成批量写入后解决。第二,物化视图的目标表要提前建好,并且字段类型必须与源表严格对齐,否则写入会直接失败。第三,如果团队确实离不开Kibana的界面,可以考虑Grafana,它对ClickHouse有成熟的插件支持,仪表盘迁移成本比自建React看板更低,这一点在方案选型时值得先评估。
总体来说,如果日志场景以写入为主、查询以聚合统计为主、全文检索需求不重,ClickHouse几乎在所有维度都优于Elasticsearch。但如果业务强依赖复杂全文搜索和高亮,就需要慎重考虑,或者在架构中让两者并存,各司其职。技术选型没有银弹,理解自身查询模式再做决策,才是迁移成功的前提。
ClickHouse日志分析ELK Stack迁移React性能优化修改时间:2026-09-12 06:24:39