第 8 章:Sidekiq、媒体、搜索与通知
一、Sidekiq 是第二条控制流
同步请求是一条短路径,Sidekiq 是一条可暂停、可重试、可批量和可延迟的控制流。理解一个 worker 至少要回答:
- 谁 enqueue 它?
- payload 里传了哪些 ID/选项?
- 它读哪些数据库事实?
- 哪些错误应该 retry?
- 重复执行是否安全?
- 成功后还有什么后续 job?
二、队列代表优先级与隔离
快照中的 Sidekiq 配置和 worker options 会把任务分到不同 queue,例如 default、push、pull、mailers、scheduler 等。队列不是装饰,它表达了资源和用户体验的优先级:
| 类型 | 典型工作 | 失败影响 |
|---|---|---|
| push | ActivityPub delivery、推送 | 远端延迟,不应阻塞发帖 |
| pull | 拉取远端 actor/status | 本地看到远端内容变慢 |
| mailers | 邮件通知 | UI 事实不受影响 |
| scheduler | 定时状态、poll 到期 | 时间精度与任务积压有关 |
| default | 常规本地副作用 | 可能直接影响时间线 |
生产部署需要根据实例规模配置多个 Sidekiq 进程/并发,而不是让一个进程处理所有队列。
三、重试语义:不是所有异常都应该 retry
临时网络/远端 5xx/连接超时 → retry
Redis 短暂不可用 → retry 或由上层恢复
记录被删除 → 通常安全 return
参数非法/权限已失效 → 不应无限 retry
目标永久不存在/不可恢复授权失败 → 标记 dead/unsalvageable
ActivityPub::DeliveryWorker 自定义 retry backoff 和 jitter,是为了避免大量实例在同一时刻重试同一远端。错误分类是可靠性设计的核心,比“最大重试次数”更重要。
四、媒体处理是异步流水线
媒体附件包含上传、内容类型检查、尺寸/时长读取、缩略图、预览、远端存储、OCR/转码等阶段。发帖 service 只在媒体已 ready 时允许绑定,后续处理由 worker 完成。
flowchart LR
Upload[上传临时媒体] --> Validate[大小/类型/权限]
Validate --> Process[缩略图/转码/元数据]
Process --> Ready[可绑定 Status]
Ready --> Remote[远端缓存/ActivityPub]
Process --> Failed[失败 + 可重试]
排障时先看 MediaAttachment 的 processing state 和存储路径,再看 status publish 是否因为 not_ready 拒绝。不要只检查浏览器上传请求返回 200。
五、链接、预览卡和趋势
ProcessLinksService 与 FetchLinkCardWorker 把正文中的 URL 转成 preview card。它们需要面对:
- 外部 HTTP 超时/重定向;
- HTML 解析和内容类型;
- SSRF 与私有网络地址;
- 图片/视频抓取成本;
- 远端站点限速;
- 已删除或编辑状态的重新索引。
Hashtag 处理也被拆成独立 service/worker:先建立 tag 关系,再注册 trends、tag followers 和 hashtag streams。
六、搜索:数据库事实与搜索索引
Mastodon 使用 Chewy/Elasticsearch 相关能力时,数据库仍是 source of truth,搜索索引是派生结构:
Account / Status commit
└─ index update enqueue
├─ bulk index
├─ retry on ES failure
└─ remove on delete/suspend
搜索结果必须再次经过可见性和 viewer policy 过滤。索引命中不等于用户有权看到,因为索引更新和权限状态可能不是同一时刻完成。
七、通知是“事实 + 投影 + 推送”
一次 mention 可能产生三层结果:
Mention关系:状态确实提及了某账户;Notification事实:收件箱里有 mention/follow/favourite/reblog 等通知;- streaming/web push/email:把更新推给在线或离线用户。
NotifyService 会根据类型、收件人偏好、静音、blocked 状态和 streaming subscription 决定后续动作。通知的“是否 unread”与“是否应该即时推送”是两个不同问题。
八、批处理与内存
大型实例不能把全部 followers、mentions 或 status 读进内存。典型写法是:
find_in_batches/in_batches;select(:id, ...)只读必要列;push_bulk一次提交多个 job;- 缓存序列化 payload;
- 分离数据库连接池和 Redis pool;
- 对每个 host 或账号做速率与失败隔离。
读一个 worker 时,看到 find_each、find_in_batches 和 push_bulk,就应把它当成吞吐设计,而不是风格偏好。
九、清理与 vacuum
删除账户、删除状态、域名阻断和媒体清理会产生大量反向副作用:feed 引用、通知、搜索索引、远端对象、缓存、附件和统计。Mastodon 提供维护 CLI 和 vacuum 类 service,把“在线请求不应做的清理”放进可观测、可分批执行的后台任务。
如果只删除 PostgreSQL row 而不处理投影,用户仍可能从 Redis feed、搜索结果或 CDN 地址看到残留。反过来,先删缓存而数据库事务回滚,会产生短暂缺失,通常需要由重建/重试修复。
十、可观测性
需要同时观察:
- Sidekiq queue latency、busy、retry、dead;
- 每个 worker 的成功/失败/执行时间;
- Redis pool、命令延迟、memory、eviction;
- PostgreSQL slow query、连接池、锁;
- Elasticsearch bulk 失败与 lag;
- 媒体处理耗时、失败类型、存储容量;
- ActivityPub 每 host delivery success/failure;
- streaming connected clients 和 Redis message rate。
OpenTelemetry、Prometheus exporter 和日志 middleware 的价值在于把“用户觉得慢”拆成具体边界。
十一、源码入口清单
config/initializers/sidekiq.rbapp/workersapp/services/notify_service.rbapp/services/fetch_link_card_service.rbapp/services/process_links_service.rbapp/chewylib/mastodon/cli
十二、小结
Sidekiq 不是简单的“后台线程”,而是 Mastodon 吸收外部不确定性和内部写放大的主要机制。下一章把注意力集中到安全:为什么认证、policy、可见性和签名必须在多个边界重复出现。