perf(db): 干掉列表/计数热路径上的「整表拼单聚合」,并给数据库留出 1 核

生产实测(2026-10-03,order_site):postgres 在 22.6 小时里烧了 12.2 CPU·小时
(均 0.54 核),却因 2 核配额累计被 cgroup 节流 9,337 秒;top 12 条语句占全部
DB CPU 的 69.4%,其中三条最热的就是「工单列表 / 计数 / 大厅候选集」。

一、查询形状(根因,不是缺索引)
接单大厅那条 COUNT 被调 71,539 次、均 103ms,EXPLAIN(ANALYZE,BUFFERS) 显示
82.8ms 里 35ms 花在「把整张 work_order_shares 聚合一遍再 join」:
Index Only Scan rows=22,987 → GroupAggregate → 最终只用到 11 行;同一条 SQL 里
还有第二遍同样的全局聚合(filter_share_summary)。已改成按工单限定的关联子查询
(走 idx_work_order_shares_work_order):
- buildWorkOrderWhere 的 excludeFilledSharing 过滤条件;
- buildHallCandidateIdsSql 的 candidate_shares。
同参数实测:50.3ms → 2.1ms、35.9ms → 3.8ms、28.4ms → 1.8ms,buffer 10,145 → 187,
三种参数下结果逐行一致(11/11、5/5、11/11)。
有意保留 operational_share_summary 那一处全局聚合:全表统计(103,073 行)时
关联子查询要 103,073 次探测(330ms),一次聚合只要 55ms;已有用例钉住「只允许剩一处」。

二、容器与数据库参数
- POSTGRES_CPUS 2 → 3(4 核机器,同机其它容器实测只用 0.2-0.4 核且几乎不节流);
- max_parallel_workers_per_gather 2 → 1(>1 时一条查询要 leader+2 worker=3 核,必撞配额);
- checkpoint_timeout 300 → 900s、max_wal_size 1GB → 4GB(43 天累计 21,778 次检查点,
  sar 里就是每 20-40 分钟一轮写突发)。

三、迁移 092:热表 autovacuum 调参
可见性图陈旧导致 index-only scan 回表 5,580 次;work_orders/order_items/orders/
kuaishou_industry_vouchers/fulfillment_tasks 死元组 1.9 万-5.3 万、上次清理在前一天
甚至 9/29。把这些表的 scale_factor 0.2→0.05、analyze 0.1→0.02、cost_limit 200→800,
把「攒批猛扫」拆成「小步勤清」,并打开 5s 以上的 autovacuum 日志便于观测。

用例:新增 share-summary-scope.test.ts(钉住热路径不再出现整表聚合、只保留一处且有注释说明),
env-documentation 白名单补 3 个新增部署变量。全量 599 通过,typecheck/lint/check:sql 通过。
This commit is contained in:
yml2213
2026-10-03 17:21:21 +08:00
parent ccbcb7cbae
commit 905236d83a
6 changed files with 204 additions and 21 deletions
@@ -0,0 +1,62 @@
-- 092_hot_table_autovacuum_tuning.sql —— 让热表的 autovacuum 更勤、更快。
--
-- 背景(2026-10-03 生产实测,库 order_site @47.114.114.225,4 核机器):
--
-- 1) 可见性图陈旧,index-only scan 退化成回表:
-- 接单大厅那条 COUNT 的 EXPLAIN 里 `Index Only Scan on work_order_shares`
-- 出现 **Heap Fetches: 5580**(22,987 行里 1/4 要回堆),10,145 个 buffer 命中
-- 换回 1 行 COUNT 结果。heap fetch 是随机页访问,比顺序扫还贵。
--
-- 2) 默认 autovacuum_vacuum_scale_factor = 0.2,等于「死元组攒到 20% 才清」:
-- work_orders 死元组 18,798(15.4%,最后一次 autovacuum 是前一天 15:42)、
-- order_items 52,803(15.8%,上次 9/29)、orders 19,610(上次 9/30)、
-- kuaishou_industry_vouchers 38,325(10.9%,1,007,929 次更新)。
-- 攒一大批再扫一次 → 单次 vacuum 又长又重,正好和「CPU 忽高忽低」的
-- 周期性尖峰、以及 sar 里每 20–40 分钟一轮的读突发(rtps 95/s、6MB/s)对上。
--
-- 3) 这几张表更新极频繁(work_orders 649,751 次更新、fulfillment_tasks 2,314,711 次),
-- 是典型的「小步勤清」比「攒批猛扫」更划算的场景。
--
-- 因此对最热的 5 张表单独调参(只改表级 storage parameter,不动全局默认):
-- autovacuum_vacuum_scale_factor 0.2 → 0.05 触发阈值降到 5% 死元组
-- autovacuum_analyze_scale_factor 0.1 → 0.02 统计信息更及时
-- autovacuum_vacuum_cost_limit 200 → 800 单次 vacuum 更快跑完(IO 更碎而不是更长)
--
-- 安全性:这些只是把「一次大扫」拆成「多次小扫」,单位时间总 IO 相近;
-- 机器白天有 60–70% 空闲(sar: idle 62.9%),碎片化清理比尖峰更可控。
-- 回滚:ALTER TABLE ... RESET (autovacuum_*)。
ALTER TABLE work_orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 800
);
ALTER TABLE order_items SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 800
);
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 800
);
ALTER TABLE kuaishou_industry_vouchers SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 800
);
ALTER TABLE fulfillment_tasks SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 800
);
-- 打开 autovacuum 观测(原先 log_autovacuum_min_duration = 600000,
-- 只有超过 10 分钟的 vacuum 才会留日志,等于什么都看不到)。
-- 这里改成表级不可设,只能改库级:记录超过 5 秒的自动清理,便于事后核对清理节奏。
ALTER DATABASE order_site SET log_autovacuum_min_duration = 5000;