Skip to content

1.6 Infra Repo 阅读框架

学习目标

完成本节后,你要能回答三个问题:

  1. 读一个 infra / platform repo 时,为什么不能从目录树开始平均阅读。
  2. 如何把“架构图、契约、运行链路、状态、故障处理”拆成可验证的问题。
  3. 这套方法落到 Langfuse 当前 repo 时,应该先看哪些源码。

1.6.1 先给结论:读 infra repo 要先找控制面和数据面

多数初学者读大型 infra repo 会犯一个错误:打开目录树,然后从 componentsservicesutils 之类的目录开始看。这样很快会陷入细节,因为目录只能告诉你“代码在哪里”,不能告诉你“系统怎么动”。

读 Langfuse 这种 repo,第一步应该问:

问题在 Langfuse 里的答案先看源码
谁在系统外部调用它?Browser、SDK、OTel collector、API client、LLM provider、webhook targetweb/src/pages/api/public/**web/src/pages/**
哪些请求必须同步返回?UI tRPC、Public REST API、ingestion 接收阶段web/src/server/api/trpc.tswithMiddlewares.ts
哪些工作必须异步处理?ingestion 合并、eval、批量删除、导出、webhook、retentionworker/src/app.tsworker/src/queues/**
producer 和 consumer 靠什么对齐?Zod schema、QueueNameQueueJobsTQueueJobTypespackages/shared/src/server/queues.ts
数据为什么分多种存储?Postgres 管事务状态,ClickHouse 管分析事件,Redis 管队列/短期缓存,S3 管 raw payloadPrisma 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。

外部参考可以从这里开始:

所以这不是一本“文件名清单”。每章都按同一个问题模板展开:

  1. 入口在哪里。
  2. 入口拿到什么输入。
  3. 输入先被哪个 schema 或 procedure 校验。
  4. 它生成什么跨边界契约。
  5. 契约由谁消费。
  6. 状态写到哪里。
  7. 失败时如何重试、降级或暴露指标。

1.6.3 五张图读法

读 infra repo 至少需要五类图。少任何一类,都会漏掉关键复杂度。

这五张图在 Langfuse 中分别对应:

解释对象本书位置
上下文图Browser、SDK、OTel、外部 provider、webhook target第 3 篇
容器图webworkerpackages/sharedee、数据设施第 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 开始。推荐顺序是:

  1. 先看 repo 边界AGENTS.mdpnpm-workspace.yamlturbo.jsonpackage.json
  2. 再看运行时入口web/src/server/api/root.tsweb/src/pages/api/public/**worker/src/app.ts
  3. 再看跨运行时契约packages/shared/src/server/queues.tspackages/shared/src/interfaces/filters.tspackages/shared/src/domain/**
  4. 再追一条主链路:例如 ingestion 从 HTTP 到 S3、Redis、worker、ClickHouse。
  5. 最后读局部 feature:例如 events table、score analytics、eval、prompt、dataset。

这里有一个判断标准:如果一个文件被 webworker 同时依赖,它更可能是架构核心;如果一个文件只被一个页面引用,它通常是局部实现。

1.6.5 用 Langfuse 的 ingestion 做一次示范

以 SDK 上报 observation 为例,一条链路可以拆成下面的“教学颗粒度”:

发生了什么源码
外部入口SDK POST event 或 batchweb/src/pages/api/public/ingestion.ts
API 边界CORS、method dispatch、错误处理、API key scope、rate limit、Zod responsewithMiddlewares.tscreateAuthedProjectAPIRoute.ts
请求侧数据面processEventBatch 校验 event、排序、按 entity 分组、写 raw JSON 到 S3、投递轻量 BullMQ jobpackages/shared/src/server/ingestion/processEventBatch.ts
队列契约job 只携带 typeeventBodyIdfileKeybucketPrefixauthCheck.scope.projectIdpackages/shared/src/server/queues.ts
异步执行worker 读取 S3、做 Redis seen cache、secondary queue 分流、调用 IngestionServiceworker/src/queues/ingestionQueue.ts
业务转换合并 create/update,补 usage/cost/prompt/tool metadata,生成 ClickHouse recordworker/src/services/IngestionService/index.ts
写入策略ClickhouseWriter 按表排队、批量 JSONEachRow、retry、截断、drop metricsworker/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。
  • 如果你要改列表查询,先看 EventsQueryBuilderselectIOAndMetadata

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,本书每个核心章节都会保持三层粒度:

  1. 系统层:这个组件在控制面、数据面、契约面还是状态面。
  2. 源码层:具体入口文件、关键函数、输入输出。
  3. 设计层:为什么这样拆,替代方案会有什么问题。

如果一段内容只说“这里用了队列”“这里用了 ClickHouse”,那还不够。必须继续说明:

  • 队列 payload 长什么样;
  • producer 和 consumer 如何保持兼容;
  • job 失败后谁负责重试;
  • ClickHouse 写入是单条还是批量;
  • 查询为什么读 events_core 而不是 events_full
  • UI filter 最终如何变成 SQL。

后续章节都会按这个标准写。

下一节

快速开始