max_notify_queue_pages 并不是因为用户突然需要一个新的调优旋钮才出现的。它解决的是一个更基础的问题:过去由实现细节自然形成的容量上限不再可靠,于是 PostgreSQL 需要用显式配置重新定义 LISTEN/NOTIFY 队列的资源边界。
这个参数看似冷门,却关系到磁盘占用、跨数据库影响,以及生产事务能否顺利提交。只要系统用 PostgreSQL 通知做任务唤醒、缓存失效广播或轻量事件分发,就值得理解它。
一条队列服务整个集群
PostgreSQL 的 LISTEN/NOTIFY 不是为每个连接或数据库建立一条独立队列。通知被写入整个集群共享的队列,并以 SLRU 页面形式保存在数据目录的 pg_notify/ 下。
这里有三个容易忽略的含义:
- 队列容量是集群级资源,不是单个数据库的配额。
- 某个应用制造的通知压力,可能影响同一实例中的其他数据库。
- 参数按页面计数,而不是直接使用 MB 或 GB。
到 PostgreSQL 16 为止,旧实现中的限制会自然约束队列大小。当这个隐式约束不再成立后,如果没有新的显式上限,队列就可能失去原有的资源边界。max_notify_queue_pages 因此更像一道保险丝,而不是提高吞吐量的开关。
设置较大的值也不会立即分配等量内存。通知页面位于磁盘上的 SLRU 文件中,并按 PostgreSQL 的机制缓存。不过,更高的上限确实意味着异常情况下允许 pg_notify/ 消耗更多磁盘空间。
真正危险的通常不是通知多,而是队列无法回收
通知只有在事务提交后才会发送。监听者也不会在事务中途接收通知,而要等自己的事务结束。于是,一个已经执行 LISTEN、随后长时间停留在事务中的会话,可能阻止旧通知被清理。
队列持续增长时,常见链路是:
- 某个监听连接开启长事务。
- 其他连接持续提交不同的通知。
- 队列因无法越过旧位置而不断增长。
- 达到上限后,包含
NOTIFY的事务可能在提交阶段失败。
这也是为什么简单调大 max_notify_queue_pages 只能延后故障,不能消除长事务、消费者停滞或错误架构带来的问题。
可以先检查当前版本是否提供该参数,并把页面数换算成容易理解的容量:
SELECT version();
SELECT
name,
setting::bigint AS pages,
current_setting('block_size')::bigint AS bytes_per_page,
pg_size_pretty(
setting::bigint * current_setting('block_size')::bigint
) AS approximate_capacity,
context,
pending_restart
FROM pg_settings
WHERE name = 'max_notify_queue_pages';
如果查询没有返回记录,说明当前服务器版本不认识这个参数,不要直接把它写入配置文件。
队列利用率可以直接监控:
SELECT
pg_notification_queue_usage() AS usage_ratio,
round((pg_notification_queue_usage() * 100)::numeric, 6) AS usage_percent;
这个函数返回的是比例,而不是字节数。生产监控应同时记录增长速度;一个绝对值不高、但持续单调上升的队列,往往比短暂尖峰更值得警惕。
用两个 psql 会话复现阻塞场景
下面的实验适合在本地或测试数据库运行,不要用它测试生产队列容量。
在终端 A 中注册监听,然后故意停留在事务里:
LISTEN job_events;
BEGIN;
SELECT pg_sleep(300);
在等待期间,打开终端 B,提交一批 payload 各不相同的通知:
DO $$
BEGIN
FOR i IN 1..20000 LOOP
PERFORM pg_notify(
'job_events',
json_build_object('event_id', i, 'kind', 'demo')::text
);
END LOOP;
END
$$;
SELECT pg_notification_queue_usage();
payload 使用不同的 event_id,是为了避免同一事务中完全相同的通知被折叠。这个示例用于观察事务与通知交付的关系,不应该以填满队列为目标。
实验结束后回到终端 A:
ROLLBACK;
随后再查询利用率。队列清理是异步发生的,数值不一定在同一瞬间归零。
排查生产问题时,还可以先列出长事务候选者:
SELECT
pid,
datname,
usename,
application_name,
client_addr,
state,
now() - xact_start AS transaction_age,
wait_event_type,
wait_event,
left(query, 120) AS current_query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
这个查询不能证明某个会话就是监听者,但能快速缩小范围。应用最好设置明确的 application_name,否则事故发生时很难区分通知消费者和普通业务连接。
调整参数前先算容量
参数值是页面数。常见的 PostgreSQL 构建使用 8 KiB 页面,因此可以这样估算:
队列上限 ≈ max_notify_queue_pages × block_size
例如,下面只是一个约 2 GiB 的演示值,不能直接视为生产推荐:
# postgresql.conf
# 262144 pages × 8 KiB/page ≈ 2 GiB
max_notify_queue_pages = 262144
也可以通过 SQL 写入自动配置:
ALTER SYSTEM SET max_notify_queue_pages = 262144;
SELECT pg_reload_conf();
SELECT name, setting, context, pending_restart
FROM pg_settings
WHERE name = 'max_notify_queue_pages';
是否需要重启,应以 pg_settings.context 和 pending_restart 为准。如果服务器提示等待重启,在自管环境中可以使用:
pg_ctl restart -D "$PGDATA"
托管数据库则应通过云平台的参数组和维护窗口操作。调整前还要为 WAL、临时文件、检查点峰值和 pg_notify/ 留出共同的磁盘余量。
更稳妥的采用清单
把 max_notify_queue_pages 纳入配置管理时,可以逐项确认:
- 监控
pg_notification_queue_usage(),同时关注数值和增长斜率。 - 对监听连接设置短事务边界,避免空闲事务。
- 为消费者连接设置可识别的
application_name。 - 给
idle_in_transaction_session_timeout设置合理值,但先验证它不会误杀正常任务。 - 不要手工删除
pg_notify/下的文件。 - 不要把 LISTEN/NOTIFY 当作持久消息系统;消费者断线期间不能依赖它保存业务事件。
- 如果要求重放、确认、积压管理或每个消费者独立游标,应把真实事件写入表或专业消息系统,只用
NOTIFY发送唤醒信号。
这个参数最重要的价值不是让通知队列更大,而是让一个曾经隐含于实现中的限制变得可见、可监控、可规划。容量配置负责控制故障半径,短事务和可靠的事件存储才负责解决根因。