Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
cd94422a
Commit
cd94422a
authored
Aug 30, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: 启用子场景并发(生产全量 ×2.31)+ 推翻旧的并发否定结论 → main
parents
6f0915f7
0c391bd1
Pipeline
#3632
failed in 0 seconds
Changes
2
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
53 additions
and
15 deletions
+53
-15
apps/pac-service/.env.example
+15
-0
apps/pac-service/src/modules/plan/engine/scenarios/treatment-initiation-recall.scenario.ts
+38
-15
No files found.
apps/pac-service/.env.example
View file @
cd94422a
...
...
@@ -205,3 +205,18 @@ SENTRY_ENVIRONMENT=
SENTRY_TRACES_SAMPLE_RATE=0.1
# release(可选):部署时注入 git SHA
SENTRY_RELEASE=
# ── plan 批量重算性能 ────────────────────────────────────────────────
# 召回子场景并发度。**生产设 4**(2026-08-30 起)。
# 生产全量实测(113 万患者,同一台 RDS,相邻两轮):
# =1 场景段 2h00m / 整轮 2h24m
# =4 场景段 51m56s / 整轮 1h20m → 场景段 ×2.31,省 64 分钟
# 瓶颈是**延迟**(逐次索引探查在等 page 返回)不是磁盘吞吐,所以并发 2 就超线性(×1.50)。
# ⚠️ 判据只能看 `[plan] 阶段耗时` 的**墙钟** —— 并发下单条查询的 sql= 会被争抢拉长,
# 看单条会误判成"变慢了"。详见 treatment-initiation-recall.scenario.ts 里 conc 处注释。
PAC_RECALL_SUBSCENARIO_CONCURRENCY=4
# gap 计算形态:legacy(默认,逐行相关子查询)/ setbased(集合式)。
# ⚠️ 生产**未启用**。集合式已在测试机验证零差异且再省 22~31%,但未经生产验证。
# 见 docs/design/gap-set-based-rewrite-plan.md。
PAC_GAP_VARIANT=
apps/pac-service/src/modules/plan/engine/scenarios/treatment-initiation-recall.scenario.ts
View file @
cd94422a
...
...
@@ -221,13 +221,28 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// 合并/去重(下游 hitsByPatient Map + max 分 + Set 比对)与顺序无关 → 并行 = 结果逐字节相同。
// PAC_RECALL_SUBSCENARIO_CONCURRENCY=N(>1)开并行(分块 Promise.all);默认串行,行为不变。
//
// ⛔ **别指望靠这个旋钮提速** —— 2026-08-29 测试服实测(585K 患者 / 87,661 命中,空闲机):
// 串行(默认 1): 24.6 / 21.7 / 23.5 分钟(三次)
// 并发 3: 24.1 分钟 ← 不但没快,还略慢
// 原因:本阶段是**共享磁盘 I/O 受限**,不是查询延迟受限。采样显示全程 DataFileRead,
// 并行只是让几条查询抢同一批 page,总读取量一个字节都没少。
// 要提速得**减少读取量**(索引 / 收窄扫描范围),不是提高并行度。
// (单条最慢的子场景查询 6.7 分钟,其余 12~35 秒 —— 并行后墙钟被最慢那条兜住。)
// ⭐ **这是目前实测最有效的一根杠杆:生产全量 ×2.31。** 生产已设 =4。
//
// 2026-08-30 生产全量实测(113 万患者 / 54.8 万命中,同一台 RDS,相邻两轮):
// 串行(=1): 场景段 7,200,096ms(2h00m) 整轮 8,654,105ms(2h24m)
// 并发(=4): 场景段 3,115,928ms(51m56s) 整轮 4,806,869ms(1h20m)
// → 场景段 ×2.31,整轮 ×1.80,省 64 分钟。**2 小时窗口由此进得去。**
// 先在生产一个 39,148 患者的诊所上交替标定过(串行 57.6/56.9s → 并发4 26.1/26.2/27.9s,
// ×2.18),再由全量复现 ×2.31 —— 子集外推这次成立。
//
// 🔴 **本段注释此前写反了,导致生产一年多没开这个旋钮**,原文:
// 「⛔ 别指望靠这个旋钮提速 —— 并发3=24.1分 vs 串行23.3分,不但没快还略慢。
// 原因:本阶段是共享磁盘 I/O 受限…并行只是抢同一批 page,总读取量一个字节都没少。」
// 两处都错:
// ① **3% 的差距落在噪音里**。2026-08-30 用「两边跑同一条 SQL」的对照组量过,
// 该测试机的环境噪音是 ±25% —— 那次比较什么也没证明,却被当成定论。
// ② **机制归因也错**:真瓶颈是**延迟**(逐次索引探查在等 page 返回,CPU 和磁盘都闲着),
// 不是吞吐。所以并发 2 就能拿到超线性(生产 ×1.50,测试机 ×2.88)。
// 若真是吞吐受限,墙钟应约等于各查询耗时之和;实测墙钟只有求和的 43%。
//
// ⚠️ **读数注意**:并发下**单条**查询的 sql= 会被争抢拉长(生产 caries 23.8→27.7 分,
// perio 12.6→16.9 分),看单条会误以为变慢了。**判据只能是阶段墙钟**,
// 不能看各条耗时之和 —— 上面那次误判有一半就栽在这。
const
conc
=
Math
.
max
(
1
,
Number
(
process
.
env
.
PAC_RECALL_SUBSCENARIO_CONCURRENCY
)
||
1
);
// ⚠️ 不能 hits.push(...subHits):spread 把每个元素当实参压栈,V8 实参上限 ~6.5万;
// host 患者到 ~28 万后单子场景命中可超限 → RangeError: Maximum call stack size exceeded
...
...
@@ -485,19 +500,27 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// → **68% 的时间花在「每候选行跑一次」的 gap 相关子查询上**
// 5.8 万个 (患者×信号) 候选,每行跑两个子查询。
//
// ⛔ 已实测**否定**的两个方案,别再试:
// · 子场景并发(PAC_RECALL_SUBSCENARIO_CONCURRENCY=3):24.1 分钟 vs 串行 23.3 —— 无效。
// 瓶颈是共享磁盘 I/O(全程 DataFileRead),并行只是抢同一批 page,总读取量不变。
// ⛔ 已实测否定的方案(别再试)/ 已被推翻的旧结论:
// · ~~子场景并发~~ —— ⚠️ **这条不是否定项,是被推翻的错误结论,见上面 conc 处的注释**。
// 原记「并发3=24.1分 vs 串行23.3 —— 无效,瓶颈是共享磁盘 I/O」;
// 3% 落在 ±25% 的环境噪音里,不成立。2026-08-30 生产全量实测 **×2.31**,已启用。
// · 给信号码建部分索引(((content->>'code')) WHERE type IN (...) AND status='active'):
// EXPLAIN 估算成本 1,837,125 → 139,816(13×),信号扫描 122 万行 → 5,593 行,
// **但墙钟 24.5 分钟 vs 23.3 —— 无效**。教训:别拿 EXPLAIN 的估算成本当依据,
// 成本模型改善不等于实际耗时改善;要 EXPLAIN (ANALYZE) 看真实的 actual time。
//
// ✅ 真正能解决的方向(尚未做,是个独立项目):
// 把 buildGapCore 的 resolvedTeethSql 从**逐行相关子查询**改成**集合式**
// (一次算出全部患者的 resolved 牙位再做连接)。理论上能降一到两个数量级。
// ⚠️ 但它是召回与画像共用的单一真理源,重写必须配等价性验证
// (改写前后逐患者逐牙位比对),否则就是拿静默少召赌运气。
// ✅ 已解决:**开子场景并发**(生产 =4)。生产全量 场景段 2h00m → 51m56s(×2.31),
// 整轮 2h24m → 1h20m,2 小时窗口进得去。代价是一行环境变量。
// 上面 ③ 说的「68% 花在逐行 gap 子查询上」仍然成立 —— 只是那部分现在被并行摊掉了,
// 不再是墙钟瓶颈。
//
// 🔭 仍然可做、但已非必需:把 resolvedTeethSql 改成**集合式**
// (一次算出全部患者的 resolved 牙位再做连接)。
// 2026-08-30 已在 test 分支实现并验证:测试机 585K 上 11 个子场景逐
// (患者×信号×牙位) **零差异**,场景段再省 22~31%。但它是召回与画像共用的
// 单一真理源,1600 行改动 + 一套对拍工具,且**尚未在生产验过**。
// 并发已把窗口解决掉之后,性价比要重新权衡 —— 见
// docs/design/gap-set-based-rewrite-plan.md。
// ══════════════════════════════════════════════════════════════════
// ⚡ 调试开关:PAC_RECALL_DUMP_SQL=1 时把本子场景的完整 SQL 打出来。
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment