Reed's News
← 返回精选

Postgres LISTEN/NOTIFY 实际可扩展

Tech 81 KraftyOne 2026/7/24 1093 字 原文 ↗

曾有一篇广为流传的博客称Postgres的LISTEN/NOTIFY机制无法扩展https://www.recall.ai/blog/postgres-listen-notify-does-not-scale,这让它背上了不好的名声。如果事实果真如此,那未免太过可惜——毕竟LISTEN/NOTIFY是个强大工具,能让Postgres数据库实现低延迟的可靠通知、流处理与发布/订阅(pub/sub)功能。批评并非全无道理:NOTIFY的性能特性确实违反直觉,且未在文档中明确说明,根源在于它使用了全局锁。但“违反直觉”不等于“无法扩展”。本文将展示我们如何优化基于LISTEN/NOTIFY的流处理系统,最终在单台Postgres服务器上实现每秒6万次写入,同时保持毫秒级延迟。

基于LISTEN/NOTIFY的低延迟流处理

用Postgres搭建流处理系统的基础设计很简单:创建一张流表,每个流数据块(比如大语言模型(LLM)的响应token)对应表中一行,写入流数据只需向表中插入新行即可。

难点在于读取流数据——你无法预知下一个数据块何时抵达。一种解决方案是轮询:让每个读取者持续查询流的末尾,检查是否有新数据块。但轮询的扩展性很差:如果轮询间隔太长,延迟会过高,无法满足在线聊天等交互式场景的需求;如果间隔太短,大量并发轮询请求会压垮数据库。

更好的方案是LISTEN/NOTIFY。它允许读取者阻塞等待,直到写入者发送通知,告知有新数据块已发布到流中。这样一来,读取者无需浪费资源轮询,新数据块一到就能立即唤醒处理。

在我们最初基于LISTEN/NOTIFY的流处理实现中,流表上的触发器会调用一个函数,每当有新流数据块写入时就发送通知。读取者等待这些通知,收到后就去读取新的数据块。

这个实现逻辑正确,延迟也很低,但在高并发场景下吞吐量极差。即便使用配置很高的Postgres数据库,它也无法维持每秒超过2900次的流写入。奇怪的是,性能瓶颈出现时,Postgres的CPU、内存、IOPS等资源使用率并没有明显飙升。你或许已经猜到,问题根源正是那个“LISTEN/NOTIFY无法扩展”的老问题:Postgres在执行NOTIFY时会获取全局锁。那么,Postgres为什么要这么做?我们又该如何优化,同时保留Postgres通知机制的优势?

LISTEN/NOTIFY的排他锁

要理解问题所在,我们得先搞清楚Postgres LISTEN/NOTIFY的实际工作机制。

性能低下的根源在于:在Postgres中,提交包含NOTIFY调用的事务时,必须获取全局排他锁。这把锁在事务开始提交时获取,直到事务完全提交、内容通过fsync()刷入磁盘后才会释放。

这把锁是必要的,因为Postgres保证通知会按照事务提交的顺序发送。为了实现这一点,Postgres会将所有待发送的通知存储在一个全局内部队列中,队列顺序必须与发送通知的事务提交顺序完全一致。将通知加入队列的操作必须作为提交过程的一部分,以事务方式执行。但Postgres要等到事务完成提交后才会确定其提交顺序——因为事务提交所需的时间并不固定。

这就产生了一个顺序难题:包含通知的事务必须按提交顺序加入队列,但提交顺序要到提交完成后才能确定。解决办法就是全局锁:它让包含通知的事务提交操作串行化,这样就能提前确定提交顺序,确保事务在内部通知队列中排列正确。

这把排他锁解释了我们观察到的性能瓶颈。由于我们通过流表上的触发器调用NOTIFY,每一次流写入都会触发NOTIFY调用。要完成提交,每个流写入操作都必须获取全局锁,并在整个提交过程(包括磁盘刷写)中持有锁。这意味着流写入操作必须串行提交,Postgres原本的组提交(将多个事务合并,通过一次fsync()完成提交)等优化手段都无法生效。结果就是,流写入的速度不可能超过Postgres单事务提交的速度,这就造成了瓶颈。这也解释了为什么我们看不到CPU或磁盘等资源被大量占用:所有事务都被全局锁串行化了,根本没有资源竞争的情况。

顺带一提,网上有关于Postgres相关补丁https://github.com/postgres/postgres/commit/282b1cde9dedf456ecf02eb27caf086023a7bb71的讨论。这个计划在Postgres 19中发布的补丁并未移除全局锁,也没有解决我们遇到的瓶颈问题。它只是针对一种更特定的场景做了优化:当存在大量通知通道,且每个监听者只监听特定通道时。

优化LISTEN/NOTIFY

要让基于LISTEN/NOTIFY的流处理系统更快,我们必须绕开这个瓶颈。关键洞察在于:对于流处理以及LISTEN/NOTIFY的许多其他应用场景,通知本身并非数据源,它只是用来触发读取者去检查数据库表(真正的数据源)中是否有新数据。因此,通知不需要全局有序,也不需要绝对可靠。基于这一点,我们可以通过在内存中缓冲通知、定期批量刷写的方式优化NOTIFY,大幅减少全局锁的竞争。

缓冲并批量发送NOTIFY可以避免瓶颈,因为只有在刷写缓冲时才需要获取全局锁,而不是每一次流写入都要获取。这意味着单个流写入操作可以快速执行,充分利用Postgres的组提交等优化手段实现高吞吐量,而缓冲刷写则在后台完成。

引入缓冲会带来新问题:如果缓冲中有待发送的通知时进程崩溃,这些通知就永远无法送达。为解决这个问题,我们给流读取者增加了一个 fallback 机制:除了等待通知,读取者还会定期轮询数据库,检查是否存在没有对应通知的流写入。由于轮询只是作为未送达通知的兜底,频率可以很低,因此不会对性能造成显著影响。

对优化后的方案进行基准测试后,我们看到性能大幅提升:在有并发读取者的情况下,系统每秒可处理多达6万次流写入(是之前的20倍),同时延迟保持在15-100毫秒。在最大吞吐量下,Postgres的CPU被完全利用,说明数据库真正达到了负载饱和,而非因锁竞争陷入瓶颈。

了解更多

所有基准测试代码均可在GitHub获取:github.com/dbos-inc/dbos-postgres-benchmark

如果你热爱构建可扩展、高可靠的系统,欢迎联系我们。在DBOS,我们的目标是让基于Postgres的可靠执行尽可能简单高效。欢迎了解: