Skip to content

第 3 篇 · 3.1 系统架构总览

学习目标

完成本节后,你将能够:

  1. 用“控制面、数据面、契约面、执行面、状态面”解释 Langfuse 当前 repo。
  2. 说清 webpackages/sharedworker 如何组合,而不是只记目录名。
  3. 从架构图追到真实源码入口,并知道每个入口承担什么责任。

3.1.1 先给结论:Langfuse 是事件数据平台,不是普通 CRUD

Langfuse 的核心对象是 trace、observation、score、dataset、prompt、eval。这些对象既要被 UI 管理,也要被 SDK 高频写入,还要被 ClickHouse 高速查询。

所以这个 repo 同时做三件事:

解决的问题典型源码
控制面用户、组织、项目、API key、RBAC、rate limit、feature flagweb/src/server/api/trpc.tscreateAuthedProjectAPIRoute.ts、Prisma schema
数据面SDK/OTel 事件接收、raw payload 保存、异步合并、ClickHouse 写入processEventBatch.tsworker/src/queues/ingestionQueue.ts
产品面把 trace、observation、score、eval、dataset 做成 UI/API 工作流web/src/features/**web/src/pages/**

如果只按 webworkerpackages/shared 三个目录读,会漏掉真正的边界:同步请求和异步执行的边界、外部契约和内部契约的边界、事务状态和分析状态的边界。

3.1.2 容器图:运行时如何组合

读图时注意三条边:

  1. webworker 不互相 import,它们通过 packages/shared 的类型/schema 和 Redis/BullMQ job 连接。
  2. ingestion 请求侧不直接写 ClickHouse,它先写 S3 raw JSON,再投递轻量 job。
  3. 查询默认读 events_core,只有需要完整 input/output 或 metadata 时才读 events_full

3.1.3 每个组件的输入、输出和不该做的事

组件输入输出不该做的事第一批源码
web UI浏览器事件、URL filter、sessiontRPC 调用、页面状态、table query params不处理长耗时后台 jobweb/src/pages/**web/src/features/**
tRPC router已登录 session、projectId、Zod inputproject-scoped procedure result不接受外部 SDK 的公共契约web/src/server/api/root.tstrpc.ts
Public API wrapperHTTP method、headers、query/body、API keyJSON response、统一错误不绕过 Zod response schemawithMiddlewares.tscreateAuthedProjectAPIRoute.ts
processEventBatchSDK/OTel event array、auth scopeS3 raw event、BullMQ ingestion job、batch result不做业务 merge,不直接 insert ClickHousepackages/shared/src/server/ingestion/processEventBatch.ts
packages/shared跨运行时共享规则schema、types、query builders、clients不放只属于一个页面的临时 helperqueues.tsfilters.tsdomain/**
workerBullMQ jobClickHouse records、webhook、eval result、export file不定义新的 queue payload 源头worker/src/app.tsworker/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/BullMQjob、短期 key、rate/caches/locks调度、重试、去重不存完整大 payloadpackages/shared/src/server/redis/**
S3/blobraw event、大字段、导出文件可重放/可下载对象不做索引查询StorageServiceeventBucketPath

3.1.4 三条主路径如何组合

UI 查询路径

Browser 进入 Next.js 页面,页面通过 tRPC 调用 appRouterprotectedProjectProcedure 做 session 和 project membership 校验,并把 orgIdprojectId、project role 注入 ctx。router 再调用 feature service 或 shared repository,最后读 Postgres 或 ClickHouse。

典型例子是 events table:

  1. UI 维护 FilterState、排序、分页。
  2. eventsRouter 把参数交给 eventsService.getEventList
  3. service 调用 shared 里的 ClickHouse query helper。
  4. list 查询默认 selectIOAndMetadata: false,避免列表页扫完整大字段。
  5. 需要 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 请求侧只做:

  1. 校验 event schema 和 access scope。
  2. 按 timestamp 排序,create 早于 update。
  3. entityType + body.id 分组。
  4. 写 raw JSON 到 S3/blob。
  5. 把轻量 job 放进 Redis/BullMQ。

worker 消费 job 后才做 S3 读取、Redis seen cache、secondary queue 分流、IngestionService.mergeAndWriteClickhouseWriter.addToQueue。这就是同步控制面和异步数据面的分离。

3.1.5 为什么 packages/shared 在中间

packages/shared 不是“工具包”,而是跨运行时契约中心。判断一个东西是否应该放进 shared,可以问:这个规则是否必须被至少两个运行时共同理解?

契约为什么必须共享例子
Domain schemaAPI 校验、worker 转换、测试数据都要理解同一个 trace/observation/score 形状packages/shared/src/domain/**
Queue payloadproducer 在 web/shared,consumer 在 worker,中间还可能有旧 jobIngestionEventTQueueJobTypes
FilterStateUI 编辑 filter,后端把它 lowering 成 ClickHouse SQLpackages/shared/src/interfaces/filters.ts
QueryBuilderweb/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 只放指针和 scopeproducer/consumer 要维护路径兼容避免大 payload 压垮队列
批量写 ClickHouseClickhouseWriter 按表缓冲并批量 insert有短暂写入延迟换取吞吐、retry 和集中错误处理
轻重表分离events_core 做列表/筛选,events_full 做完整 I/Oquery builder 更复杂列表页不默认扫大字段
shared 契约集中web 和 worker 复用 schema/query/queue typesshared 变更成本高减少跨运行时规则漂移

这也是你做类似 infra 时最应该学的部分:不是“用了某个技术栈”,而是把同步入口、异步执行、状态存储、契约兼容和查询成本拆开。

3.1.7 配套 draw.io 架构图

中文 draw.io XML 文件在:

这张图适合作为容器图和契约图入口。第 4 篇会继续把它放大成 tRPC、Public API、ingestion、worker、v4 events query 五条运行链路。

本篇后续