开发者生态
morning
Postgres LISTEN/NOTIFY 实际上可以扩展
2026-07-25
1 阅读
约5分钟阅读
KraftyOne
字号:
Postgres LISTEN/NOTIFY 名声不佳,部分原因是一篇流行的博客文章声称它无法扩展。如果这是真的,那就太可惜了,因为 LISTEN/NOTIFY 是一个强大的工具,允许您使用 Postgres 数据库进行低延迟持久通知、流和发布/订阅。这些指控并没有错:NOTIFY 由于使用全局锁而具有不直观且未记录的性能特征。但“不直观的行为”并不等同于“不可扩展”。在这篇博文中,我们将展示如何大规模优化 LISTEN/NOTIFY 支持的流,在单个 Postgres 服务器上实现每秒 60K 写入,并具有毫秒级延迟。使用 LISTEN/NOTIFY 的低延迟流 Postgres 支持的流的基本设计很简单:创建一个流表,其中每个流块(例如,LLM 响应令牌)都是一个新行,然后通过插入到表中来写入流。 棘手的部分是从流中读取,因为您不知道下一个块何时到达。一种解决方案是轮询:让每个读取器轮询流的末尾以查找新块。然而,民意调查的规模很差。如果轮询间隔设置得太高,则对于交互式用例(例如在线聊天)来说延迟太高。但如果轮询间隔设置得太低,并发轮询器就会淹没数据库。更好的解决方案是聆听/通知。这允许读取器阻止等待写入器发出新块已发布到流的通知。这样,读者就不会浪费资源进行轮询,而是在新的流块到达时立即唤醒。在我们基于 LISTEN/NOTIFY 的流的初始实现中,流表上的触发器触发了一个函数,该函数在每次写入新的流块时发送通知。读者等待这些通知并醒来看到一个新的流块。这种实现是正确的,并且延迟较低,但从规模上看,其吞吐量很差。即使使用大型 Postgres 数据库,它也无法维持每秒超过 2.9K 的流写入。有趣的是,它在没有明显消耗任何 Postgres 资源(CPU、内存或 IOPS)的情况下出现了瓶颈。正如您可能已经猜到的,根本原因是最初的“LISTEN/NOTIFY 不可扩展”问题:Postgres 在 NOTIFY 期间采用全局锁定。但是为什么 Postgres 会这样做,我们如何在不失去 Postgres 通知优势的情况下对其进行优化呢? LISTEN/NOTIFY 独占锁 要理解这个问题,我们需要检查 Postgres LISTEN/NOTIFY 的实际工作原理。性能不佳的根本原因是在Postgres中,提交调用NOTIFY的事务需要获取全局排他锁。该锁在事务开始提交时获取,直到事务完全提交并且其内容已使用 fsync() 刷新到磁盘后才会释放。此锁是必要的,因为 Postgres 保证通知按照事务提交顺序发送。为了强制执行此操作,它将所有传出通知存储在全局内部队列中,其顺序必须与发送这些通知的事务的提交顺序完全匹配。向此队列添加通知必须作为提交的一部分以事务方式完成。但是,Postgres 在事务完成提交之前不会为事务分配提交顺序,因为提交可能需要不同的时间。这会产生一个排序问题:包含通知的事务必须按提交顺序将自己添加到队列中,但提交顺序在提交完成之前才定义。解决方案是全局锁,它序列化包含通知的事务的提交,因此它们的提交顺序是提前定义的,并且它们可以在内部通知队列中正确地对自己进行排序。这种独占锁解释了我们观察到的糟糕性能。因为我们从流表上的触发器调用 NOTIFY,所以每个流写入都包含对 NOTIFY 的调用。为了提交,每个流写入都需要获取全局锁并在提交的整个持续时间内保持它,包括刷新到磁盘。这意味着流写入需要按顺序提交,从而排除了 Postgres 通常的优化,例如组提交(在单个 fsync() 中一起提交许多事务)。因此,流写入的完成速度不会快于 Postgres 提交事务的速度,从而导致了这一瓶颈。这也解释了为什么我们没有看到任何 Postgres 资源(例如 CPU 或磁盘)的大量消耗:没有任何资源,因为所有事务都是由全局锁序列化的。顺便说一句,网上有一些与此问题相关的 Postgres 补丁的讨论。该补丁(将在 Postgres 19 中发布)不会消除全局锁或修复我们观察到的瓶颈。相反,它优化了较窄的情况,即有许多通知通道并且每个侦听器仅在特定通道上等待。优化 LISTEN/NOTIFY 使 LISTEN/NOTIFY 支持的流更快
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱