第 3 篇 · 3.1 系统架构总览
学习目标
完成本节后,你将能够:
- 用“控制面、数据面、契约面、执行面、状态面”解释 Langfuse 当前 repo。
- 说清
web、packages/shared、worker如何组合,而不是只记目录名。 - 从架构图追到真实源码入口,并知道每个入口承担什么责任。
3.1.1 先给结论:Langfuse 是事件数据平台,不是普通 CRUD
Langfuse 的核心对象是 trace、observation、score、dataset、prompt、eval。这些对象既要被 UI 管理,也要被 SDK 高频写入,还要被 ClickHouse 高速查询。
所以这个 repo 同时做三件事:
| 面 | 解决的问题 | 典型源码 |
|---|---|---|
| 控制面 | 用户、组织、项目、API key、RBAC、rate limit、feature flag | web/src/server/api/trpc.ts、createAuthedProjectAPIRoute.ts、Prisma schema |
| 数据面 | SDK/OTel 事件接收、raw payload 保存、异步合并、ClickHouse 写入 | processEventBatch.ts、worker/src/queues/ingestionQueue.ts |
| 产品面 | 把 trace、observation、score、eval、dataset 做成 UI/API 工作流 | web/src/features/**、web/src/pages/** |
如果只按 web、worker、packages/shared 三个目录读,会漏掉真正的边界:同步请求和异步执行的边界、外部契约和内部契约的边界、事务状态和分析状态的边界。
3.1.2 容器图:运行时如何组合
读图时注意三条边:
web和worker不互相 import,它们通过packages/shared的类型/schema 和 Redis/BullMQ job 连接。- ingestion 请求侧不直接写 ClickHouse,它先写 S3 raw JSON,再投递轻量 job。
- 查询默认读
events_core,只有需要完整 input/output 或 metadata 时才读events_full。
3.1.3 每个组件的输入、输出和不该做的事
| 组件 | 输入 | 输出 | 不该做的事 | 第一批源码 |
|---|---|---|---|---|
web UI | 浏览器事件、URL filter、session | tRPC 调用、页面状态、table query params | 不处理长耗时后台 job | web/src/pages/**、web/src/features/** |
| tRPC router | 已登录 session、projectId、Zod input | project-scoped procedure result | 不接受外部 SDK 的公共契约 | web/src/server/api/root.ts、trpc.ts |
| Public API wrapper | HTTP method、headers、query/body、API key | JSON response、统一错误 | 不绕过 Zod response schema | withMiddlewares.ts、createAuthedProjectAPIRoute.ts |
processEventBatch | SDK/OTel event array、auth scope | S3 raw event、BullMQ ingestion job、batch result | 不做业务 merge,不直接 insert ClickHouse | packages/shared/src/server/ingestion/processEventBatch.ts |
packages/shared | 跨运行时共享规则 | schema、types、query builders、clients | 不放只属于一个页面的临时 helper | queues.ts、filters.ts、domain/** |
worker | BullMQ job | ClickHouse records、webhook、eval result、export file | 不定义新的 queue payload 源头 | worker/src/app.ts、worker/src/queues/** |
ClickhouseWriter | 表名 + insert record | 批量 JSONEachRow insert、metrics | 不做业务语义合并 | worker/src/services/ClickhouseWriter/index.ts |
| Postgres | 事务型 CRUD | 组织、项目、API key、配置、关系状态 | 不承担高吞吐事件扫描 | packages/shared/prisma/schema.prisma |
| ClickHouse | 事件宽表写入和查询 | 列式分析结果 | 不承担强事务主状态 | packages/shared/clickhouse/migrations/** |
| Redis/BullMQ | job、短期 key、rate/caches/locks | 调度、重试、去重 | 不存完整大 payload | packages/shared/src/server/redis/** |
| S3/blob | raw event、大字段、导出文件 | 可重放/可下载对象 | 不做索引查询 | StorageService、eventBucketPath |
3.1.4 三条主路径如何组合
UI 查询路径
Browser 进入 Next.js 页面,页面通过 tRPC 调用 appRouter。protectedProjectProcedure 做 session 和 project membership 校验,并把 orgId、projectId、project role 注入 ctx。router 再调用 feature service 或 shared repository,最后读 Postgres 或 ClickHouse。
典型例子是 events table:
- UI 维护
FilterState、排序、分页。 eventsRouter把参数交给eventsService.getEventList。- service 调用 shared 里的 ClickHouse query helper。
- list 查询默认
selectIOAndMetadata: false,避免列表页扫完整大字段。 - 需要 input/output 时再走 batch I/O 查询。
Public API 路径
外部系统进入 web/src/pages/api/public/**。路由外层先经过 withMiddlewares,负责 CORS、method dispatch、OpenTelemetry context、统一错误处理。具体 handler 再经过 createAuthedProjectAPIRoute,负责 API key、access level、rate limit、query/body/response Zod schema。
这条路径的关键是“外部契约稳定”:新增或修改 Public API 不只改 route,还要同步 web/src/features/public-api/types/** 和 fern/apis/**。
Ingestion 数据路径
SDK/OTel 上报事件后,web 请求侧只做:
- 校验 event schema 和 access scope。
- 按 timestamp 排序,create 早于 update。
- 按
entityType + body.id分组。 - 写 raw JSON 到 S3/blob。
- 把轻量 job 放进 Redis/BullMQ。
worker 消费 job 后才做 S3 读取、Redis seen cache、secondary queue 分流、IngestionService.mergeAndWrite、ClickhouseWriter.addToQueue。这就是同步控制面和异步数据面的分离。
3.1.5 为什么 packages/shared 在中间
packages/shared 不是“工具包”,而是跨运行时契约中心。判断一个东西是否应该放进 shared,可以问:这个规则是否必须被至少两个运行时共同理解?
| 契约 | 为什么必须共享 | 例子 |
|---|---|---|
| Domain schema | API 校验、worker 转换、测试数据都要理解同一个 trace/observation/score 形状 | packages/shared/src/domain/** |
| Queue payload | producer 在 web/shared,consumer 在 worker,中间还可能有旧 job | IngestionEvent、TQueueJobTypes |
| FilterState | UI 编辑 filter,后端把它 lowering 成 ClickHouse SQL | packages/shared/src/interfaces/filters.ts |
| QueryBuilder | web/API/worker 都可能读 events 表,tenant filter 和 field set 必须一致 | event-query-builder.ts |
| DB clients | 连接配置、log comment、telemetry context 要统一 | db.ts、ClickHouse client、Redis client |
反过来,如果一个 helper 只服务某个页面、某个 processor 或某个组件,就不要提升到 shared。
3.1.6 架构设计取舍
| 取舍 | 当前做法 | 代价 | 为什么接受 |
|---|---|---|---|
| ingestion 快速返回 | 请求侧只完成校验、raw upload、enqueue | 数据不是立即可查 | SDK 不被 ClickHouse 抖动拖住 |
| raw payload 可重放 | S3/blob 保存原始 events | 多一个路径和清理契约 | worker 可以重新读取、合并、审计 |
| job 轻量化 | Redis job 只放指针和 scope | producer/consumer 要维护路径兼容 | 避免大 payload 压垮队列 |
| 批量写 ClickHouse | ClickhouseWriter 按表缓冲并批量 insert | 有短暂写入延迟 | 换取吞吐、retry 和集中错误处理 |
| 轻重表分离 | events_core 做列表/筛选,events_full 做完整 I/O | query builder 更复杂 | 列表页不默认扫大字段 |
| shared 契约集中 | web 和 worker 复用 schema/query/queue types | shared 变更成本高 | 减少跨运行时规则漂移 |
这也是你做类似 infra 时最应该学的部分:不是“用了某个技术栈”,而是把同步入口、异步执行、状态存储、契约兼容和查询成本拆开。
3.1.7 配套 draw.io 架构图
中文 draw.io XML 文件在:
- 在线可访问版本:
/architecture/langfuse-current-architecture.drawio - 源文件:
architecture/langfuse-current-architecture.drawio
这张图适合作为容器图和契约图入口。第 4 篇会继续把它放大成 tRPC、Public API、ingestion、worker、v4 events query 五条运行链路。