PostgreSQL 的 max_notify_queue_pages:给 LISTEN/NOTIFY 队列加一道明确边界

2026-09-18 30 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

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、随后长时间停留在事务中的会话,可能阻止旧通知被清理。

队列持续增长时,常见链路是:

  1. 某个监听连接开启长事务。
  2. 其他连接持续提交不同的通知。
  3. 队列因无法越过旧位置而不断增长。
  4. 达到上限后,包含 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.contextpending_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 发送唤醒信号。

这个参数最重要的价值不是让通知队列更大,而是让一个曾经隐含于实现中的限制变得可见、可监控、可规划。容量配置负责控制故障半径,短事务和可靠的事件存储才负责解决根因。


相关推荐