River:一个适用于 Go 和 Postgres 的快速、健壮的作业队列

一个新的 Go 库 River 提议将 PostgreSQL 用作事务性作业队列,把作业创建与修改应用数据的同一数据库事务绑定在一起。支持者认为这种“单一依赖”方案简化了架构,通过原子化入队提升正确性,并且对大多数工作负载来说扩展性足够,像 Oban 这样的类似系统也证明了这一点。批评者则反驳说,关系型数据库并不适合高规模队列,更偏好专用服务或基于 HTTP 的任务系统,同时提出了关于性能、长时间运行作业,以及与 Redis、Kafka、Temporal 或云任务队列等工具相比的权衡。

设计:基于 Postgres、事务性的作业队列

  • 核心想法:作业与业务数据在同一个数据库事务中入队(“transactional outbox” 风格)。
  • 这确保了作业创建与领域变更具有原子性:要么两者都提交,要么都不提交。
  • 作业由独立进程处理;事务只用于入队,不用于执行。
  • 通过 ScheduledAt 选项支持定时(延迟)作业,不过文档仍在逐步完善中。

RDBMS 作为队列:优点与缺点

  • 支持者:
    • 强正确性保证、简单的心智模型;如果已经在使用 Postgres,移动组件更少。
    • 对大多数系统来说吞吐量足够;其他生态中的例子显示每秒可达数万作业。
    • 对许多中小型应用来说,运维比再引入 Redis/RabbitMQ/Kafka 更简单。
  • 怀疑者:
    • “数据库不适合作为队列”仍是常见看法,理由包括可扩展性、膨胀和长时间运行作业问题。
    • 有些人基于以往经验表示他们“绝不会”把 RDBMS 用作作业队列。

实现细节与模式

  • 典型模式:使用 SELECT … FOR UPDATE SKIP LOCKED(或其变体)在多个 worker 之间安全地租借作业。
  • 建议包括:
    • 使用 FOR NO KEY UPDATE 以避免阻塞外键插入。
    • 对作业表进行分区、按租户排序,以及通过租借进行批量处理,以获得更好的吞吐量。
  • Postgres NOTIFY 可用于唤醒 worker,但有人担心其开销以及与 PgBouncer 之类连接池的兼容性。
  • 一些功能仍在开发中或被请求:UI 仪表盘、作业完成通知、更丰富的工作流支持。

与其他系统的比较

  • 文中引用了其他语言中的 Postgres 作业队列(例如知名的 Elixir 和 JS 库),作为先例和该模型可行的证据。
  • 讨论的替代方案包括:
    • 基于 HTTP 的队列(GCP Tasks、SQS 风格)因简单性受到称赞,但也因事务正确性问题和超时限制而受到批评。
    • 基于 Kafka 或 NATS 的方案、类似 Temporal 的工作流引擎,以及纯 SQL 队列(例如 PGMQ)被提及为不同的权衡点。

项目方向、许可证与署名

  • 有人关心是商业化目标还是纯开源目标,以及类似的 Go 队列是否应“联合起来”。
  • 由于 Go 库采用 LGPLv3 许可证且涉及静态链接,其可行性受到质疑;维护者承认需要澄清或调整。
  • 关于设计在多大程度上受现有基于 Postgres 的作业系统启发,以及需要更清晰署名的问题,也存在争论。