第 8 章:Sidekiq、媒体、搜索与通知

一、Sidekiq 是第二条控制流

同步请求是一条短路径,Sidekiq 是一条可暂停、可重试、可批量和可延迟的控制流。理解一个 worker 至少要回答:

  1. 谁 enqueue 它?
  2. payload 里传了哪些 ID/选项?
  3. 它读哪些数据库事实?
  4. 哪些错误应该 retry?
  5. 重复执行是否安全?
  6. 成功后还有什么后续 job?

二、队列代表优先级与隔离

快照中的 Sidekiq 配置和 worker options 会把任务分到不同 queue,例如 default、push、pull、mailers、scheduler 等。队列不是装饰,它表达了资源和用户体验的优先级:

类型典型工作失败影响
pushActivityPub 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。

五、链接、预览卡和趋势

ProcessLinksServiceFetchLinkCardWorker 把正文中的 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 可能产生三层结果:

  1. Mention 关系:状态确实提及了某账户;
  2. Notification 事实:收件箱里有 mention/follow/favourite/reblog 等通知;
  3. 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_eachfind_in_batchespush_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 的价值在于把“用户觉得慢”拆成具体边界。

十一、源码入口清单

十二、小结

Sidekiq 不是简单的“后台线程”,而是 Mastodon 吸收外部不确定性和内部写放大的主要机制。下一章把注意力集中到安全:为什么认证、policy、可见性和签名必须在多个边界重复出现。