Postgres 当消息队列用,凌晨 3 点你会后悔的

凌晨 3 点,你的寻呼机响了。队列积压,数据库着火——而这两件事其实是同一件事。当有人建议你“就用 Postgres”时,没人警告过这个陷阱。

Postgres 作为消息队列:一个看似完美的捷径

“Just use Postgres” 这句建议在初期非常有效。你不需要部署六个不同的服务,一个单体应用就能搞定一切,晚上也能睡个好觉。对于小团队、初创项目或内部工具,Postgres 作为消息队列确实省去了引入额外组件的复杂度。你只需要建一张表,插入消息,再让 worker 去轮询或使用 LISTEN/NOTIFY,就能实现基本的队列功能。

这个捷径解决了实际问题:你不需要学习新的 API,不需要维护新的服务,所有数据都在同一个数据库里,事务一致性也天然得到保证。但问题恰恰隐藏在这种便利性中。当队列和业务数据放在同一个 Postgres 实例里,它们共享同一份资源——CPU、内存、磁盘 I/O、连接数。你最初以为只是“顺便”用一下,但队列的负载会逐渐侵蚀数据库的性能,而数据库的故障也会直接拖垮队列。

凌晨 3 点的真相:当队列和数据库变成同一个故障点

凌晨 3 点,寻呼机被激活。你的队列积压了,数据库也着火了——这两件事其实是同一件事。当 Postgres 同时承担业务数据和消息队列时,它就成了一个单点故障(SPOF)。队列积压会导致表膨胀、锁竞争、I/O 飙升,进而拖慢业务查询;而业务查询变慢又会进一步加剧队列积压,形成恶性循环。

更糟的是,当数据库发生故障(比如磁盘满、连接数耗尽、主从切换失败),队列和业务数据会同时不可用。你无法通过“重启队列服务”来恢复,因为队列本身就在数据库里。你只能面对一个既不能处理新消息、也不能响应业务请求的数据库。这种耦合让故障的爆炸半径变得极大,而恢复时间也成倍增加。

从“够用”到“失控”:Postgres 队列的扩展瓶颈

随着业务增长,Postgres 队列的瓶颈会逐渐显现。首先是吞吐量:Postgres 的轮询机制(SELECT … FOR UPDATE SKIP LOCKED)在高并发下会产生大量的锁等待和死锁重试,吞吐量远低于专用消息队列。LISTEN/NOTIFY 虽然能减少轮询,但它在高频率下会消耗大量连接资源,且消息不持久化,一旦连接断开就可能丢失。

其次是延迟:Postgres 队列的延迟通常在毫秒级,但在负载高峰时会飙升到秒级甚至分钟级。而专用消息队列(如 RabbitMQ、Kafka)通过内存缓冲和批量写入,能将延迟稳定在毫秒级以下。

运维上,Postgres 队列也会带来额外的负担。你需要定期清理已处理的消息,否则表会无限膨胀;你需要监控队列长度和数据库性能,但两者相互影响,难以隔离问题。当队列积压时,你无法单独扩容队列,只能扩容整个数据库,成本高昂。

真实案例:那些在凌晨 3 点后悔的团队

虽然没有公开的“某某公司因 Postgres 队列宕机”的详细报告,但类似的案例在技术社区中屡见不鲜。许多开发者在 Hacker News 或 Reddit 上分享过自己的经历:某个周末,队列突然积压,数据库 CPU 100%,所有业务请求超时,最终不得不手动清空队列、重启数据库,才勉强恢复。

更典型的场景是:团队为了快速上线,用 Postgres 实现了简单的任务队列,初期运行良好。但随着用户量增长,队列中的消息越来越多,清理任务跟不上,表膨胀导致索引失效,查询越来越慢,最终在凌晨 3 点触发告警。此时,你既不能简单地“重启队列”,也不能“扩容队列”,只能面对一个被队列拖垮的数据库,在凌晨 3 点手动处理数据。

替代方案:专用消息队列到底强在哪里

专用消息队列(如 RabbitMQ、Kafka)在设计上就与数据库不同。它们将消息的存储和传输与业务数据隔离,因此不会互相干扰。RabbitMQ 基于 Erlang/OTP,支持多种消息协议(AMQP、MQTT 等),提供可靠的消息确认、死信队列和灵活的路由,适合需要复杂路由和低延迟的场景。Kafka 则采用分布式日志架构,提供高吞吐、持久化和消息重放能力,适合日志收集、事件流处理和流式分析。

在架构上,专用消息队列可以独立扩展。你可以根据队列负载增加消费者实例,而不影响数据库;你可以对队列进行监控、告警和自动伸缩,而不用担心影响业务数据。在可靠性上,专用队列通常支持消息持久化、副本机制和故障转移,保证消息不丢失。

何时可以继续用 Postgres?—— 决策框架

Postgres 队列并非一无是处。在以下场景中,它仍然可以接受:

  1. 低吞吐量:每秒消息数在几十到几百,且峰值可控。
  2. 低延迟要求:允许秒级延迟,不需要毫秒级响应。
  3. 消息量小:队列长度通常不超过几千,且消息体小。
  4. 团队规模小:没有专门的运维团队,不想引入额外组件。
  5. 业务容忍短暂故障:队列故障不会导致核心业务中断,可以接受手动恢复。

如果满足以上条件,Postgres 队列可以作为一种“够用”的解决方案。但一旦业务增长,吞吐量或延迟要求超出 Postgres 的能力,或者队列故障会直接影响用户体验,就应该考虑迁移到专用消息队列。

架构演进:从 Postgres 队列平滑迁移到专用队列

如果决定迁移,可以采取以下策略来最小化风险:

  1. 双写模式:在迁移期间,同时向 Postgres 队列和专用队列写入消息。消费者优先从专用队列读取,Postgres 队列作为备份。这样可以在切换过程中保证消息不丢失。
  2. 逐步切换:先让一部分消费者使用专用队列,观察性能和稳定性,再逐步扩大比例,直到完全切换。
  3. 数据迁移:将 Postgres 中未处理的消息导出,并导入到专用队列中。注意处理消息顺序和重复消费的问题。
  4. 回滚计划:在切换过程中,保留 Postgres 队列的代码路径,以便在出现问题时快速回滚。

迁移完成后,及时清理 Postgres 中的队列表,释放资源。同时,对专用队列进行监控和告警,确保其稳定运行。

总之,Postgres 作为消息队列是一个“甜蜜的陷阱”。初期它让你省心,但长期来看,它可能成为你凌晨 3 点的噩梦。在架构决策时,务必评估业务规模和增长预期,不要被“Just use Postgres”的 hype 冲昏头脑。

参考来源