Skip to content

1.3 源码拆解方法

学习目标

完成本节后,你将能够:

  1. 用 infra/平台型系统的视角读 Langfuse,而不是只按目录浏览。
  2. 区分控制面、数据面、执行面、契约面和状态面。
  3. 把外部方法论映射到本 repo 的具体文件。

1.3.1 为什么不能从目录树开始

大型 infra repo 的目录树通常不是系统架构本身。目录回答“代码放在哪里”,但不回答:

  • 请求从哪里进入;
  • 哪些组件只是入口,哪些组件真正执行业务;
  • 哪些类型是跨进程契约;
  • 哪些存储承载事务状态,哪些承载分析事件;
  • 哪些路径是同步返回,哪些路径是异步重试;
  • 系统在高吞吐、失败、重放、延迟写入时如何保持可理解。

Langfuse 就是这种系统。web 不是简单前端,worker 不是附属脚本,packages/shared 也不是工具箱。它们组合成一个“LLM observability 数据平台”:外部 SDK 和 UI 产生请求,web 做同步控制,shared 固化契约,worker 执行后台数据面,ClickHouse/Postgres/Redis/S3 分别承载不同状态。

1.3.2 先建立六个阅读镜头

这套镜头来自几类常见架构阅读方法:C4 的分层放大、Kubernetes 式 control plane / worker node 分工、SRE 对生产系统运行属性的关注、Well-Architected 对质量属性的追问、ADR 对设计取舍的记录方式,以及 Diátaxis 对教程内容组织的区分。

镜头阅读问题Langfuse 里对应什么
系统上下文谁在系统外部?谁调用它?Browser、SDK、OTel collector、API client、LLM provider、webhook target
容器/运行时哪些进程在跑?谁同步,谁异步?webworker、Postgres、ClickHouse、Redis、S3/blob
控制面谁做鉴权、路由、限流、调度、开关?tRPC procedure、Public API wrapper、WorkerManager.register、env flags
数据面高吞吐数据如何进入、转换、写入、查询?ingestion API、S3 raw events、BullMQ、IngestionServiceClickhouseWriter
契约面哪些结构必须被多方共同遵守?Zod domain、QueueNameQueueJobsFilterState、query builder fields
状态面数据放在哪里,为什么放那里?Postgres 元数据、ClickHouse 事件、Redis 队列/缓存、S3 原始事件

用这六个镜头读代码,比直接打开 web/src 更稳定。原因很简单:infra 系统的复杂度通常不在单个函数,而在“组件之间如何约定、失败时谁接手、状态如何跨进程流动”。

1.3.3 Langfuse 的阅读主线

这张图不是系统运行图,而是读代码顺序。先看上下文和运行时,知道有哪些外部入口和进程;再看控制面和数据面,知道每类请求如何被处理;随后看契约面,确认谁和谁共享规则;最后看状态面,理解为什么数据被拆到不同存储。

1.3.4 每个镜头对应的第一批文件

镜头先打开读到什么程度算懂
系统上下文part03-architecture/index.md、原 repo 的 web/src/pages/api能说出 UI、SDK、Public API、OTel 分别走哪条入口。
运行时容器pnpm-workspace.yamlpackage.jsonturbo.json能解释 webworkershared 的依赖方向和启动命令。
控制面web/src/server/api/trpc.tscreateAuthedProjectAPIRoute.tsworker/src/app.ts能指出鉴权、限流、项目权限、队列注册在哪里完成。
数据面processEventBatch.tsingestionQueue.tsIngestionServiceClickhouseWriter能从 SDK event 追到 ClickHouse insert。
契约面packages/shared/src/server/queues.tsinterfaces/filters.ts、domain schema能说出 producer 和 consumer 共享哪些结构。
状态面schema.prisma、ClickHouse migrations、Redis queue classes、S3 path helpers能解释每个存储承担的状态类型和失败语义。

不要一次读完所有文件。每轮只追一个链路,用这个链路反向验证架构边界。

1.3.5 场景驱动阅读

场景阅读路线关键判断
UI 查询列表页面/feature component -> tRPC router -> service/repository -> ClickHouse query builder是否把筛选语义保持在 FilterState 和 query builder 中。
Public APIpages/api/public -> withMiddlewares -> createAuthedProjectAPIRoute -> service -> Fern是否维护外部契约、错误格式、分页和限流。
ingestion 写入API route -> processEventBatch -> S3/Redis -> worker processor -> IngestionService -> ClickhouseWriter是否理解“请求侧只排队,写入侧再合并”的设计。
worker jobqueues.ts -> producer -> worker/src/queues/* -> worker/src/app.tsqueue name、job name、payload、processor 注册是否一致。
筛选语法search-bar README -> FilterState -> field map -> query buildergrammar、validate、lower、SQL 是否同构。
数据模型变更domain schema -> storage schema -> writer -> reader -> API/UI contract是否同时处理写入、读取、迁移和兼容。

1.3.6 读 infra repo 时必须追问的质量属性

代码“能跑”只是第一层。平台型系统还要回答:

质量属性在 Langfuse 里怎么问
可靠性job 失败后谁 retry?哪些错误应该进入 secondary queue?哪些错误不能无限重试?
可扩展性高吞吐事件是否走批量写入?查询是否避免大字段和无界扫描?
安全性project/org scope 在哪里校验?Public API key 与 session 鉴权是否分开?
成本为什么 raw event 放 S3、job 放 Redis、分析数据放 ClickHouse?
可观测性span attributes、queue metrics、ClickHouse log comment 是否覆盖关键路径?
兼容性queue payload、Public API response、ClickHouse schema 如何处理滚动部署和旧数据?

这些问题会把教程从“文件介绍”推向“系统原理”。

1.3.7 反模式

  1. 目录漫游:从 web/src/components 读起,会看到很多 UI 细节,但不知道请求归属哪条链路。
  2. 只看 route handler:会误以为 API route 做了全部事情,忽略 shared 契约和 worker 执行器。
  3. 只看 SQL:会错过筛选语法、tenant filter、field set、排序优化和大字段延迟读取。
  4. 忽略队列契约:producer 和 consumer 分属不同进程,payload 变更不是普通类型重命名。
  5. 忽略 env 开关:worker 里很多 processor 是否启动由配置决定,代码存在不代表运行时一定消费。
  6. 把 shared 当 utils:shared 里的类型会被多个运行时消费,放进去就意味着承诺了跨边界稳定性。

1.3.8 外部参考如何落到本书结构

本书后续章节会按这套方式组织:

  • 第 3 篇用 C4 式层级解释系统上下文、运行时容器和组件组合。
  • 第 4 篇用动态视角解释同步 API、异步 ingestion、worker、查询链路。
  • 第 5 篇专门讲契约,因为 infra repo 的变化风险主要出现在跨边界协议上。
  • 第 6 篇讲前端时不只讲 UI,而是讲 UI 状态如何进入 API 和查询契约。
  • 第 7 篇讲二次开发时用任务路线图,而不是散列文件名。

参考

下一节

从产品数据模型读 Langfuse