饮墨

子安饮墨馀三斗,留与卿儿作赋来

用 ClickHouse 替代 ELK 做日志分析:存储降 80%、查询快 50 倍的实操方案

痛点: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 节点。

您还没有登录,请登录后发表评论。