1.6 Infra Repo 阅读框架
学习目标
完成本节后,你要能回答三个问题:
- 读一个 infra / platform repo 时,为什么不能从目录树开始平均阅读。
- 如何把“架构图、契约、运行链路、状态、故障处理”拆成可验证的问题。
- 这套方法落到 Langfuse 当前 repo 时,应该先看哪些源码。
1.6.1 先给结论:读 infra repo 要先找控制面和数据面
多数初学者读大型 infra repo 会犯一个错误:打开目录树,然后从 components、services、utils 之类的目录开始看。这样很快会陷入细节,因为目录只能告诉你“代码在哪里”,不能告诉你“系统怎么动”。
读 Langfuse 这种 repo,第一步应该问:
| 问题 | 在 Langfuse 里的答案 | 先看源码 |
|---|---|---|
| 谁在系统外部调用它? | Browser、SDK、OTel collector、API client、LLM provider、webhook target | web/src/pages/api/public/**、web/src/pages/** |
| 哪些请求必须同步返回? | UI tRPC、Public REST API、ingestion 接收阶段 | web/src/server/api/trpc.ts、withMiddlewares.ts |
| 哪些工作必须异步处理? | ingestion 合并、eval、批量删除、导出、webhook、retention | worker/src/app.ts、worker/src/queues/** |
| producer 和 consumer 靠什么对齐? | Zod schema、QueueName、QueueJobs、TQueueJobTypes | packages/shared/src/server/queues.ts |
| 数据为什么分多种存储? | Postgres 管事务状态,ClickHouse 管分析事件,Redis 管队列/短期缓存,S3 管 raw payload | Prisma schema、ClickHouse migrations、StorageService |
这就是本书后续的核心读法:先看“面”,再看“层”,最后看“函数”。
1.6.2 外部教程给我们的组织方法
我参考了几类常见 infra / architecture 教程的写法,并把它们改造成适合本 repo 的阅读框架:
| 外部方法 | 适合学习什么 | 本书怎么用 |
|---|---|---|
| C4 model | 从系统上下文逐步放大到容器、组件、代码 | 第 3 篇先画外部入口、web、shared、worker、状态层,再在第 4 篇放大运行链路。 |
| Diátaxis | 区分教程、How-to、解释、参考 | 第 0-2 篇是入门教程,第 3-6 篇解释原理,第 7-8 篇是改造练习,附录是参考。 |
| Terraform/IaC 教程 | 先讲 root module、child module、inputs、outputs、state、environment | 本书把 packages/shared 当成“跨运行时输入/输出契约”,把 Postgres/ClickHouse/Redis/S3 当成状态层。 |
| ADR / architecture decision | 讲清“为什么这样设计”和替代方案代价 | 每条链路都解释为什么不是直接写 ClickHouse、为什么 queue payload 需要兼容旧 job。 |
| SRE runbook | 关注指标、失败、重试、退避、隔离 | worker 和 ClickhouseWriter 章节会讲 wait time、processing time、failed、stalled、rows_dropped。 |
外部参考可以从这里开始:
- C4 model:学习 context、container、component、dynamic diagram 的分层画法。
- Diátaxis:学习教程、how-to、reference、explanation 的文档分工。
- Terraform standard module structure:学习 infra 文档如何围绕入口、模块、输入、输出、示例组织。
- Google SRE Book: Monitoring Distributed Systems:学习运行系统文档为什么要关注指标、失败和故障语义。
所以这不是一本“文件名清单”。每章都按同一个问题模板展开:
- 入口在哪里。
- 入口拿到什么输入。
- 输入先被哪个 schema 或 procedure 校验。
- 它生成什么跨边界契约。
- 契约由谁消费。
- 状态写到哪里。
- 失败时如何重试、降级或暴露指标。
1.6.3 五张图读法
读 infra repo 至少需要五类图。少任何一类,都会漏掉关键复杂度。
这五张图在 Langfuse 中分别对应:
| 图 | 解释对象 | 本书位置 |
|---|---|---|
| 上下文图 | Browser、SDK、OTel、外部 provider、webhook target | 第 3 篇 |
| 容器图 | web、worker、packages/shared、ee、数据设施 | 第 3 篇 |
| 契约图 | Domain Zod、Queue payload、FilterState、QueryBuilder、Fern API | 第 5 篇 |
| 链路图 | tRPC、Public API、ingestion、worker、v4 events 查询 | 第 4 篇 |
| 状态图 | 事务状态、分析事件、raw payload、短期调度状态 | 第 3.5 节 |
1.6.4 本 repo 的阅读顺序
不要从 web/src/components 开始,也不要从 ClickHouse migration 开始。推荐顺序是:
- 先看 repo 边界:
AGENTS.md、pnpm-workspace.yaml、turbo.json、package.json。 - 再看运行时入口:
web/src/server/api/root.ts、web/src/pages/api/public/**、worker/src/app.ts。 - 再看跨运行时契约:
packages/shared/src/server/queues.ts、packages/shared/src/interfaces/filters.ts、packages/shared/src/domain/**。 - 再追一条主链路:例如 ingestion 从 HTTP 到 S3、Redis、worker、ClickHouse。
- 最后读局部 feature:例如 events table、score analytics、eval、prompt、dataset。
这里有一个判断标准:如果一个文件被 web 和 worker 同时依赖,它更可能是架构核心;如果一个文件只被一个页面引用,它通常是局部实现。
1.6.5 用 Langfuse 的 ingestion 做一次示范
以 SDK 上报 observation 为例,一条链路可以拆成下面的“教学颗粒度”:
| 层 | 发生了什么 | 源码 |
|---|---|---|
| 外部入口 | SDK POST event 或 batch | web/src/pages/api/public/ingestion.ts |
| API 边界 | CORS、method dispatch、错误处理、API key scope、rate limit、Zod response | withMiddlewares.ts、createAuthedProjectAPIRoute.ts |
| 请求侧数据面 | processEventBatch 校验 event、排序、按 entity 分组、写 raw JSON 到 S3、投递轻量 BullMQ job | packages/shared/src/server/ingestion/processEventBatch.ts |
| 队列契约 | job 只携带 type、eventBodyId、fileKey、bucketPrefix、authCheck.scope.projectId | packages/shared/src/server/queues.ts |
| 异步执行 | worker 读取 S3、做 Redis seen cache、secondary queue 分流、调用 IngestionService | worker/src/queues/ingestionQueue.ts |
| 业务转换 | 合并 create/update,补 usage/cost/prompt/tool metadata,生成 ClickHouse record | worker/src/services/IngestionService/index.ts |
| 写入策略 | ClickhouseWriter 按表排队、批量 JSONEachRow、retry、截断、drop metrics | worker/src/services/ClickhouseWriter/index.ts |
| 查询状态 | events_full 保存完整事件,events_core 是轻量查询投影 | ClickHouse migrations、event-query-builder.ts |
这张表比“web 调 shared,worker 写 ClickHouse”更有用,因为它能指导你修改系统:
- 如果你要改外部 payload,先改 ingestion schema 和 public API contract。
- 如果你要改异步 job,先看
queues.ts兼容性。 - 如果你要改写入表,先看
ClickhouseWriter和 ClickHouse migration。 - 如果你要改列表查询,先看
EventsQueryBuilder和selectIOAndMetadata。
1.6.6 读 infra repo 的七个检查问题
以后你读任何类似 repo,都可以用这七个问题:
| 问题 | Langfuse 示例 |
|---|---|
| 入口是不是可信? | Browser session 可信度不同于 Public API key;OTel ingestion 又是另一种来源。 |
| tenant scope 在哪里建立? | tRPC 用 protectedProjectProcedure,Public API 用 createAuthedProjectAPIRoute,worker 从 queue payload 继承 projectId。 |
| schema 是静态类型还是运行时校验? | FilterState、queue payload、domain schema 都用 Zod 表达运行时契约。 |
| 请求是否可以同步完成? | ingestion 不直接写 ClickHouse,而是 S3 + BullMQ + worker。 |
| 状态写入是否可重试? | BullMQ job 可重试,ClickhouseWriter 有 retry/backoff,S3 SlowDown 会打 secondary queue 标记。 |
| 查询是否会默认读大字段? | events list 默认 selectIOAndMetadata: false,详情/批量 I/O 再读 events_full。 |
| 修改是否影响 rolling deploy? | queue payload 字段通常要 optional,因为旧 job 可能仍在 Redis 里。 |
1.6.7 本书的粒度约束
为了让初学者能消化并做出类似 infra,本书每个核心章节都会保持三层粒度:
- 系统层:这个组件在控制面、数据面、契约面还是状态面。
- 源码层:具体入口文件、关键函数、输入输出。
- 设计层:为什么这样拆,替代方案会有什么问题。
如果一段内容只说“这里用了队列”“这里用了 ClickHouse”,那还不够。必须继续说明:
- 队列 payload 长什么样;
- producer 和 consumer 如何保持兼容;
- job 失败后谁负责重试;
- ClickHouse 写入是单条还是批量;
- 查询为什么读
events_core而不是events_full; - UI filter 最终如何变成 SQL。
后续章节都会按这个标准写。