Commit 470a44d9 by luoqi

fix(生产): 主进程堆上限 4G→8G —— 每 4 小时崩一次,三天没跑成过一次全量 plans

【症状】容器 exitCode=139 自动重启,08-23~08-26 共 **11 次**,间隔约 4 小时。
  页面无感(自动重启 + health 200),所以一直没人发现。

【根因】每轮增量同步结束会在**主进程内**跑一次 runAllForHost 全量召回。补摄把患者
  灌到 111 万、召回池 54 万之后,selectHits 一次出 **96 万条命中**,连同 prefetch 的
  latest/snoozed/persona 全在堆里 —— 而主进程启动命令
  `node --enable-source-maps dist/main.js` **没带 max-old-space-size**,
  取 Node 默认 **4144 MB**,装不下:
    Mark-Compact 3985.6 → 3953.5 MB (mu = 0.008)   ← GC 已经回收不动
    FATAL ERROR: Reached heap limit — JavaScript heap out of memory

【代价】那三天 `Result(ruier-grp)` 在日志里出现 **0 次** —— 一次完整的全量 plans
  都没跑成过。每轮崩在 selectHits 之后、写库之前:
     增量导入成功落库    persona 重算成功落库    plans 整轮作废
  数据不丢,丢的是"召回池的定时刷新"。另外每次崩溃留一个僵尸 sync 锁(重启后被回收),
  且会带走同容器里正在跑的 docker exec —— bj04 第一次尝试就是这么死的(白跑 1h27m,
  靠 batch.sh 的 3 次重试救回)。

【为什么现在才发现】补摄期间增量被 sync 并发锁挡下(00:15/08:15/10:15 全是 skip),
  不跑全量 plans 就不崩 —— 实测连续 12 小时零崩溃。补摄一停,下一轮就会再崩。

️ 排查时别往"容器内存不够"上找:容器**没有内存上限**(HostConfig.Memory=0),
  宿主 15.36G 一直有 7-11G 空闲。撑爆的是 V8 自己的堆,与宿主余量无关。
️ 同一容器里 CLI 那条路(cold-import / recompute-plans)一直是 8192,
  **两条路配置不一致**,崩的一直是没配的这条。这次补齐。
️ 宿主内存账写进注释了:8G(服务) + 8G(CLI) = 16G > 15.36G。实测两者不会同时顶到
  上限(max-old-space 是天花板不是预留;实测峰值 服务 4G / CLI 4.9G),但
   别手工并发跑全量 `recompute-plans`(它不走 sync 锁)。

️ **这是止血不是根治**:池子还在涨,8G 迟早也会到顶。根治是「增量之后别跑全量」——
  recompute-plans 已经有 --clinics 收窄能力,同样的思路搬到
  SyncIncrementalSchedulerService 那一处即可。另开分支做。

改动只有一行环境变量(docker-compose.prod.yml 的 pac-service),不动代码不动数据。
YAML 已校验;managed overlay 只合并 DATABASE_URL/REDIS_URL(非 !override),不影响本项。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 07e47a65
...@@ -102,6 +102,31 @@ services: ...@@ -102,6 +102,31 @@ services:
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3101 PORT: 3101
# 🔴 **主进程堆上限** —— ⛔ 别删,删了每 ~4 小时崩一次(2026-08-26 生产实证)。
#
# 症状:`FATAL ERROR: Reached heap limit — JavaScript heap out of memory`,
# 容器 exitCode=139 自动重启。08-23~08-26 崩了 **11 次**,期间**一次完整的
# 全量 plans 都没跑成过**(日志里 `Result(ruier-grp)` 出现 0 次)。
# 根因:每轮增量同步结束会在**主进程内**跑一次 `runAllForHost` 全量召回。
# 补摄把患者灌到 111 万、召回池 54 万后,`selectHits` 一次出 96 万条命中,
# 连同 prefetch 的 latest/snoozed/persona 全在堆里 —— 而主进程启动命令
# (`node --enable-source-maps dist/main.js`)**没带 max-old-space-size**,
# 取 Node 默认 **4144 MB**,装不下。
# ⚠️ 容器**没有内存上限**(HostConfig.Memory=0),宿主 15.36G 也一直有 7-11G 空闲 ——
# ⛔ 别往"容器内存不够"上找,撑爆的是 V8 自己的堆,与宿主余量无关。
# ⚠️ 同一容器里 CLI 那条路(cold-import / recompute-plans)一直是 `-max-old-space-size=8192`,
# **两条路配置不一致**,崩的一直是没配的这条。这里补齐。
#
# ⚠️ 宿主内存账:15.36G 总量,本进程 8G 上限 + CLI 8G 上限 = 16G > 总量。
# 实测两者不会同时顶到上限(补摄期间增量被 sync 并发锁挡下,实测 12 小时零崩溃),
# 且 max-old-space 是**天花板不是预留**(实测峰值:服务 4G / CLI 4.9G)。
# ⛔ 但**别手工并发跑** `recompute-plans --host=<全量>`(它也吃 8G)——
# 那条 CLI **不走 sync 锁**,与服务自己那轮全量撞上时才真有可能把宿主吃满。
#
# ⚠️ 这是**止血不是根治**:池子还在涨,8G 迟早也会到顶。
# 根治是「增量之后别跑全量」—— `recompute-plans` 已经有 `--clinics` 收窄能力,
# 同样的思路搬到 SyncIncrementalSchedulerService 那一处即可。
NODE_OPTIONS: --max-old-space-size=8192
# 覆盖 .env 的 localhost URL → 走 docker 内部网络 # 覆盖 .env 的 localhost URL → 走 docker 内部网络
DATABASE_URL: postgresql://${POSTGRES_USER:-pac}:${POSTGRES_PASSWORD:-pac}@postgres:5432/${POSTGRES_DB:-pac}?schema=public DATABASE_URL: postgresql://${POSTGRES_USER:-pac}:${POSTGRES_PASSWORD:-pac}@postgres:5432/${POSTGRES_DB:-pac}?schema=public
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment