性能优化
在 Postgres 14+ 上需要零死元组膨胀、原生…
在 Postgres 14+ 上安装并运行 PgQue,用 tick 快照批处理和 TRUNCATE 轮换实现零膨胀、支持多消费者 fan-out 的队列。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 Postgres 14+ 上需要零死特定
解决思路
- 确认环境:Postgres 14+;推荐 pg_cron,或用外部调度器每秒调用 pgque.ticker()。
- 从 SQL 文件安装:git clone https://github.com/NikolayS/pgque.git;cd pgque;PAGER=cat psql --no-psqlrc --single-transaction -d mydb -f sql/pgque.sql。
- 或在 psql 会话中安装:begin; \i sql/pgque.sql; commit;
- 用 pg_cron 启动 ticker:select pgque.start(); 它会创建 ticker 每 1 秒和 maintenance 每 30 秒的 pg_cron 作业。
- 无 pg_cron 时启动 ticker:外部每 1 秒运行 psql -d mydb -c "select pgque.ticker()";每 30 秒运行 psql -d mydb -c "select pgque.maint()"。
- 注意:没有运行 ticker,消费者看不到任何东西;入队有效,但不会创建批次。
- 卸载:\i sql/pgque_uninstall.sql。
- 角色分工:pgque_reader 用于仪表盘/指标/只读;pgque_writer 用于生产者和消费者(大多数应用);pgque_admin 用于运维/迁移。
- 授予生产者/消费者访问:CREATE USER app_worker WITH PASSWORD '...'; GRANT pgque_writer TO app_worker;
- 授予只读指标访问:CREATE USER metrics_reader WITH PASSWORD '...'; GRANT pgque_reader TO metrics_reader;
- 创建队列:SELECT pgque.create_queue('orders');
- 订阅消费者:SELECT pgque.subscribe('orders', 'order-processor');
- 发送单个事件:SELECT pgque.send('orders', 'new_order', '{"order_id": 42, "amount": 99.99}');
- 发送批量事件:SELECT pgque.send_batch('orders', ARRAY[ROW('new_order', '{"order_id": 43}')::pgque.event_data, ROW('new_order', '{"order_id": 44}')::pgque.event_data]);
- 接收下一批:SELECT * FROM pgque.receive('orders', 'order-processor');
- 确认成功处理:SELECT pgque.ack('orders', 'order-processor', :batch_id);
- 否定确认(重试/死信):SELECT pgque.nack('orders', 'order-processor', :batch_id);
- 取消订阅:SELECT pgque.unsubscribe('orders', 'order-processor');
- 底层 API 入队单事件:SELECT pgque.insert_event('orders', 'new_order', '{"order_id": 42}');
- 底层 API 注册消费者:SELECT pgque.register_consumer('orders', 'order-processor');
- 底层 API 获取下一个可用批次 ID:SELECT pgque.next_batch('orders', 'order-processor'); 返回 batch_id (bigint) 或 NULL。
- 底层 API 获取批次事件:SELECT * FROM pgque.get_batch_events(:batch_id); 返回 ev_id, ev_time, ev_txid, ev_retry, ev_type, ev_data, ev_extra1..4。
- 底层 API 标记批次成功:SELECT pgque.finish_batch(:batch_id);
- 底层 API 安排事件重试(延迟秒):SELECT pgque.event_retry(:batch_id, :ev_id, 60);
- 底层 API 注销消费者:SELECT pgque.unregister_consumer('orders', 'order-processor');
- 生产者 Python psycopg2 示例:连接 DATABASE_URL,autocommit=False;定义 enqueue_order(order_id, amount),用 cur.execute("SELECT pgque.send(%s, %s, %s)", ("orders", "new_order", json.dumps({"order_id": order_id, "amount": amount}))),然后 conn.commit()。
- 消费者 Python psycopg2 示例:连接 DATABASE_URL,autocommit=False;setup 调用 SELECT pgque.subscribe(%s, %s);process_batch 用 RealDictCursor 执行 SELECT * FROM pgque.receive(%s, %s);无行则 conn.rollback() 返回 False;取 rows[0]["batch_id"],遍历 rows 解析 json.loads(row["ev_data"]) 处理;然后执行 SELECT pgque.ack(%s, %s, %s) 并 conn.commit();主循环调用 process_batch,无批次时 time.sleep(1) 等待下一个 tick。
- Fan-Out 示例:创建队列 SELECT pgque.create_queue('user-events'); 订阅多个独立消费者:SELECT pgque.subscribe('user-events', 'analytics-service'); SELECT pgque.subscribe('user-events', 'notification-service'); SELECT pgque.subscribe('user-events', 'audit-log'); 生产者发送一次:SELECT pgque.send('user-events', 'user_signup', '{"user_id": 1}'); 每个消费者独立接收相同事件:SELECT * FROM pgque.receive('user-events', 'analytics-service'); SELECT * FROM pgque.receive('user-events', 'notification-service'); SELECT * FROM pgque.receive('user-events', 'audit-log');
- 重试/死信模式:用 DO $$ 块声明 v_batch_id, v_ev;SELECT pgque.next_batch('orders', 'order-processor') INTO v_batch_id;若 NULL 则 RAISE NOTICE 并 RETURN;FOR v_ev IN SELECT * FROM pgque.get_batch_events(v_batch_id) LOOP;尝试处理,EXCEPTION WHEN OTHERS THEN PERFORM pgque.event_retry(v_batch_id, v_ev.ev_id, 60);最后 PERFORM pgque.finish_batch(v_batch_id)。
- 监控队列信息:SELECT * FROM pgque.get_queue_info(); SELECT * FROM pgque.get_queue_info('orders');
- 监控消费者滞后和位置:SELECT * FROM pgque.get_consumer_info(); SELECT * FROM pgque.get_consumer_info('orders'); SELECT * FROM pgque.get_consumer_info('orders', 'order-processor');
- 监控批次详情:SELECT * FROM pgque.get_batch_info(:batch_id);
- 查询版本:SELECT pgque.version();
- 修改 tick 频率:默认每 1 秒通过 pg_cron;执行 SELECT cron.alter_job(job_id := (SELECT jobid FROM cron.job WHERE command LIKE '%pgque.ticker%'), schedule := '* * * * *');
- 强制立即 Tick(测试/演示):SELECT pgque.force_tick('orders'); 或 SELECT pgque.ticker();
- pg_cron 日志卫生:ALTER SYSTEM SET cron.log_run = off; SELECT pg_reload_conf(); 或定期清理:SELECT cron.schedule('pgque-cron-purge', '0 * * * *', $$DELETE FROM cron.job_run_details WHERE end_time < now() - interval '1 hour'$$);
- pg_cron 在不同数据库:若 pg_cron 在 postgres DB 而 PgQue 在 mydb,在 pg_cron 数据库运行:SELECT cron.schedule_in_database('pgque-ticker', '* * * * *', 'SELECT pgque.ticker()', 'mydb'); SELECT cron.schedule_in_database('pgque-maint', '* * * * *', 'SELECT pgque.maint()', 'mydb');
- 事务性入队:BEGIN; INSERT INTO orders (id, amount) VALUES (42, 99.99); SELECT pgque.send('orders', 'new_order', '{"order_id": 42}'); COMMIT;
- 扩容前检查队列深度:SELECT queue_name, ev_per_sec, consumer_count, pending_events FROM pgque.get_queue_info() WHERE pending_events > 1000;
- 列出所有消费者及其滞后:SELECT queue_name, consumer_name, pending_events AS lag, last_seen FROM pgque.get_consumer_info() ORDER BY lag DESC;
- 消费者收不到任何东西时排查:原因 ticker 未运行;检查 SELECT * FROM pgque.get_queue_info('orders'); 看 last_tick;手动触发 SELECT pgque.ticker(); 检查 pg_cron 作业 SELECT * FROM cron.job WHERE command LIKE '%pgque%';
- 发送后事件不出现时排查:确认 ticker 运行并强制 SELECT pgque.ticker(); 检查队列存在 SELECT * FROM pgque.get_queue_info('orders'); 检查消费者已注册 SELECT * FROM pgque.get_consumer_info('orders', 'my-consumer');
- 性能/VACUUM 压力:PgQue 事件路径设计上免疫死元组膨胀;若看到 VACUUM 活动,是应用表而非 PgQue 队列表。
- 重试事件不再出现时排查:维护作业处理重试调度;确保每 ~30 秒运行 pgque.maint();执行 SELECT pgque.maint(); 检查重试状态事件 SELECT * FROM pgque.get_queue_info('orders');
- 升级/重装:升级路径仍在稳定中;安全重装:psql -d mydb -c "\i sql/pgque_uninstall.sql"; psql -d mydb --single-transaction -f sql/pgque.sql; psql -d mydb -c "select pgque.start()";
适用边界 · 注意事项
- 需要亚秒级或即时投递:端到端延迟约 1–2 秒(一个 tick 间隔 + 轮询)。
- 无法运行周期性 ticker(pg_cron 或外部调度器):没有 ticker 消费者看不到任何东西。
- 必须使用 SKIP LOCKED 行级锁语义或要求 C 扩展的场景:PgQue 不使用 skip locked,且是纯 PL/pgSQL。