Postgres LISTEN/NOTIFY 实际上是可扩展的
Hacker News 摘要原标题:Postgres LISTEN/NOTIFY actually scales
Postgres 的 LISTEN/NOTIFY 机制因为一些性能方面的负面评价而常被误解。很多人认为它不具备可扩展性,但这其实是对其运行机制的误读。DBOS 团队通过优化,证明了在单台 Postgres 服务器上利用该机制可以实现每秒 60000 次写入,并保持毫秒级的延迟。
低延迟流式传输的挑战
在构建低延迟流式应用(例如大语言模型响应或在线聊天)时,一种常见的做法是建立一个流表,将每个数据块作为新行插入。读取端面临的问题是如何得知新数据的到来。
• 轮询方式:读取者不断查询数据库。这种方式在低频率时延迟高,在高频率时会消耗大量数据库资源,难以扩展。
• LISTEN/NOTIFY 方式:读取者挂起并等待通知。当写入者发布新数据时,发送通知唤醒读取者。这种方式不浪费轮询资源,能实现即时响应。
但在初始测试中,由于在触发器中直接调用 NOTIFY,每秒写入量仅能达到 2900 次左右,且数据库的 CPU 和磁盘利用率并不高,这说明性能遇到了瓶颈。
瓶颈根源:全局排他锁
Postgres 的 LISTEN/NOTIFY 性能问题源于其内部的锁机制。在提交包含 NOTIFY 调用的事务时,Postgres 会获取一个全局排他锁。该锁在事务开始提交时获取,直到事务完成磁盘刷新(fsync)后才释放。
Postgres 这样做的原因是它必须保证通知的发送顺序与事务提交顺序完全一致。它维护了一个全局内部队列,为了确保顺序,它必须串行化所有包含通知的事务提交过程。
这种设计导致了以下问题:
1. 写操作无法并发提交:即使是无关的流写入,也必须排队等待前一个事务刷新到磁盘。
2. 破坏了组提交优化:Postgres 无法将多个事务合并到一次磁盘刷新中,导致写入速度受限于磁盘同步的速度。
即使是未来的新补丁(如计划在 Postgres 19 发布的优化),也只是针对多频道监听场景的优化,并不能解决这个全局锁带来的吞吐量瓶颈。
优化方案:缓冲与批量处理
为了绕过这个瓶颈,DBOS 采用了缓冲机制。其核心思路是:通知本身并不是数据的唯一来源,它只是提醒读取者去查看数据库表。因此,通知不需要绝对的持久化保证,也不需要严格的全局顺序。
具体优化策略如下:
1. 内存缓冲:不直接在每次写入时调用 NOTIFY,而是将通知缓存在内存中。
2. 批量刷新:定期通过一个单独的事务批量发送通知。这样全局锁只会在批量刷新时被占用一次,而不会阻塞成千上万次的流写入。
3. 利用组提交:由于普通写入事务不再持有 NOTIFY 全局锁,它们可以利用 Postgres 的组提交特性并行处理,极大地提升了吞吐量。
4. 降级轮询机制:为了防止进程崩溃导致内存中的通知丢失,读取端保留了一个低频轮询作为备份。即使通知未送达,读取者也会定期检查数据库,这不会对性能产生明显影响。
性能基准测试结果
经过优化后,在有并发读取者的情况下,该系统实现了每秒 60000 次的流写入,性能提升了 20 倍。此时的延迟保持在 15 到 100 毫秒之间。
在达到最高吞吐量时,数据库的 CPU 达到了满负荷状态。这表明瓶颈已经从锁竞争转移到了实际的计算资源利用上,证明了 LISTEN/NOTIFY 机制在经过合理优化后具备极强的扩展能力。
原文:https://www.dbos.dev/blog/postgres-listen-notify-scalability