日志分析系统怎么选?ELK、Loki、ClickHouse 三年踩坑实录与成本对比

🔑 关键词:日志分析,ELK,Loki,ClickHouse,Fluent Bit

📖 摘要:从采集端 CPU 占用到索引存储成本,用我实际环境里的实测数据对比 ELK、Loki、ClickHouse 三套日志方案的取舍,附高频故障排查步骤、关键参数配置和一份并不中听的选型结论。

日志分析系统怎么选?ELK、Loki、ClickHouse 我全用了一遍

图片

2022 年 12 月,凌晨 2 点 47 分,手机在床头柜上震个不停。报警内容很简单:ES 集群 CPU 打到 96%,bulk 写入拒绝率 38%,已经持续 11 分钟。我爬起来连 VPN,先看 Kibana 的 slow log,发现罪魁祸首是白天一个同事跑的查询 —— message:*timeout*,没加时间范围,从 2021 年 6 月一路扫到今天,把三个 data node 的 IO 全吃满,连带把实时写入也拖死了。那一刻我才真正想明白:大部分团队在日志上花的钱,一大半是被“怎么查”这件事烧掉的,而不是“存了多少”。

这篇文章我不打算再讲一遍 ELK 是什么、Loki 是什么,那种文章一搜一大把。我只讲我踩过的坑、我测出来的数字,以及我最后为什么放弃了“一套方案打天下”这个念头。顺便说一句,下面所有的性能数字都来自我自己维护的集群,硬件和日志重复度不一样结果会差挺多,别当基准测试看。

一、第一个瓶颈永远在采集端,不在后端

刚做日志的时候我理所当然上了 Logstash,理由很朴素:插件多、文档全、社区大。结果第一个月就被打脸。当时是 8 台业务机,每台 4C8G,日志量加起来 400GB/天左右,我铺了 4 个 Logstash 实例做聚合,每个 -Xmx4g。

三周后开始出问题。业务高峰期老年代一直涨到 3.2G 才触发 full GC,每次停顿 1.5~2 秒,停顿期间 event queue 堆积,queue.max_bytes 默认 1GB 顶不住,直接开始丢数据。我去翻 pipeline 日志,发现是 grok 正则写得太贪,一个 %{GREEDYDATA} 把整行 2KB 的 stack trace 全吞进去做回溯匹配,单条 event 处理耗时从 0.3ms 涨到 7ms。

换 Fluent Bit 之后,同样的量,我在每台 node 上跑 DaemonSet,单实例 CPU 稳定在 0.2~0.35 核,内存 40MB 上下。但这不代表 Fluent Bit 全面碾压,它的 filter 能力是真的简陋,复杂解析还是得靠 Lua 或者正则硬怼。我现在的分工是:Fluent Bit 只做重命名、加元数据、简单正则,真正的结构化交给后端。Vector 我也认真用过,VRL 写转换逻辑很舒服,vector top 看管道吞吐也直观,但早期版本长时间运行后有内存缓慢上涨的情况,2023 年初我遇到过一次连续跑 40 天 RSS 从 60MB 涨到 400MB,升级之后没再复现。

有个坑几乎每个自建 k8s 的人都会踩一次:Fluent Bit 的 tail 插件用 SQLite 记录文件读取位置,默认在 /var/lib/fluent-bit/tail.db。容器日志轮转后 inode 变了,如果这个 db 文件没挂持久化卷,Pod 每次重启都会从头再读一遍,后端就冒出一大堆重复日志,你去重都来不及。我当时是把 db 放到 hostPath,同时给状态目录单独做了个 1Gi 的 PVC,问题才彻底消掉。

图片

二、存储成本的真相:贵的不是磁盘,是索引

很多人算日志成本只算磁盘,一块 4TB NVMe 多少钱,这其实算错了方向。真正吃钱的是索引结构和副本数,磁盘反而是最便宜的那一环。

我做过一次完整的对比:1TB/天的原始日志,保留 30 天。ES 方案是 3 节点、每节点 16C64G + 4TB NVMe,7 天热数据留存、23 天冷数据,1 副本。实际占用的索引空间大概是 9~13TB,取决于字段数量和是否开了 _source 压缩。Loki 走 S3 后端,压缩后大概 3~5TB,因为 Loki 只索引标签,正文按 chunk 压缩,gzip 之后常见 8~12 倍压缩比,日志重复度越高压得越狠。ClickHouse 用 ZSTD(3) 存 Nginx access log,我实测 1TB 原始数据落到大概 65~80GB,压缩比 12~18 倍。

但 ES 贵的原因不只是空间。默认的 index.refresh_interval 是 1 秒,意味着每秒都可能产生一个新 segment,段合并的 IO 压力很大。我把它从 1s 改到 30s,写入吞吐大概涨了 40%,代价是数据可见性延迟 30 秒 —— 对于排查问题来说完全可以接受,反正你也不会盯着秒级实时日志看。分片大小我也从默认的 5 分片改成了 rollover 到 50GB 一个分片,避免小分片满天飞导致 cluster state 膨胀。

另一个隐形成本是 mapping explosion。ES 默认 index.mapping.total_fields.limit 是 1000,我们有段时间业务方往里塞 JSON 日志,字段带随机 key,直接把 mapping 打爆,索引变成 read-only。解决办法要么在采集端做字段白名单,要么把动态结构塞进一个 flattened 或 keyword 类型的字段里,别让它自动展开。

三、Loki 的标签基数,是个能让你集群崩掉的坑

Loki 最吸引人的地方就是便宜和简单:只索引标签,正文不索引。但这也意味着它对标签基数极度敏感,而这一点几乎所有人第一次用都会踩。

图片

我见过最典型的错误,是把 request_id、trace_id、user_id 这类高基数字段做成标签。我们有个团队为了“方便按 trace 查”,在 Promtail 的 pipeline 里把 trace_id 提出来当作 label。结果两周后 Loki 的 index 直接起飞,查询从 2 秒退化到 30 秒以上,ingester 内存一路飙到 12GB 被 OOMKill。原因很简单:每个唯一标签值都会产生一条独立的索引流(stream),几十万个 trace_id 就是几十万条流。

修的方式不复杂,分三步走。第一步,把所有高基数字段从 label 里挪出去,改成结构化元数据(structured metadata),或者干脆不做处理,让它在正文里待着,查询时用 |= 过滤。第二步,用 pattern 解析器替代 regex 解析器,正则解析在 Loki 里非常吃 CPU,pattern 快得多。第三步,标签最多保留 5~8 个,比如 cluster、namespace、app、level、env,就这些。

改完之后,同样 300GB/天的日志,Loki 的 ingester 内存从 12GB 降到 3.5GB,查询一天范围的关键词从 30 秒降到 4~8 秒。这个坑我建议你在上线前就用 loki_ingester_streams_created_total 这个指标盯着,一天新增的 stream 超过几万就该警觉了。

四、ClickHouse 不是银弹,它换了一种方式让你难受

ClickHouse 存日志是真的爽。一个 MergeTree 表,ORDER BY (service, level, timestamp),ZSTD(3) 压缩,查询一天范围内的聚合基本都在 1 秒以内,甚至比 ES 快一个数量级。我拿它跑“某接口 5xx 按分钟分布”这类查询,扫描 8 亿行只要 0.6 秒。

代价在别的地方。第一,没有全文索引,你要做 LIKE '%error%' 这种查询就是全表扫,虽然列存加持下也能扛,但跟 ES 的倒排索引完全不是一个量级。第二,写入要有本地表 + 分布式表的双层结构,加上 Buffer 表或者 Kafka 引擎,维护复杂度比 Loki 高不少。第三,MergeTree 的合并是后台异步的,如果你写入速率超过合并速率,parts 数量会暴涨,Too many parts 的报错就来了。我的经验是单机每秒写入控制在 20~40 万行以内比较稳,超过就得分片。

还有个细节容易被忽略:ClickHouse 的 ORDER BY 决定了一切。我有次把 timestamp 放第一列,结果按 service 过滤的查询慢得要死,因为时间戳基数太高,跳数索引根本用不上。改成 (service, level, toStartOfHour(timestamp)) 之后,同样的查询快了 60 倍。这件事你没法在文档里直接读到,得自己踩一次才记得住。

图片

五、我最后的架构:三层分流,而不是一套通吃

折腾了三年,我的结论是别指望一套系统解决所有问题。我现在的做法是把日志按用途分三层。

第一层是实时告警层。走 Prometheus + Loki,只收集 error 级别和关键业务指标日志,保留 7 天,成本极低。告警规则用 LogQL 写,比如 sum(rate({app="order"} |= "timeout" [5m])) > 10。这一层不追求查得全,只追求查得快、报警准。

第二层是全量检索层,也就是 ES。保留 30 天,热数据 7 天在 NVMe,冷数据转 S3 快照。这一层是给人用的,产品、测试、开发都会来查,所以查询体验必须好。为了防止再出现那个凌晨 2 点的查询事故,我在 Kibana 前面加了个网关,强制所有查询必须带时间范围,最大跨度 7 天,单次返回上限 10000 条,另外给每个用户设了并发查询数上限。

第三层是分析层,用 ClickHouse。所有日志全量落到这里,保留 180 天,专门用来做长周期的聚合分析、看趋势、算 SLA。这一层不给人随便查,只通过固定的报表和看板输出结果。

三层加起来,月成本比我原来纯 ES 方案大概低了 55%,而且查询体验反而变好了,因为每类需求都在它最擅长的系统上跑。

图片

六、几个高频问题的排查步骤

问题一:日志采集不上来。先确认 /proc/sys/fs/inotify/max_user_watches,默认值经常不够,容器节点上几十万个文件很常见,改成 524288 再 sysctl -p。然后看采集进程有没有权限读 /var/log/containers 的软链接,很多安全加固过的镜像里这个路径权限是 700。

问题二:ES 写入被拒。看 thread_pool.write.queue 是不是满了,默认 10000。先别急着加节点,先看是不是有大批量单文档写入,把 bulk 大小调到 5~15MB 之间,flush_interval 别低于 1s,然后检查是不是有节点磁盘到了 watermark 阈值导致分片无法分配,默认低水位 85%、高水位 90%。

问题三:时间戳全错。九成是时区问题。采集端统一用 ISO8601 带时区,比如 2024-03-15T08:23:11.482+08:00,解析器里显式指定 %Y-%m-%dT%H:%M:%S.%L%z,别用 localtime。ES 里存 UTC,展示层做转换,这条规矩定死了能省掉无数扯皮。

问题四:日志重复。除了前面说的 Fluent Bit db 文件问题,还要检查 k8s 里是不是同时跑了两个 DaemonSet,或者容器日志软链接指向了被轮转掉的旧文件。用 Path_Key 加上 inode 做去重键比较保险。

七、算一笔账

以 1TB/天原始日志、保留 30 天为例,这是我实测出来的对比,硬件是三台 16C64G + 4TB NVMe 的物理机,S3 按标准存储算:

图片

方案 存储占用 查询 1 天范围关键词 维护复杂度
ES 7.17(1 副本) 9~13TB 1.2~4s 中
Loki + S3 3~5TB 3~15s 低
ClickHouse ZSTD(3) 1.8~2.5TB 0.3~1.5s 高

如果是买商业方案,Splunk 是按索引量 GB/天 计价的,公开报价单上前几年大概是每 GB/天 一百多美元一年的量级,具体折扣看你怎么谈。按这个价格算,1TB/天 就是每年千万级别的投入,这也是为什么国内大部分团队最后还是自建。

八、几句可能不太中听的结论

第一,在你搞清楚要查什么之前,不要上全量索引。 大部分团队的日志里,真正被查过的比例不到 5%。先把采集端的 debug 日志丢掉,能省掉 30% 以上的量,这一步的性价比比换任何后端都高。

第二,日志不是数据资产,是负债。 它不会给你带来收入,只会持续消耗存储、CPU 和人力。每加一路日志之前问一句:出问题的时候我会看它吗?如果答案是“应该不会”,那就别加。

第三,不要为了技术选型而选型。 我见过太多团队上 ClickHouse 是因为“快”,结果发现团队没人会写 SQL 调优,最后查询比 ES 还慢。选型的标准应该是你团队的运维能力和查询习惯,不是 benchmark 榜上的排名。

最后回到那个凌晨 2 点 47 分。那次事故之后我做的第一件事不是扩容,是给查询加了强制时间范围。系统没变,成本没变,但报警再也没在半夜响过。很多时候,日志分析的问题不在技术栈,在于有没有人认真想过“谁会来查、怎么查、查多久”。

🏷️ 标签: