Postgres LN 扩展性优化:2.9K 到 60K/秒

如果你写过 Postgres 的 LISTEN/NOTIFY,大概率听过一句忠告:这东西不能扩展,生产环境慎用。

这个标签不是凭空来的。你看到过流行的 benchmark 显示它撑死了每秒 2.9K 写入------对大部分后端场景确实不够看。

但 DBOS 团队用一套优化把这个数字推到了 60K 写入/秒(20x),延迟 15-100ms。更值钱的是 Hacker News 上 300 多分的讨论,从多个角度挖出了幕后真相。

问题是:60K/s 意味着 LISTEN/NOTIFY 真的「扩展了」吗?还是说藏着什么条件?

一、LISTEN/NOTIFY 的常见应用

sql 复制代码
-- 订阅端
LISTEN channel_name;

-- 发布端
NOTIFY channel_name, 'payload';

NOTIFY 不是在调用时立即发送,而是在 事务提交之后 才发出。事务回滚→通知不发,保证了事务一致性。典型用途:

场景 做法
缓存失效 数据变更后 NOTIFY → 订阅方清除本地缓存
实时通知 新消息写入后 NOTIFY → 在线用户收到推送
流式处理 新数据到达后 NOTIFY → 消费者批量拉取
实时协作 消息写入后 NOTIFY → 其他参与者收到更新

但问题就出在这个事务提交通知机制上。

二、全局锁:2.9K/s 的罪魁祸首

sql 复制代码
-- naive 实现:每行 insert 都 NOTIFY
CREATE FUNCTION notify_stream_chunk() RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('stream_channel', NEW.id::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DBOS 最初就是这样做的,然后发现无论你怎么调参数,吞吐始终卡在 2.9K/s,但 CPU、内存、IOPS 全部没有负载。

根因:NOTIFY 的事务提交需要拿全局排它锁(LWLock:NotifyQueueLock)。持有到 fsync 完成才释放。

为什么要有这个锁?因为 PG 要保证通知按事务提交顺序发送。但「提交顺序」在提交完成后才能确定。PG 的解法:用锁串行化所有含 NOTIFY 的提交。后果是 group commit 失效,每个 fsync 都变成了一场串行等待。

顺带一提,Postgres 19 即将发布的 patch 不解决这个问题------它优化的是多 channel 场景下 listener 的唤醒效率,全局锁还在。

json 复制代码
// 瓶颈速览
{
  "naive 吞吐": "2.9K 写入/秒",
  "瓶颈": "LWLock:NotifyQueueLock(全局排它锁)",
  "后果": "group commit 失效,每个提交串行 fsync",
  "PG 19 patch": "只解决多 channel 场景,不涉及全局锁"
}

三、优化方案:buffer + batch + fallback

核心洞察:NOTIFY 只是信号(ping),不是数据本身。 缓存失效场景下,客户端收到通知后会去查数据库拿最新数据。通知丢了一个,等到下次 polling 也能补上。它不需要全局有序,也不需要完全持久化。

第一步:内存 buffer

不直接 NOTIFY,把通知放到内存队列里。

第二步:批量 flush

后台线程每 50ms 把 buffer 一次性 flush 出去。全局锁只在 flush 时拿一次。

第三步:降级轮询

读者除了 LISTEN,每 30 秒 polling 兜底。防止进程 crash 导致通知丢失。

python 复制代码
# 伪代码:核心逻辑
notify_buffer = []

def flush_notify_buffer():
    if not notify_buffer:
        return
    ids = notify_buffer[:]
    notify_buffer.clear()
    db.execute("SELECT pg_notify('channel', $1)", [json.dumps(ids)])

四、60K/s 值得庆祝吗?

方案 吞吐
Naive trigger → NOTIFY 2.9K/s
Buffer + batch + polling 60K/s

但是 HN 评论区挖出了一个你需要注意的关键信息:benchmark 跑在 96 vCPU、384GB RAM 的机器上。 所以 60K/s 是巨型单机压出来的上限。有人直言:「这么大的机器才跑 60K,恰恰说明这个方案扩展能力有限。」

另外还有一些值得关注的边界:

  • 进程 crash 丢通知:30 秒 polling fallback 对某些场景不可接受
  • 单慢 reader 阻塞所有 channel 的写入------全局队列是共享的
  • 8000 bytes payload 上限:不适合传大消息
  • 代码复杂度增加:buffer + timer + polling fallback 的维护成本和加一个 Redis 差不多

有人在 HN 上说:「我在生产里用过,高负载下会丢消息。」还有人说:「看完这个优化代码,第一反应是这能调试吗?」

五、工程选择

场景 推荐度 理由
缓存失效通知 ✅ 推荐 丢了 polling 能补,最自然场景
消息队列/流式 ⚠️ 可用 buffer 方案可上,但要权衡复杂度
实时协作/编辑 ❌ 谨慎 buffer crash 丢通知不可接受
精确一次传递 ❌ 不推荐 上 Kafka / Pulsar

如果你每秒写入低于 2000,naive 实现完全够。到了瓶颈再上 buffer + batch 方案,约 50 行代码。

六、总结

LISTEN/NOTIFY 的「不可扩展」标签不是谣言------全局锁确实有硬限制。但你仔细看下来会发现,它也不是废物。当你的业务天然就是「通知可丢,polling 能补」的场景,用它比多维护一个消息中间件省心。

关键是:你技术决策别只读一个标题就下结论。你真正需要的是读懂瓶颈在哪,对号入座。那 50 行代码写得值不值,取决于你是在给 2000 写入/秒的系统加优化,还是为一个 100 写入/秒的系统提前操心。


原文:www.dbos.dev/blog/postgr...

HN:news.ycombinator.com/item?id=490...

相关推荐
面向Google编程5 小时前
你把 Claude Code 当聊天框,高手却让它"自己查自己":3 个拉开差距的工程习惯
ai编程·claude
lifallen5 小时前
Skill 的生命周期:问题消失以后
人工智能·学习·ai·ai编程
小虎AI生活8 小时前
两天让 20 多位零基础学员各写出一篇真实文章:一套「IP 启蒙课」的完整拆解
ai编程
七牛云行业应用8 小时前
Harness Engineering 是什么:从“写提示词“到“设计 Agent 边界“的工程方法论
人工智能·agent·ai编程
DO_Community11 小时前
AMD旗舰GPU仅需4美元每小时,DigitalOcean上线Spot GPU实例
大数据·人工智能·agent·ai编程
全栈弄潮儿13 小时前
初学者关于 AI 编程最常问的 10 个问题
aigc·openai·ai编程
RAOY的AI笔记13 小时前
Codex注册教程:从账号认证、环境配置到AI编程任务实践
chatgpt·ai编程
骥龙14 小时前
模块五:n8n自动化工作流 + Ollama + Health-MCP
人工智能·ai编程
嘿嘿-6615 小时前
Cherry Studio 接入ChatGpt:GPT-5.6 Sol 文本测试与 GPT-Image-2 出图教程
人工智能·gpt·ai·chatgpt·node.js·ai编程
angered15 小时前
「AI 应用 / AI Agent」行业日报 · 2026-09-04
人工智能·ai编程