Skip to content

3.5 数据设施

学习目标

完成本节后,你将能够:

  1. 区分 Postgres、ClickHouse、Redis/BullMQ、S3/blob 的系统职责。
  2. 从写入路径和查询路径解释为什么 Langfuse 需要多种存储。
  3. 判断新增字段、事件或任务应该落到哪种状态设施。

3.5.1 先给结论:存储不是按“方便”拆的

Langfuse 是高吞吐 LLM observability 平台。它既有组织、项目、API key、prompt、dataset 这类事务型状态,也有 trace、observation、score、events 这类高吞吐分析数据,还要处理异步 job 和 raw payload。

因此它使用多种状态设施:

状态设施主要职责不适合做什么
PostgreSQL + Prisma事务型元数据、关系模型、配置、权限、prompt/dataset 等产品状态高吞吐事件扫描和宽表分析。
ClickHouseobservation、trace、score、events 的列式分析和过滤强事务主状态、复杂关系约束。
Redis/BullMQjob 调度、队列、短期缓存、限流、seen cache、锁保存完整业务事实或大 payload。
S3/blobraw 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 tracesobservationsscores
  • v4 events_full
  • v4 events_core
  • batch/export/retention 相关的分析表或日志表。

源码入口:

  • packages/shared/clickhouse/migrations/**
  • packages/shared/src/server/repositories/definitions.ts
  • packages/shared/src/server/repositories/events.ts
  • packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts
  • worker/src/services/ClickhouseWriter/index.ts

events_fullevents_core

角色适合场景
events_fullfull-fidelity 宽事件,保留完整 input/output/metadata详情页、batchIO、需要完整 payload 的查询。
events_corequery-optimized 投影,默认轻量读取列表、筛选、常见聚合、filter option。

这体现了仓库架构原则:列表页不要默认读取完整大字段。eventsRouter.all 通过 eventsService.getEventList 设置 selectIOAndMetadata: false,列表先读轻字段;需要 input/output 时再通过 batchIO 读取。

3.5.5 Redis/BullMQ:调度和短期状态

Redis/BullMQ 在 Langfuse 里承担四类职责:

职责例子
job queueingestion、eval、batch export、delete、webhook、monitor。
shardingingestion/eval/OTel queue 根据 shard name 分摊负载。
short-term cacheingestion recently processed cache、rate limit、S3 slowdown flag。
coordinationlocks、stalled job recovery、queue metrics。

源码入口:

  • packages/shared/src/server/queues.ts
  • packages/shared/src/server/redis/**
  • worker/src/queues/workerManager.ts
  • worker/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、bucketPrefixfileKeyskipS3List 变成跨进程契约,必须在 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/limiterqueue payload 必须兼容 rolling deploy。
多租户隔离query builder 和 procedure 自动携带 project scope手写 SQL 和绕过 wrapper 风险更高。

读数据设施时,要同时看“为什么这样放”和“这带来了什么维护成本”。

下一节

draw.io 架构图