Mastodon 源码拆解
一条状态从浏览器进入 Rails,写入 PostgreSQL,经过 Sidekiq 分发到本地 Feed 与远端 Inbox,再由 Redis 推到 WebSocket 和 React UI——这条链路就是读懂 Mastodon 的主线。
这份拆解回答什么
参考 refs/dg-ai-notes 对 Pi-Agent 的写法,本教程每章都按四个问题展开:
- 是什么:先建立概念和边界,不急着跳进文件。
- 怎么做:沿真实调用链定位 class、service、worker、serializer 和前端 action。
- 为什么这样做:解释一致性、吞吐、安全性和联邦兼容之间的取舍。
- 从哪里继续读:列出源码入口、实验路径和容易踩坑的位置。
阅读地图
flowchart LR
A[浏览器 / 第三方客户端] --> B[Rails Web]
B --> C[(PostgreSQL)]
B --> D[Sidekiq]
D --> C
D --> E[ActivityPub HTTP]
B --> F[(Redis)]
D --> F
F --> G[Node Streaming]
G --> H[WebSocket / SSE]
H --> A
B --> I[React + Redux + Vite]
I --> A
先建立一张“系统心智模型”
Mastodon 不是一个只返回 JSON 的 Rails API。它是由多个角色共同完成一次用户动作的分布式系统:
| 角色 | 主要职责 | 关键源码目录 |
|---|---|---|
| Rails Web | 页面、REST API、OAuth、入站 ActivityPub | app/controllers, config/routes |
| Domain Service | 发帖、关注、通知、删除、联邦处理 | app/services |
| PostgreSQL | 事实数据、关系、约束、迁移 | app/models, db/migrate |
| Sidekiq | 延迟、重试、批量 fan-out、远端投递 | app/workers, config/initializers/sidekiq.rb |
| Redis | Feed、缓存、锁、Pub/Sub、速率限制 | app/lib, streaming/redis.js |
| Streaming | 将 Redis 事件转成 WebSocket/SSE | streaming/index.js |
| React | 时间线、通知、编辑器、管理界面 | app/javascript/mastodon |
重要事实与范围
本文档基于工作区中的 mastodon-main 快照。快照的 docker-compose.yml 使用 v4.6.4 镜像,前端 package.json 要求 Node.js 22+;因此文中的源码路径以该快照为准,不能把所有结论直接当成上游最新 main 的 API 保证。
从规模上看,快照包含约 248 个模型、338 个控制器、99 个 service、118 个 worker 和 531 个数据库迁移文件。数字用于帮助读者理解复杂度,不是运行时指标。
推荐阅读顺序
第一遍:只看主链路
第 1 章 → 第 3 章 → 第 4 章 → 第 6 章。这条路线能解释“点发送之后发生了什么”。
第二遍:理解网络与可靠性
第 2 章 → 第 5 章 → 第 8 章 → 第 9 章。这条路线解释数据为何这样建模、远端为何需要重试、权限为何不能只写在 Controller 中。
第三遍:准备改代码
第 7 章 → 第 10 章,然后回到具体的 service、worker 和对应 spec 做小范围实验。
一次发帖的全景追踪
POST /api/v1/statuses
└─ Api::V1::StatusesController#create
└─ PostStatusService#call
├─ 校验媒体、敏感标记、可见性、提及和引用
├─ 事务写入 Status / Mention / MediaAttachment 关系
└─ postprocess_status!
├─ ProcessHashtagsService
├─ LinkCrawlWorker
├─ DistributionWorker
└─ ActivityPub::DistributionWorker
DistributionWorker
├─ FanOutOnWriteService → Redis Feed + FeedInsertWorker
├─ NotifyService → LocalNotificationWorker / PushUpdateWorker
└─ ActivityPub::DeliveryWorker → 远端 inbox
Redis Pub/Sub
└─ streaming/index.js → WebSocket → React timeline reducer
接下来从进程拓扑开始。