3.5 数据设施
学习目标
完成本节后,你将能够:
- 区分 Postgres、ClickHouse、Redis/BullMQ、S3/blob 的系统职责。
- 从写入路径和查询路径解释为什么 Langfuse 需要多种存储。
- 判断新增字段、事件或任务应该落到哪种状态设施。
3.5.1 先给结论:存储不是按“方便”拆的
Langfuse 是高吞吐 LLM observability 平台。它既有组织、项目、API key、prompt、dataset 这类事务型状态,也有 trace、observation、score、events 这类高吞吐分析数据,还要处理异步 job 和 raw payload。
因此它使用多种状态设施:
| 状态设施 | 主要职责 | 不适合做什么 |
|---|---|---|
| PostgreSQL + Prisma | 事务型元数据、关系模型、配置、权限、prompt/dataset 等产品状态 | 高吞吐事件扫描和宽表分析。 |
| ClickHouse | observation、trace、score、events 的列式分析和过滤 | 强事务主状态、复杂关系约束。 |
| Redis/BullMQ | job 调度、队列、短期缓存、限流、seen cache、锁 | 保存完整业务事实或大 payload。 |
| S3/blob | raw ingestion event、大对象、媒体、导出文件 | 低延迟查询和结构化过滤。 |
| 外部服务 | LLM provider、webhook、Stripe、Slack、PostHog 等副作用 | 不能作为 Langfuse 内部一致性来源。 |
3.5.2 状态流向图
这张图的关键是:同一条用户行为可能同时触达多个状态设施。例如 ingestion 请求侧把 raw event 写 S3、把轻量 job 写 Redis;worker 再读 S3,合并后批量写 ClickHouse。
3.5.3 Postgres:事务型元数据
Postgres 适合保存需要关系一致性和事务语义的数据:
- organization、project、membership、role;
- API key、session、user account;
- prompt、dataset、annotation queue 等产品对象;
- Public API auth、feature 配置、部分调度状态;
- ClickHouse 查询前需要补充的元数据。
源码入口:
packages/shared/prisma/schema.prisma@langfuse/shared/src/db- feature service 中的
ctx.prisma或 shared repositories
判断一个数据是否应该放 Postgres,可以问:它是否需要外键关系、事务更新、强一致读取、权限模型或管理 UI CRUD?如果是,优先考虑 Postgres。
3.5.4 ClickHouse:高吞吐分析状态
ClickHouse 承载的不是普通 CRUD,而是高基数、宽事件、按时间和 project 分区过滤的分析数据。
典型数据:
- legacy
traces、observations、scores; - v4
events_full; - v4
events_core; - batch/export/retention 相关的分析表或日志表。
源码入口:
packages/shared/clickhouse/migrations/**packages/shared/src/server/repositories/definitions.tspackages/shared/src/server/repositories/events.tspackages/shared/src/server/queries/clickhouse-sql/event-query-builder.tsworker/src/services/ClickhouseWriter/index.ts
events_full 和 events_core
| 表 | 角色 | 适合场景 |
|---|---|---|
events_full | full-fidelity 宽事件,保留完整 input/output/metadata | 详情页、batchIO、需要完整 payload 的查询。 |
events_core | query-optimized 投影,默认轻量读取 | 列表、筛选、常见聚合、filter option。 |
这体现了仓库架构原则:列表页不要默认读取完整大字段。eventsRouter.all 通过 eventsService.getEventList 设置 selectIOAndMetadata: false,列表先读轻字段;需要 input/output 时再通过 batchIO 读取。
3.5.5 Redis/BullMQ:调度和短期状态
Redis/BullMQ 在 Langfuse 里承担四类职责:
| 职责 | 例子 |
|---|---|
| job queue | ingestion、eval、batch export、delete、webhook、monitor。 |
| sharding | ingestion/eval/OTel queue 根据 shard name 分摊负载。 |
| short-term cache | ingestion recently processed cache、rate limit、S3 slowdown flag。 |
| coordination | locks、stalled job recovery、queue metrics。 |
源码入口:
packages/shared/src/server/queues.tspackages/shared/src/server/redis/**worker/src/queues/workerManager.tsworker/src/app.ts
Redis 适合保存“可以重建、短期有效、用于调度”的状态。它不适合保存完整 raw event,也不适合作为最终业务事实。
3.5.6 S3/blob:raw payload 和大对象
S3/blob 用来保存不适合放进 Redis job 的大 payload:
- SDK ingestion raw event JSON;
- OTel raw payload;
- media files;
- export files;
- blob storage integration data。
在 ingestion 里,processEventBatch 把同一 entity 的 events 分组后写入 S3,再把 file pointer 放进 BullMQ job。worker 读取 S3 后再调用 IngestionService。
这样做的好处是:
- Redis job 保持轻量;
- raw event 可以重读和重放;
- worker 可以批量下载和合并;
- HTTP 请求不承担 ClickHouse 写入延迟;
- 大 payload 不挤占队列内存。
代价是:S3 key、bucketPrefix、fileKey、skipS3List 变成跨进程契约,必须在 producer 和 consumer 之间保持兼容。
3.5.7 写入路径和查询路径分开看
写入路径
写入路径强调吞吐、批量、可重试和可重放。
查询路径
查询路径强调 tenant filter、时间窗口、field set、轻重字段分离和分页。
同一个实体在写入和读取时经过不同抽象。写入侧关注 record 合并和 batch insert;读取侧关注 filter lowering 和 field selection。
3.5.8 数据落点判断
| 问题 | 倾向 |
|---|---|
| 是否需要事务一致性、关系约束、管理 UI CRUD? | Postgres。 |
| 是否是高吞吐 telemetry 或需要按时间/高基数字段分析? | ClickHouse。 |
| 是否是异步任务、重试、限流、短期去重? | Redis/BullMQ。 |
| 是否是大 payload、原始正文、导出文件、媒体? | S3/blob。 |
| 是否会被列表页频繁过滤/排序? | ClickHouse 轻量列或 events_core。 |
| 是否只在详情页读取,且可能很大? | events_full 或 S3/blob。 |
| 是否是外部 API 的稳定响应字段? | 还要同步 Public API schema 和 Fern。 |
3.5.9 设计取舍
| 取舍 | 当前做法 | 代价 |
|---|---|---|
| 高吞吐写入 | ingestion HTTP 只写 S3 和 queue,worker 批量写 ClickHouse | 数据有短暂异步延迟。 |
| 查询性能 | events_core 默认轻量读取,完整 I/O 延迟加载 | schema/query builder 更复杂。 |
| 可重放 | raw event 存 S3 | 多一套对象存储和 key contract。 |
| 可重试 | BullMQ 管理 job retry/stalled/limiter | queue payload 必须兼容 rolling deploy。 |
| 多租户隔离 | query builder 和 procedure 自动携带 project scope | 手写 SQL 和绕过 wrapper 风险更高。 |
读数据设施时,要同时看“为什么这样放”和“这带来了什么维护成本”。