2026-09-04 生产 OOM 宕机 2.5 小时。内核日志:
Killed process (node) anon-rss:6,321,668kB(6.03GB) total-vm:16.7GB
机器 14GB。进程被杀后整机 swap 抖死,sshd 与 Web 全部无响应,靠人工重启才能恢复。
docker-compose.prod.yml 那段注释 2026-08-26 就写过「这是止血不是根治:池子还在涨,
8G 迟早也会到顶」—— 那天到了。
## 病根不是"没分块",是"不释放"
取数早就是 CHUNK=2000 一块(prefetchForBatch 内),但**产物累积**在跨全量存活的 Map 里;
runPool 的闭包又把 latest/persona/visit/logRows 全部 context-allocate,
V8 要到 runAllForHost 整个 frame 结束才可能回收 → 第 3 步扫全池时它们全还在,那一下就是峰值。
堆账(550,049 命中患者 / 约 96 万条 hit,按 V8 对象布局建模):
latestByPatient 1.93GB + hitsByPatient 1.06GB + persona/visit 0.14GB
+ activePlans 0.09GB + logRows 0.07GB ≈ 活跃保留 3.3GB,实测 RSS 6.03GB(≈1.8×)
## 改动
· prefetchForBatch 拆成两半:
- prefetchSnoozeAnchors(scope, now) —— **整轮一次**。它的代价跟"终态且冷静期未到期的
计划数"走、与患者数无关(生产实测全库 106 行)。拆出来是让这条纪律在类型上也成立:
它拿不到 patientIds,想塞进批循环也塞不了。
- prefetchOneBatch(scope, ids) —— 每批一次,删掉内部的 CHUNK 循环,切批权交给调用方。
· 写入段改成端到端批循环:每批 预取 → runPool 写库 → createMany 日志 → 释放。
hit 的所有权交给本批局部数组后立刻 hitsByPatient.delete(pid),
批末连同 latest/persona/visit/logRows 一起出作用域回收。
· 新增 resolvePlanBatchSize():默认 2000,夹在 [1, 20000](上限守 PG 32767 bind ——
latest/persona 走 patientId in ids),**显式传 0 = single-shot 回退开关**
(照 cold-import 的 resolveCohortBatchSize 同一形状)。
· 每 5 批 + 末批打一行 rss= / heapUsed= / heapTotal=(rss 沿用 cold-import 字样便于并排 grep;
判 8G 堆余量只能看 heapUsed,rss 含 Prisma Rust 引擎与碎片)。
## 唯一的硬全局依赖,以及它的坑
第 3 步 stale-close 拿本轮命中集对整个召回池取补集(`!hitsByPatient.has(pid)` → supersede)。
分批后 hitsByPatient 被逐批清空 ⇒ 改用跨批累加的 `hitPatientIds` Set(只存 id,约 54MB)。
⛔ 第 3 步**必须等所有批跑完再跑一次**。搬进循环里 = 每批把其他批的患者判成"信号消失"
→ 整池 supersede + 每条认领单一条 auto_release,**而且不报错**,
那行「本轮关闭 N 条无信号 plan」的日志反而会解释成「这不是 bug」。
⛔ 循环体里**不加 try/catch**:今天的语义是"selectHits 抛错发生在任何写之前 → 整轮中止、
第 3 步不跑";分批后前 N 批已落库,吞错"把剩下的批跑完"会让第 3 步拿着残缺命中集去关池子。
## 测试
新增 describe.each 覆盖 PAC_PLAN_BATCH_SIZE ∈ {0,1,2,3,2000},同一 fixture 各跑一遍,
断言引擎返回八个字段 + plan 终态 + 日志行**逐字段一致**,直接编码「结果与批大小无关」。
**为什么必须新加**:既有 20+ 条用例每个只有 1~4 个患者、默认批 2000,永远只有一批 ——
分批写错它们全绿。batch=1 是最凶的一档,同时压测跨批不误关 / touchedPlanIds 按批等价 /
snooze 外提后仍被每批查到。fixture 里特意放了一个本轮 0 命中的 p9 压 stale-close。
有牙验证:把第 3 步判据改回 hitsByPatient.has(分批后已清空)→ plansClosed 0→3、
active 与 assigned 双双变 superseded,多条用例立刻红。
## 预计效果与未做的部分
峰值从"latest+hits+logRows 同时在堆"的堆叠拆成单批驻留,预计 3.3GB → 约 2.1GB。
⚠ ️ 这一级**不碰任何 SQL、不碰 selectHits**,08-30 那根 ×2.31 的并发杠杆与 work_mem 收益
一个字节不动。但 hitsByPatient 仍在第 1 步满载(1.06GB),峰值仍与命中总数线性相关。
要真正解耦需要二级(患者域分区 selectHits),那要改 SQL 且必须先在生产标定
11×P 次子场景查询的墙钟 —— 生产当前宕机,标不了,另开。
⚠ ️ 第 3 步的 activePlans findMany 仍是无分页全池扫(约 94MB,占比 3%)。改键集分页有真实风险:
plan-engine-stale-close.spec 的 findMany mock 忽略 take/orderBy/id.gt,分页写错在 CI 里全绿。
单独一件事做,别和本次混。
⚠ ️ PAC_PLAN_BATCH_CONCURRENCY 从来没被 withCohortDerivedPool 算进连接池(2026-09-02 已因
漏算一次导致健康日报连续三天超时)。本次不改,记在这。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| dto | Loading commit data... | |
| engine | Loading commit data... | |
| recall-debug | Loading commit data... | |
| agent-roster.service.ts | Loading commit data... | |
| assignment-facts.ts | Loading commit data... | |
| assignment-proposal.service.ts | Loading commit data... | |
| assignment-signals.ts | Loading commit data... | |
| assignment.controller.ts | Loading commit data... | |
| claim-guard.ts | Loading commit data... | |
| cohort-attributes.service.ts | Loading commit data... | |
| cohort-filter.ts | Loading commit data... | |
| dispatch-guard.ts | Loading commit data... | |
| execution-callback.service.ts | Loading commit data... | |
| execution.service.ts | Loading commit data... | |
| plan-assignment.service.ts | Loading commit data... | |
| plan-event.recorder.ts | Loading commit data... | |
| plan-label.service.ts | Loading commit data... | |
| plan-label.sql.ts | Loading commit data... | |
| plan-rearrange.service.ts | Loading commit data... | |
| plan.controller.ts | Loading commit data... | |
| plan.module.ts | Loading commit data... | |
| plan.service.ts | Loading commit data... | |
| rearrange-facts.ts | Loading commit data... | |
| rearrange.controller.ts | Loading commit data... | |
| reason-temperature.sql.ts | Loading commit data... | |
| recall-suppression.ts | Loading commit data... | |
| recycle-scheduler.service.ts | Loading commit data... |