痛点:ELK 越用越贵,查询越来越慢
日志量上了 TB 级,ELK 的问题就暴露了:
- 存储成本飙升:Elasticsearch 默认全文索引,100GB 原始日志存进去膨胀到 300GB+,SSD 费用月增几千块
- 查询变慢:日志超过 7 天的历史查询动辄 30 秒,Kibana 转圈转到怀疑人生
- 运维负担重:JVM 调优、分片再平衡、节点扩容,ES 集群自身的运维量不亚于业务系统
- 资源浪费:大多数运维场景只需要
WHERE + GROUP BY + ORDER BY,根本用不到全文检索
如果你 90% 的日志查询是 "某时间段 + 某服务 + 关键字过滤 + 聚合统计",ClickHouse 是更优解。
方案:ClickHouse 列式存储 + MergeTree 引擎
ClickHouse 是 OLAP 列式数据库,天生适合日志分析场景:
| 对比项 | Elasticsearch | ClickHouse |
|---|---|---|
| 存储模型 | 倒排索引 + 行存 | 列式压缩存储 |
| 压缩率 | 1:3(膨胀) | 10:1(压缩) |
| 10亿行聚合查询 | 10-60s | 0.2-2s |
| 内存占用 | 高(JVM堆) | 低(按需加载列) |
| 全文检索 | 原生支持 | 有限(需 tokenbf_v1 索引) |
| 运维复杂度 | 高 | 中等 |
核心结论:放弃全文检索能力,换来 10 倍压缩和 50 倍查询速度提升——对运维日志分析来说,这笔账划算。
实操步骤
第 1 步:部署 ClickHouse(Docker 单节点快速验证)
# 创建数据目录
mkdir -p /data/clickhouse/{data,logs}
# 启动 ClickHouse
docker run -d \
--name clickhouse-server \
--ulimit nofile=262144:262144 \
-p 8123:8123 \
-p 9000:9000 \
-v /data/clickhouse/data:/var/lib/clickhouse \
-v /data/clickhouse/logs:/var/log/clickhouse-server \
clickhouse/clickhouse-server:24.8
# 验证连接
docker exec -it clickhouse-server clickhouse-client --query "SELECT version()"
生产环境建议 3 节点 ReplicatedMergeTree + 2 分片,但单节点即可抗住日均 50GB 日志。
第 2 步:设计日志表结构
CREATE TABLE logs.app_logs
(
-- 时间字段:必须是排序键第一列
timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1)),
-- 结构化字段
service LowCardinality(String),
level LowCardinality(String), -- INFO/WARN/ERROR
host LowCardinality(String),
trace_id String CODEC(ZSTD(1)),
-- 日志正文
message String CODEC(ZSTD(3)),
-- 扩展属性(JSON 字段动态提取)
attributes Map(String, String) CODEC(ZSTD(1))
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (service, level, timestamp)
TTL timestamp + INTERVAL 30 DAY DELETE
SETTINGS index_granularity = 8192;
-- 二级索引:加速 message 关键字搜索
ALTER TABLE logs.app_logs
ADD INDEX idx_message message TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4;
-- 二级索引:加速 trace_id 精确查找
ALTER TABLE logs.app_logs
ADD INDEX idx_trace_id trace_id TYPE bloom_filter(0.01) GRANULARITY 1;
关键设计决策:
PARTITION BY toYYYYMMDD:按天分区,TTL 自动清理过期数据,DROP PARTITION秒删历史ORDER BY (service, level, timestamp):最常见查询模式是"某服务 + 某级别 + 时间范围",排序键直接命中LowCardinality:枚举型字段用字典编码,存储再压缩 5-10 倍CODEC(ZSTD):message 字段用 ZSTD 高压缩比,牺牲少量 CPU 换存储空间tokenbf_v1:布隆过滤器索引实现近似全文搜索,不如 ES 精确但够用
第 3 步:用 Vector 采集日志写入 ClickHouse
Vector(Datadog 开源)比 Logstash/Fluentd 性能高 10 倍且资源占用极低,是日志管道首选:
# /etc/vector/vector.toml
# 采集来源:读取容器日志
[sources.docker_logs]
type = "docker_logs"
include_containers = ["app-*", "api-*"]
# 解析:提取结构化字段
[transforms.parse]
type = "remap"
inputs = ["docker_logs"]
source = '''
# 解析 JSON 格式日志
parsed = parse_json(.message) ?? {}
.timestamp = to_timestamp(parsed.ts) ?? now()
.service = parsed.service ?? .container_name
.level = upcase(parsed.level ?? "INFO")
.host = get_hostname!()
.trace_id = parsed.trace_id ?? ""
.message = parsed.msg ?? .message
.attributes = {}
# 把其余字段丢进 attributes map
del(.container_name)
del(.container_id)
'''
# 输出:批量写入 ClickHouse
[sinks.clickhouse]
type = "clickhouse"
inputs = ["parse"]
endpoint = "http://clickhouse-server:8123"
database = "logs"
table = "app_logs"
compression = "gzip"
batch.max_bytes = 10485760 # 10MB 一批
batch.timeout_secs = 5
# 健康检查
healthcheck.enabled = true
启动 Vector:
docker run -d \
--name vector \
-v /etc/vector:/etc/vector:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
timberio/vector:0.42.0-alpine
第 4 步:高频查询示例
-- 最近1小时 ERROR 日志统计(按服务 Top 10)
SELECT service, count() AS err_count
FROM logs.app_logs
WHERE level = 'ERROR'
AND timestamp >= now() - INTERVAL 1 HOUR
GROUP BY service
ORDER BY err_count DESC
LIMIT 10;
-- 全链路追踪:根据 trace_id 拉出完整调用链
SELECT timestamp, service, level, message
FROM logs.app_logs
WHERE trace_id = 'abc-123-def-456'
ORDER BY timestamp;
-- 模糊搜索:message 包含 "timeout"
SELECT timestamp, service, message
FROM logs.app_logs
WHERE hasToken(message, 'timeout')
AND timestamp >= now() - INTERVAL 24 HOUR
ORDER BY timestamp DESC
LIMIT 100;
-- 日志趋势图:每 5 分钟 ERROR 数量
SELECT
toStartOfFiveMinutes(timestamp) AS ts,
count() AS cnt
FROM logs.app_logs
WHERE level = 'ERROR'
AND timestamp >= now() - INTERVAL 6 HOUR
GROUP BY ts
ORDER BY ts;
10 亿条日志中执行上述查询,ClickHouse 通常在 0.5-3 秒内返回,ES 同等查询需要 15-60 秒。
避坑指南
坑 1:不要用 String 存低基数字段
-- 错误:host 只有 100 种值,用 String 浪费空间
host String
-- 正确:LowCardinality 字典编码
host LowCardinality(String)
低基数字段(< 10000 种取值)务必加 LowCardinality,存储减少 5-10 倍,查询加速 2-3 倍。
坑 2:写入不要一条条 INSERT
ClickHouse 每次 INSERT 生成一个 data part,频繁小批量写入会导致 "too many parts" 错误。
正确做法:攒批写入,每批 1000-10000 条或 5-10 秒刷一次。Vector/Kafka Connect 默认就是批量模式,别自己写单条 INSERT 循环。
坑 3:ORDER BY 顺序决定查询性能
排序键的列顺序 = 索引效率。如果你最常按 service 过滤,它必须排在前面:
-- 好:先 service 再 timestamp,WHERE service = 'x' 走索引
ORDER BY (service, level, timestamp)
-- 差:timestamp 在前,按 service 查要全表扫描
ORDER BY (timestamp, service, level)
设计排序键前先统计 Top 10 查询模式,让最高频的 WHERE 条件命中排序键前缀。
可视化方案
ClickHouse 不自带 UI,推荐搭配:
- Grafana + ClickHouse 数据源插件:直接写 SQL 做 Dashboard,替代 Kibana
- Tabix:轻量 Web SQL 客户端,做 Ad-hoc 查询
- Superset:复杂报表和数据探索
Grafana 配置 ClickHouse 数据源只需填 HTTP 地址 + 用户名即可,5 分钟搞定。
总结
| 维度 | 迁移前(ELK) | 迁移后(ClickHouse + Vector) |
|---|---|---|
| 日均 100GB 日志存储 | ~300GB(含索引膨胀) | ~30GB(10:1 压缩) |
| 7 天历史查询 | 15-60s | 0.5-3s |
| 单节点月成本 | ~$400(SSD+内存) | ~$80(普通磁盘即可) |
| 运维复杂度 | 高(JVM + 分片调优) | 中(原生集群,配置简单) |
适用场景:日志量 > 10GB/天、以结构化查询为主、不强依赖全文检索的团队。
不适用场景:需要自然语言全文搜索、已深度绑定 Kibana 生态且日志量小的团队。
迁移路径建议:先双写(ELK + ClickHouse 并行)跑 2 周,对比查询结果和性能,确认无误后切流、下线 ES 节点。