日志系统的架构演进——从本地文件到 ELK 再到 ClickHouse 的存储选型

发布时间:2026/7/23 8:50:48

日志系统的架构演进——从本地文件到 ELK 再到 ClickHouse 的存储选型 日志系统的架构演进——从本地文件到 ELK 再到 ClickHouse 的存储选型一、日志系统的问题不是存不下而是查不动大多数 Java 后端团队的日志体系是从log4j写本地文件开始的。随着服务实例从几台增长到几百台运维开始通过rsyslog或 Filebeat 将日志集中到一台 ELK 集群Elasticsearch Logstash Kibana。这套架构在日志量小于每天 500GB 时工作良好搜索延迟在 2 秒以内。但当日志量增长到每天 2TB 时——主要来自微服务间的 RPC 调用日志和网关访问日志——ES 集群的写入压力和查询延迟同时爆发。核心瓶颈在 ES 的倒排索引机制。每条日志的 message 字段都会被分词建立倒排索引这对全文搜索来说是特征但对日志场景来说是负担——大多数日志查询只需要按时间范围 按 traceId或按服务名 按错误级别不需要对日志内容做全文检索。另外定时删除旧索引会产生大量墓碑和段合并开销影响在线写入。二、从 ELK 到 ClickHouse按列存储 冷热分层新架构的核心理念是不同用途用不同的存储引擎。Elasticsearch 保留 3 天的热数据仅用于实时指标仪表盘和故障快速定位。ClickHouse 作为温存储保留 90 天数据承担所有历史日志查询——按 traceId 定位完整调用链、按时间段做错误率统计、按服务名做 QPS 趋势分析。超过 90 天的日志压缩为 Parquet 格式存入对象存储做冷归档只在合规审计时回读日常不占用在线资源。日志缓冲层选择了 Kafka 来解耦采集和存储。Kafka 起到了削峰作用——在发布期间日志量会从每秒 5 万条暴增到 30 万条下游 ClickHouse 的写入速率保持稳定不被瞬时峰值打垮。同时 Kafka 的多 Topic 设计允许不同的消费者独立消费Logstash 消费原始日志做解析和清洗后写入 ClickHouse而实时告警模块也消费同一份 Kafka 数据做异常模式检测。三、ClickHouse 日志表设计与 Java 写入ClickHouse 的日志表设计决定了查询效率和存储成本。下面是应用日志表的 DDL 和通过 JDBC 批量写入的示例。-- ClickHouse 应用日志表设计 CREATE TABLE app_logs_local ON CLUSTER default ( timestamp DateTime64(3), service LowCardinality(String), level LowCardinality(String), trace_id String, span_id String, message String, exception String CODEC(ZSTD(3)), host_ip IPv4, cost_ms UInt32, http_status UInt16 ) ENGINE MergeTree() PARTITION BY toYYYYMMDD(timestamp) ORDER BY (service, level, toUnixTimestamp(timestamp)) TTL timestamp INTERVAL 90 DAY;public class ClickHouseLogWriter { private final JdbcTemplate clickHouseTemplate; private final BlockingQueueListObject[] batchBuffer; private static final int BATCH_SIZE 5000; private static final long FLUSH_INTERVAL_MS 2000; public void write(ListAppLog logs) { ListObject[] params logs.stream() .map(this::toParams) .collect(Collectors.toList()); batchBuffer.add(params); } private Object[] toParams(AppLog log) { return new Object[]{ log.timestamp(), log.service(), log.level(), log.traceId(), log.spanId(), log.message(), log.exception(), log.hostIp(), log.costMs(), log.httpStatus() }; } Scheduled(fixedDelay FLUSH_INTERVAL_MS) public void flushBatch() { ListListObject[] batches new ArrayList(); batchBuffer.drainTo(batches, 10); for (ListObject[] batch : batches) { try { clickHouseTemplate.batchUpdate( INSERT INTO app_logs (timestamp, service, level, trace_id, span_id, message, exception, host_ip, cost_ms, http_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), batch); } catch (DataAccessException e) { // 写入失败降级到本地文件防止日志丢失 fallbackWriter.writeToFile(batch); } } } }应用日志写入 ClickHouse 采用批量提交模式每批 5000 条或每隔 2 秒刷一次避免单条插入的高延迟。写入失败时降级到本地文件DBA 可以在集群恢复正常后手动导回。ClickHouse 的 MergeTree 引擎在批量写入 5000 条以下时表现最优——写入延迟在 25ms 以内磁盘 IO 可接受。四、查询优化与成本对比ClickHouse 的最大优势在于对列的扫描而不是对行的扫描。同一个查询统计过去 30 天内 order-service 的 ERROR 日志条数在 ES 上需要扫描 80GB 的索引数据需要 5 个分片聚合耗时 8 秒在 ClickHouse 上只需要读取level和service两个列的数据按分区裁剪历史分区耗时 0.3 秒查询速度快了 25 倍以上。存储成本也有显著差异。同样的 90 天日志数据ES 集群需要 12 台物理机每台 256GB 内存 8TB SSD月度成本约 24 万元。新架构中 ES 缩到 3 台仅存 3 天数据ClickHouse 用 6 台每台 128GB 内存 24TB HDD月度成本约 12 万元降低 50%。日志治理层面也做了规范化。统一了日志格式为 JSON 输出强制每个服务在日志中包含 traceId 和 spanId禁止生产环境输出 debug 级别以上的大对象日志超过 10KB 的日志截断告警。这些治理措施在升级存储引擎之前就完成了因为存储引擎的优化效果很大程度依赖于日志格式的规范程度。五、总结日志系统从 ELK 到 ClickHouse 的演进来本质上是认识到日志查询不需要全文倒排索引而需要高效的列式扫描和压缩比。ClickHouse 在时间范围、字段过滤和聚合查询上的性能远超 ES同时存储成本更低。架构的关键设计是 Kafka 做削峰缓冲、冷热分层存储、批量写入模式以及日志格式的强制规范。

相关新闻