-
perf(persona): 重算吞吐四处优化 —— worker pool + 连接池联动 + 少两次往返 · e5122154
生产/测试服实测基线(测试服 12 万患者样本,concurrency=8): 单患者 p50 139ms / p90 230ms / p99 386ms / max 75,750ms,均值 165ms 理论上限 8÷0.165 = 2,909 人/分,实际 ~1,600 → **效率只有 55%** ## ① 批次栅栏 → worker pool(src/common/run-pool.ts) 原写法 `for (i += N) await Promise.all(slice)` 是**栅栏**:每批都等本批最慢那个, 快的 worker 干等。缺口正好是 E[N 次抽样最大值](≈p90)与均值之比。 极端例子:那个 75.7 秒的患者把同批 7 个 worker 一起冻了 75 秒。 改成 N 个 worker 各自从共享游标取下一个,谁空谁取 —— 慢任务只拖住自己那条线。 ## ② --concurrency 根本没放大连接池(真 bug) `withCohortDerivedPool` 只认 `PAC_COHORT_CONCURRENCY`(**摄入**的旋钮), 于是 `recompute-persona --concurrency=N` 的旋钮**静默失效**:实测跑 --concurrency=8 时 进程恰好只有 9 条连接(= 4核×2+1 的 Prisma 默认),超过 9 的 worker 全卡在池上排队。
⚠ ️ 这个 bug **只在小核机器上咬人**,所以一直没被发现:服务器 4 核 → 默认池 9; 开发机 16 核 → 默认池 33,本地压根复现不出来。 改:池改读通用的 `PAC_DB_CONCURRENCY`,由各 CLI 在 NestFactory **之前**按自己的 --concurrency 设进来(PrismaService 在容器初始化时就定池,晚了没用); `PAC_COHORT_CONCURRENCY` 保留为向后兼容别名,两者取大。 ## ③ 同一张表查两遍 → 一次取,内存分两份 persona.service 对 patient_facts 每患者查两次(同患者、同索引、两次往返): ① status IN (active, fulfilled) → factsByType ② type=appointment_record AND status != superseded → appointmentsAll ② 是 ① 在 appointment 这类上的放宽,故取 `status != 'superseded'` 一把捞,内存切两份, 语义逐字节等价。 ## ⑤ 审计日志 2 写 → 1 写 原 create(status='running') + update(终态)。startedAt 可显式赋值,故耗时统计一字不差。 代价:进程被硬杀(OOM/SIGKILL)时不留痕 —— 评估可接受:try/catch 里的异常仍写 'failed' 行; 唯一丢的是"算到一半进程没了",而那种情况原本留下的是一条**永远卡在 running** 的行(没人清理)。 读侧确认无依赖(daily-health-report 只用 findFirst 最近一条 + 按 status 的 groupBy)。 全量回填还顺带少一半日志表写入(35.6 万 → 17.8 万行/轮)。 往返预算:12.7 → 10.7(-16%)。 ## 本地实测(13,268 患者,--force,concurrency=8,同为「全 unchanged」稳态) 旧代码 93.50s 优化后 76.89s → 1.22×,省 17.8%⚠ ️ 本地测不出 ① 的真实收益:本机 avg 35.9ms / p90 49ms / **max 436ms**, 而测试服 avg 165ms / p90 230ms / **max 75,750ms**(174× 均值)。本地没有那种能冻住 整批的长尾,且延迟低到瓶颈更像是 JS 单线程 CPU 而非 DB 往返。 所以 1.22× 主要是 ③⑤ 的往返削减(机器无关),① 的收益要到服务器上才显现 —— **具体多少不预估,等在测试服实跑对照。** ## 正确性 指纹法:优化后跑完 → 切回旧代码再跑 → 旧代码对全部 13,268 人判 `unchanged`, persona_features 指纹 4de1dcd0ddc6a2c99e222fd43e0755de 不变。 即**旧算法认为新代码的产出与它自己会算出来的逐字节相同**。 ## 测试 494 单测通过(36 suites),service tsc 干净。新增 tests/persona-recompute-perf.spec.ts 12 例: runPool — 不丢项 / 并发上限严格且用满 /⭐ 慢任务不冻住其他 worker / 边界 / 错误上抛 连接池 —⭐ PAC_DB_CONCURRENCY 生效 / COHORT 向后兼容 / 取大 / 封顶 40 / =1 不动 URL / 已有 connection_limit 不覆盖 / 非标准 URL 不抛 ## 未做(单独排) ④ gapSelector 每患者平均 2.66 条 SQL(实测分布 0 组 3.4% / 1 组 19.5% / 2 组 25.7% / 3 组 24.7% / 4+ 26.7%,max 9)。UNION ALL 合一能再省 ~1.7 次往返,但动的是**召回共享** 的 gap 真理源,风险最高,要配专门的对账测试。 另注:plan-engine.runAllForHost 有一模一样的批次栅栏写法(PAC_PLAN_BATCH_CONCURRENCY), 同样的 5 行改法适用,本次未动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| data | Loading commit data... | |
| prisma | Loading commit data... | |
| sql | Loading commit data... | |
| src | Loading commit data... | |
| tests | Loading commit data... | |
| .dockerignore | Loading commit data... | |
| .env.example | Loading commit data... | |
| .gitignore | Loading commit data... | |
| .swcrc | Loading commit data... | |
| Dockerfile | Loading commit data... | |
| jest.config.cjs | Loading commit data... | |
| nest-cli.json | Loading commit data... | |
| package.json | Loading commit data... | |
| tsconfig.build.json | Loading commit data... | |
| tsconfig.json | Loading commit data... |