Multigres支持跨连接池的监听/通知
Postgres的LISTEN / NOTIFY有一个扩展性问题:监听的客户端越多,每个通知接收的速度就越慢——一旦你进入数千个客户端就会出现数量级的下降。即使Multigres将连接从客户端池化,它仍然保持这一线性关系——这是该功能所依赖的唯一因素。它是如何实现这一点,以及你需要正确处理的边缘情况使其表现得与Postgres完全一致,正是本文讨论的内容。LISTEN / NOTIFY是Postgres内置的发布/订阅机制。一个会话订阅一个命名的频道:LISTEN事件;另一个会话向其发布:NOTIFY事件,例如'缓存无效:用户:42';当前在事件上监听的每个会话都会接收到一个异步消息,其中包含通知后端的PID、频道名称和有效负载。在网络上传输时,服务器将其作为NotificationResponse(消息类型'A')推送到客户端,超出正常的请求/响应流程。应用程序依赖此机制进行缓存失效、作业队列和实时分发,正是因为它避免了轮询。LISTEN将当前会话注册为监听器,该注册在会话结束时立即清除;这是每个连接的状态。另一方面,交付是更广泛的:NOTIFY到达同一数据库中所有监听该频道的会话,而不管它依附于哪个后端连接。Postgres通过一个单一的集群范围的磁盘队列(pg_notify/)实现这一点,为每个通知标记发送者的数据库OID以保持数据库本地,并通知每个监听的后端来读取它。简而言之,注册是每会话,交付是每数据库。请注意Postgres将该功能绑定到的内容:一个会话。而连接池的整个工作就是阻止客户端拥有一个。Multigres故意将客户端连接与任何单一的Postgres后端解耦,客户端的查询可以在不同的后端连接间交替进行。由于没有持久的会话来存在,LISTEN没有任何东西可以附着:LISTEN无法在借用的连接上运行。如果在任何一个空闲的后端上执行LISTEN事件,那么下一个语句可能会被路由到其他地方,而注册将被困在客户端不再拥有的连接上。通知将会到达客户端无法看到的地方。Postgres会忠实地将通知送到运行LISTEN的任何后端,但在连接池中,该后端并不是客户端拥有的。消息将落在一个已经被归还给池并被其他人重新使用的连接上。Postgres在这里并不是问题。只要它们共享相同数据库的相同实例,Postgres可以很好地在不同的后端连接之间传递通知。Multigres正是依靠这种行为——但仅在解决了上述会话拥有权问题后。天真的修复方法(将每个监听客户端固定到一个专用后端连接)在正好大量持久监听器的工作负载中抛弃了池化,这正是池化最重要的地方。我们需要监听器保持可池化。核心理念是:客户端永远不会拥有一个用于监听的Postgres会话。连接池拥有它。每个连接池保持一个与其Postgres后端的单个长生命周期的监听器连接,与查询池分开。它是懒惰地获取的(在任何客户端首次发出LISTEN时,而不是提前)并且是保留的,因此连接池将其视为永久借用,只要任何人正在监听,并且永远不会在我们之下回收它。这个连接代表连接到该连接池的每个客户端进行监听。监听器使用引用计数跟踪频道:第一个订阅该频道的客户端触发后端的真实LISTEN频道,最后一个退订触发匹配的UNLISTEN,因此我们对每个独特频道发布正好一个后端订阅,无论有多少个客户端希望订阅。因为监听连接必须发送LISTEN / UNLISTEN命令,同时接收连续的通知流,所以它在单个套接字上运行时采用拆分读取/写入模型:一个读取的goroutine排干传入消息,而事件循环写入订阅更改。这避免了每次频道集合改变时拆除并重新建立连接,这本身可能会造成通知丢失。现在,NOTIFY自然地流出。Multigres在执行时并不将其视为任何特殊的事情——计划程序将其作为普通查询路由到一个固定的目标。这个固定的路由是故意的:Postgres通知从一个实例传递到另一个实例从来没有交叉,因此每个NOTIFY必须落在与监听连接正在监视的同一实例上,以便二者能够相遇。NOTIFY在正常的后端连接上运行,Postgres将通知投递给所有在该数据库中监听该频道的会话,包括池的共享监听器,它只是同一实例上的另一个后端。这一步完全依赖于Postgres的本地跨后端交付:
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡