【技术硬核 | 存储】ClickHouse 原理与 Langfuse 存储实践:当 LLM Trace 爆炸时,PG 还扛得住吗?

周一早上,你打开云账单,发现 Token 费用比上周涨了 200%。你试图在日志里找出是哪个用户的哪个请求烧掉了预算,结果 PostgreSQL 的查询转了五分钟还没出结果。

这时候你才意识到,LLM 应用的可观测性,如果存储架构扛不住,就是一句空话。

这一段,懂的都懂

这不是个例。

当你的应用从 Demo 走向生产,每一次 LLM 调用都会产生一条复杂的 Trace:Prompt、Completion、Token 统计、延迟、向量检索过程......这些数据量比传统应用日志高出几个数量级。

如果还在用传统关系型数据库硬扛,查询慢只是表象,存储成本和写入瓶颈迟早会教做人。

LLM 可观测性的"不可能三角"

LLM 应用的可观测性,本质上是在解决三个维度的矛盾:数据粒度查询速度存储成本

你想把每次调用的 Prompt 和 Completion 都存下来做复盘,这是粒度;你想在账单异常时秒级定位到那条请求,这是速度;你想把几百 TB 的历史数据保留一年,这是成本。

在传统架构里,这三者几乎无法兼得。

PostgreSQL 这种行存数据库,写入事务强一致,适合存用户信息和账单,但让它去扫亿级 Trace 数据做聚合分析,就像让一个会计去搬砖------能搬,但效率极低。

Elasticsearch 倒是能搜,但资源消耗惊人,存一份 Trace 往往要付出 3 到 5 倍的存储冗余。

Loki 够便宜,但查询能力又太弱,稍微复杂一点的聚合就得写一堆正则。

这就是 Langfuse 在 2023 年到 2024 年间面临的现实困境。作为一个开源的 LLM Observability 平台,它最初完全基于 PostgreSQL 构建1

随着用户数据量的暴涨,单表查询超时成了家常便饭,不得不限制用户保留数据的时长。这不是产品功能问题,是底层存储的天花板到了。

为什么 Langfuse 最终选择了 ClickHouse

PostgreSQL 的瓶颈:当 Trace 不再是"日志"

在 Langfuse 的早期架构里,所有的 Observations(Span、Generation、Event)都存在 PostgreSQL 的一张表里。

对于初创项目这没问题,但当单表行数突破 1 亿,索引膨胀、写入抖动、查询超时这些问题就像定时炸弹一样排队引爆。

Trace 数据和业务数据最大的不同在于:它写多读少,但读的时候往往是大范围扫描。

PostgreSQL 的 B+ 树索引是为了点查设计的,面对 `SELECT count() FROM observations WHERE project_id = ?

AND timestamp > now() - interval '7 day'` 这类查询,优化器往往选择全表扫描,直接把 IO 打满。

PG 慢查询告警又响了,心累

更致命的是存储成本。PostgreSQL 的行存模式对字符串压缩效率不高,而 LLM 的 Prompt 和 Completion 往往是大段文本。

同样的数据,在 PG 里可能要占 100GB,在列存数据库里可能只要 10GB。

ES vs Loki vs ClickHouse:一场不对等的较量

在选型时,Langfuse 团队对比了三种主流方案:

  1. Elasticsearch (ES) :查询能力强,但资源消耗巨大。为了维持高写入吞吐,需要复杂的分片策略。而且 ES 的存储成本极高,对于初创团队来说,维护一套高可用的 ES 集群是一笔不小的开销。ClickHouse 官方的对比测试显示,在日志分析场景下,ClickHouse 的查询速度比 ES 快 2 到 3 倍,存储成本只有 ES 的 1/102

  2. Loki:成本最低,因为它是基于对象存储的。但 Loki 的查询语言 LogQL 在处理复杂聚合时非常吃力,而且它不建立索引,查询时需要暴力扫描。对于 Langfuse 这种需要频繁做 Token 统计、评分分析的场景,Loki 的查询性能无法满足需求。

  3. ClickHouse:在查询速度和存储成本之间找到了最佳平衡点。它的列式存储和向量化执行引擎,让它在聚合查询上具有压倒性优势。同时,它对数据压缩的支持极好,ZSTD 压缩算法能将文本列压缩到原大小的 10% 以下。

Langfuse 的架构演进:PG + ClickHouse 混合模式

最终,Langfuse 选择了混合架构:PostgreSQL 作为"元数据主库",ClickHouse 作为"分析型副库"3

在这个架构里,PostgreSQL 只存"冷"数据:用户账号、项目配置、Prompt 模板版本。这些数据量小,但要求强一致性。

而所有的 Trace 数据、评分数据、Session 数据,全部写入 ClickHouse。

这种设计完美规避了各自的短板:PG 不再承担海量分析查询的压力,ClickHouse 也不需要处理复杂的事务逻辑。

对于开发者来说,这套架构既保留了 PG 的事务特性,又获得了 ClickHouse 的极致分析性能。

ClickHouse 核心原理:快在哪儿?

ClickHouse 的快,不是单点优化,而是从存储结构到计算模式的全面重构。

列式存储:I/O 与压缩的双重胜利

传统数据库按行存储,一条记录的所有字段在物理上连续存放。这对于 SELECT * FROM user WHERE id = 1 这种点查非常友好,一次 IO 就能读出整行。

但在分析场景下,我们往往只需要少数几列。

比如统计"过去 7 天每个模型的平均 Token 消耗",你只需要 modelinput_tokensoutput_tokenstimestamp 这四列。

在行存数据库里,你必须把整行读出来再扔掉无用字段,这不仅是 IO 浪费,还会挤占内存带宽。

ClickHouse 按列存储,同一列的数据在物理上连续存放。查询时,只读取需要的列文件。

更关键的是,同一列的数据类型相同,内容往往高度相似(比如 model 列可能 90% 都是 gpt-4o),这使得压缩算法能发挥极致效果。

实测中,ClickHouse 的存储空间通常只有 PostgreSQL 的 1/5 到 1/10。

向量化执行:榨干 CPU 的每一滴性能

光减少 IO 还不够,CPU 的计算效率也是瓶颈。传统数据库是"火山模型",逐行处理数据,每一行都要经过复杂的函数调用栈。这导致 CPU 大量时间浪费在函数跳转和分支预测上。

ClickHouse 采用了向量化执行引擎。它不是一行一行处理,而是一批一批处理(默认 8192 行)。

比如做加法运算,它利用 CPU 的 SIMD(Single Instruction, Multiple Data)指令,一条指令同时完成 8192 个数值的加法。

这就像把"搬砖"变成了"开卡车",效率提升不是一点半点。

MergeTree 引擎:追加写的艺术

ClickHouse 最核心的表引擎是 MergeTree 家族。它的设计哲学是:数据一旦写入,就不再修改

所有写入操作都是追加写。新数据先写入内存 Buffer,定期刷盘形成一个个 Part 文件。后台线程会异步将这些小 Part 合并成大 Part。

这种 LSM-Tree 变体结构,让写入性能极高,因为每次写入都是顺序写,没有随机 IO。

对于 Langfuse 这种写入量极大的场景,MergeTree 简直是绝配。而且,MergeTree 支持稀疏索引,每隔 8192 行才建一个索引标记。

这意味着索引体积极小,完全可以常驻内存,查询时能极快地定位到数据块。

Langfuse 的 ClickHouse 实战落地

表结构设计:排序键是性能的灵魂

在 ClickHouse 里,ORDER BY 不仅仅是排序,它决定了数据在磁盘上的物理布局。Langfuse 的 observations 表设计非常讲究:

sql 复制代码
CREATE TABLE observations (
    id String,
    project_id String,
    trace_id String,
    type String,
    name String,
    start_time DateTime64(9),
    end_time Nullable(DateTime64(9)),
    -- ... 其他字段省略
    input_tokens Nullable(UInt64),
    output_tokens Nullable(UInt64),
    -- 省略 Prompt/Completion 等大文本字段
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(start_time)
ORDER BY (project_id, toDateTime(start_time), trace_id);

注意这个 ORDER BY (project_id, start_time, trace_id)。这是经过深思熟虑的:

  1. project_id 在前 :因为绝大多数查询都带有 WHERE project_id = ?。这样同一个项目的数据在物理上连续存放,查询时只需要读取该项目对应的数据块,极大地减少了 IO 范围。

  2. start_time 在中:时间范围查询是最高频的场景,比如"过去 7 天"。时间字段在排序键中,可以利用稀疏索引快速裁剪数据块。

  3. trace_id 在后:用于保证同一 Trace 的数据局部性,方便关联查询。

如果排序键设计反了,比如把 trace_id 放在最前面,数据就会按 Trace ID 随机分布。查询某个项目的数据时,就需要扫描几乎所有数据块,性能会下降几个数量级。

写入优化:应对高并发 Trace 写入

LLM 应用的 Trace 写入往往是突发性的。比如一个 RAG 应用,一次请求可能产生几十条 Span。

如果每条 Span 都单独写入,ClickHouse 会迅速生成大量小 Part,触发 Too many parts 错误。

Langfuse 采用了两层优化策略:

  1. 应用层 Buffer:在服务端内存中维护一个 Buffer,积累到一定数量或时间后再批量写入。这能显著减少写入频率。

  2. 异步插入:ClickHouse 支持异步插入模式。客户端发送数据后立即返回,服务端在后台攒批写入。这虽然会带来秒级的数据可见延迟,但对于可观测性场景,这点延迟完全可以接受。

查询加速:从秒级到毫秒级

有了 ClickHouse,Langfuse 的 Dashboard 查询速度有了质的飞跃。以前在 PG 里需要跑 30 秒的 Token 消耗统计,现在可以在 200ms 内返回1

这得益于 ClickHouse 的预聚合能力。Langfuse 利用 Materialized View,将明细数据实时聚合成小时级、天级的统计表。

Dashboard 查询时直接读聚合表,扫描的数据量减少了几个数量级。

sql 复制代码
-- 示例:按天聚合 Token 消耗的物化视图
CREATE MATERIALIZED VIEW daily_token_stats
ENGINE = SummingMergeTree()
ORDER BY (project_id, date)
AS SELECT
    project_id,
    toDate(start_time) as date,
    sum(input_tokens) as total_input,
    sum(output_tokens) as total_output,
    count() as request_count
FROM observations
WHERE type = 'generation'
GROUP BY project_id, date;

这种"空间换时间"的策略,在分析型数据库里是标准操作。但在 PostgreSQL 里,物化视图的刷新成本极高,很难做到准实时。

而 ClickHouse 的增量物化视图,写入时自动维护,几乎零维护成本。

生产环境避坑指南

写入过快:Too many parts 的应对之道

这是 ClickHouse 最经典的报错。当你看到 DB::Exception: Too many parts,说明写入频率太高,后台合并线程跟不上了。

应对方案

  1. 调大写入批次:不要一条一条插,攒够 1000 条或者 1MB 再写。

  2. 开启异步插入SET async_insert = 1, wait_for_async_insert = 0。让服务端帮你攒批。

  3. 调整参数parts_to_throw_insert 默认值是 300,可以适当调大,但这只是治标。根本还是要控制写入频率。

又是 Too many parts,我就知道攒批没攒够

查询 OOM:内存溢出的排查与优化

ClickHouse 是内存大户,复杂的聚合查询容易撑爆内存。

排查思路

  1. 查看 system.query_log,找到 memory_usage 异常的查询。

  2. 检查 GROUP BY 的基数。如果 Group By 的字段唯一值太多(比如按 User ID Group By),中间结果会极大。

优化方案

  1. 外部聚合 :设置 max_bytes_before_external_group_by。当内存使用超过阈值,ClickHouse 会把中间结果溢写到磁盘。虽然慢一点,但不会 OOM。

  2. 使用近似函数 :比如 uniq() 替代 count(distinct)uniqCombined() 误差率不到 1%,但内存占用只有原来的 1/10。

副本延迟:数据一致性的权衡

ClickHouse 的副本同步是异步的。刚写入的数据,可能要过几秒才能在另一个副本查到。对于 LLM Trace 这种场景,秒级延迟完全可以接受。但如果你的业务要求"写完立刻读",就要注意了。

应对策略

  1. 写入 Quorum :设置 insert_quorum = 2,确保至少两个副本写入成功才返回。这会牺牲写入性能,但能保证强一致性读。

  2. 读自己的写 :在 Session 级别设置 select_sequential_consistency = 1。但这会极大降低查询性能,慎用。

对于 Langfuse 这种监控平台,默认的最终一致性已经足够。毕竟,没人会在意监控数据有 5 秒钟的延迟。

写在最后

ClickHouse 不是银弹,它有自己的适用边界。如果你需要频繁的单行更新、删除,或者需要复杂的事务支持,PostgreSQL 依然是首选。

但在 LLM 可观测性这个赛道,Trace 数据的爆炸式增长,让 ClickHouse 几乎成了唯一的选择。

Langfuse 的架构演进,其实代表了 AI 时代数据栈的一个趋势:混合架构正在成为常态。用 PG 处理事务,用 ClickHouse 处理分析,用对象存储存冷数据。

每一层各司其职,而不是试图用一个数据库解决所有问题。

当你的 LLM 应用还在 Demo 阶段,PG 够用。但当你准备上线,面对真实的用户和流量,是时候考虑一下:你的存储架构,准备好迎接 Trace 数据的洪峰了吗?

参考文献

  1. Langfuse 架构演进博客

  2. ClickHouse vs Elasticsearch 性能对比

  3. Langfuse 自托管架构文档


延伸入口


相关推荐
workflower22 分钟前
装备企业的 AI 路线图
人工智能·深度学习·机器学习·设计模式·机器人
AKAMAI31 分钟前
Linode接口及默认防火墙现已全面开放使用
人工智能·云计算
前端开发江鸟1 小时前
RAG 回答错了,问题到底出在召回、重排,还是生成?
人工智能
美狐美颜SDK开放平台1 小时前
直播app开发如何实现主播级美颜效果?美颜sdk开发方案详解
人工智能·音视频·美颜sdk·视频美颜sdk·美颜api
东方小月2 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
大龄码农有梦想2 小时前
使用大模型服务如何保证数据安全?企业 AI 安全、私有化部署与治理实践
人工智能·私有化部署·数据安全·信创·ai agent·智能体·智能体开发平台
冬奇Lab2 小时前
每日一个开源项目(第170篇):CodeWiki - ACL 2026 论文级代码库自动文档生成,递归多 Agent 架构
人工智能·开源·资讯
lxw18449125142 小时前
Claude-Code企业级培训教程
人工智能
冬奇Lab2 小时前
代码库知识库系列(01):技术全景——为什么代码理解比文档检索难十倍
人工智能
MartinYeung52 小时前
[论文学习]迈向自主医疗人工智能智能体:MIRA系统深度分析
人工智能·学习