ChildProcessManager::setupEventHandlers | Child process stderr: 7: 0x1112e9c v8::internal::Heap::Co
RCA: ChildProcessManager child process stderr — V8 heap OOM crash
Overview#
What Happened#
2026-07-21 21:00:39 KST 에 cupixworks-any-bimrevision-agent 의 BIM 비교 child process 가 V8 heap OOM 으로 crash 했다. Node 가 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 를 stderr 로 출력하고 native stack trace 를 뱉으며 SIGABRT 로 종료했다. ChildProcessManager::setupEventHandlers 의 stderr handler 가 각 stack frame 라인을 logger.error 로 개별 기록해, 하나의 crash 가 error 레벨 로그 다수와 25개 fingerprint 클러스터로 확산되었다. 대표 클러스터 는 V8 heap CollectGarbage 프레임 라인 (3b141d70-...) 이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | (child process fatal — no JS exception; V8 FATAL ERROR) |
| exception.message | FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory |
| top_frame | packages/base/src/manager/child-process.manager.ts:138-141 (stderr handler in parent) |
| runtime | Node.js (V8 heap ceiling ≈ 8 GB, observed Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
clark-vdc (cupixworks-any-bimrevision-agent) |
25 clusters (single crash event) | 해당 BIM revision 작업 1 건 BimComparisonState=Error 로 마감. child process SIGABRT 로 즉시 종료. |
Timeline#
- 2026-07-21 21:00:39.174 KST — child process 가
<--- Last few GCs --->를 출력. 직전 Mark-Compact 후7909.6 -> 7909.6 MB(거의 회수 안 됨,average mu = 0.091). - 2026-07-21 21:00:39.180 KST — 두 번째 Mark-Compact
7916.1 -> 7916.1 MB(current mu = 0.006) — GC 가 무력화됨. - 2026-07-21 21:00:39.181 KST —
FATAL ERROR: ... JavaScript heap out of memory+----- Native stack trace -----. - 2026-07-21 21:00:39.181–.196 KST — V8 native frame 18줄 (
OOMErrorHandler→CollectGarbage→NewFixedArrayWithFiller→Runtime_NewArray) 이 각각 별도error로그로 기록됨 (cluster 3b141d70 의 대표는 프레임 #7). - 2026-07-21 21:01:45.908 KST — parent 가
Child process exited감지. - 2026-07-21 21:01:45.910 KST —
Process killed by signal SIGABRT (code: null). - 2026-07-21 21:01:45.911 KST —
BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)". 상태Error로 저장 후 종료. - status-board — 25 클러스터로 폭발했지만 후속 재발 없음.
2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1인시던트가 12:01:45.911Z 에 resolved.
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: 7: 0x1112e9c v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: clark-vdc
- 발생 횟수: 1 (fingerprint 기준, 실제로는 동일 crash 가 25개 fingerprint 로 분산)
- 최초 발생: 2026-07-21 21:00:39 KST
- 최근 발생: 2026-07-21 21:00:39 KST
동일 crash 이벤트가 stderr 한 줄씩 logger.error 로 흘러가며 25개 별도 클러스터 (79974cd5..339e3092) 로 파편화됐다. status-board 는 이를 하나의 svc:cupixworks-any-bimrevision-agent::unknown 인시던트로 묶었고, 66 초 뒤 (SIGABRT 시점) resolved 처리했다.
Root Cause Summary#
BIM 비교 child process (BimCompareProcess, packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts) 가 ForgeAgent.extract 호출 도중 V8 heap 한계 (약 8 GB) 를 초과해 OOM 으로 abort 했다. Mark-Compact GC 가 연속으로 mu = 0.091 → 0.006 로 붕괴한 것 (거의 회수 못 함) 이 heap 이 대형 Forge 모델 데이터로 포화 상태였음을 보여준다. Parent 의 ChildProcessManager::setupEventHandlers (packages/base/src/manager/child-process.manager.ts:138) 는 child stderr 를 라인 단위 로 logger.error 로 출력하기 때문에, V8 fatal handler 가 뱉은 native stack trace 한 줄 한 줄이 개별 error 로그로 기록되고 fingerprinter 가 이를 25개 클러스터로 나누는 부수 효과가 생겼다.
Technical Analysis#
Code Path#
- Entry point:
BimRevisionService::run→runBimCompare(packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:603) - IPC dispatch:
BimCompareManager::execute→ChildProcessManager::execute(packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:53-79) - Child worker:
BimCompareProcess::executeinvokesForgeAgent.extract(packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-104) - Failure point: child process V8 heap OOM during
ForgeAgent.extract— stderr handler atpackages/base/src/manager/child-process.manager.ts:138-141.
Parent forks child without a custom heap flag; execArgv 는 지정되지 않음 (기본값 = parent NODE_OPTIONS 상속):
this.process = fork(actualScriptPath, {
stdio: ['inherit', stdOut, stdErr, 'ipc'],
});
logger.debug(`ChildProcessManager::start | Child process forked with PID: ${this.process.pid}, stdio: ${this.options.stdioMode}`);
Stderr handler가 라인당 하나의 error 로그를 만든다 (fingerprint 파편화의 원인):
this.process.stderr?.on('data', (data: Buffer) => {
const text = data.toString();
logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});
Child 는 Forge extract 만 실행:
const result: ForgeAgent.Result = await ForgeAgent.extract(params.apiConfig, params.urn, {
region: params.region,
extractRoom: false,
extractMeta: false,
extractCompare: {
urn: params.previousUrn,
region: params.previousRegion,
query: params.query
}
});
Parent 는 SIGABRT 를 close 이벤트로 잡고 pending IPC 를 모두 reject:
this.process.on('close', (code: number | null, signal: string | null) => {
logger.error('ChildProcessManager::setupEventHandlers | Child process closed', {
code,
signal
});
const errorMsg = signal
? `Process killed by signal ${signal} (code: ${code})`
: `Process exited with code ${code}`;
this.rejectAllPending(new Error(errorMsg));
this.process = undefined;
this.emit('close', code, signal);
});
Reject 는 runBimCompare 로 전파되어 BimComparisonState=Error 로 상태 저장 (bim-revision-service.ts:188-190).
기대 동작 vs 실제 동작: bim-compare.process.ts 는 파일 헤더 주석에서 "This isolation prevents memory-intensive Forge operations from affecting the main service" 라고 명시 — child 를 두는 이유가 바로 OOM 격리다. 실제로도 parent 는 살아남았고, child 만 SIGABRT. 하지만 (a) child heap 한계 자체가 여전히 부족했고, (b) crash 로그가 25개 fingerprint 로 파편화되어 알럿·상관관계 분석을 저해했다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-bimrevision-agent status:error
from: 2026-07-21T11:59:00Z
to: 2026-07-21T12:02:00Z
sort: ascending
핵심 로그 (원문, KST 로는 21:00:39):
2026-07-21T12:00:39.174Z ChildProcessManager::setupEventHandlers | Child process stderr: <--- Last few GCs --->
2026-07-21T12:00:39.175Z ChildProcessManager::setupEventHandlers | Child process stderr: [45:0x17bccd20] 814987 ms: Mark-Compact 7909.6 (8231.3) -> 7909.6 (8227.1) MB, 1558.72 / 0.00 ms (average mu = 0.091, current mu = 0.008) allocation failure; scavenge might not succeed
2026-07-21T12:00:39.180Z ChildProcessManager::setupEventHandlers | Child process stderr: [45:0x17bccd20] 816543 ms: Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB, 1547.82 / 0.00 ms (average mu = 0.050, current mu = 0.006) allocation failure; scavenge might not succeed
2026-07-21T12:00:39.181Z ChildProcessManager::setupEventHandlers | Child process stderr: <--- JS stacktrace --->
2026-07-21T12:00:39.181Z ChildProcessManager::setupEventHandlers | Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
2026-07-21T12:00:39.181Z ChildProcessManager::setupEventHandlers | Child process stderr: ----- Native stack trace -----
2026-07-21T12:00:39.181Z ChildProcessManager::setupEventHandlers | Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
...
2026-07-21T12:00:39.195Z ChildProcessManager::setupEventHandlers | Child process stderr: 7: 0x1112e9c v8::internal::Heap::CollectGarbage(...) ← 대표 클러스터 3b141d70
...
2026-07-21T12:00:39.196Z ChildProcessManager::setupEventHandlers | Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...)
2026-07-21T12:00:39.196Z ChildProcessManager::setupEventHandlers | Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*)
2026-07-21T12:01:45.908Z ChildProcessManager::setupEventHandlers | Child process exited
2026-07-21T12:01:45.909Z ChildProcessManager::setupEventHandlers | Child process closed
2026-07-21T12:01:45.910Z Process killed by signal SIGABRT (code: null)
2026-07-21T12:01:45.911Z BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"
주목할 점:
- 두 번째 Mark-Compact 에서
current mu = 0.006— CPU 의 99.4% 가 GC 에 쓰이고 있는 상태. V8 이 "ineffective mark-compacts" 로 판단해 즉시 abort. Runtime_NewArray→NewFixedArrayWithFiller최상위 프레임 — JS 레벨에서 큰 배열 (전형적으로 Forge 모델 파싱,AECModelData.json로드 후 vertex/index buffer 생성) 을 할당하려다 실패.- Native stack 은 있지만 JS stack (
<--- JS stacktrace --->) 은 heap 이 이미 고갈 상태라 V8 이 dump 하지 못했다. - 별개 시간대(
12:42:22–25Z) 에는 같은stderrhandler 로ForgeUtilsError: API_NOT_FOUND(Autodesk Forge SDK 의 별개 downstream 이슈) 가 들어오는 것도 관측됨 — 이번 클러스터의 root cause 는 아니지만, stderr 로 여러 종류의 issue 가 뭉치는 구조적 문제를 재확인.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | BIM child process 가 V8 heap 한계 (~8 GB) 를 넘어 OOM 으로 abort | FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory; Mark-Compact 7916.1 -> 7916.1 MB (current mu = 0.006); SIGABRT (code: null, signal: SIGABRT); Runtime_NewArray/NewFixedArrayWithFiller 상위 프레임 → JS 배열 할당 실패 |
— | Confirmed |
| H2 | Autodesk Forge API_NOT_FOUND (ForgeUtilsError) 가 원인 |
12:42:22–25Z 로그에서 errorCode: 'API_NOT_FOUND' 관측 |
대표 클러스터 시각 12:00:39Z 와 42 분 차이. 12:00:39 창에는 API_NOT_FOUND 로그 없음; stderr 는 V8 fatal 로만 채워짐 |
Rejected |
| H3 | Parent (ChildProcessManager) 코드 결함으로 다중 클러스터 발생 |
25개 클러스터가 동일 서비스에서 동시 발생; stderr handler 는 라인마다 logger.error 호출 |
하지만 이는 결과이지 crash 원인 아님. root cause 는 여전히 child heap OOM. handler 는 log 파편화만 유발 | Rejected (as root cause) — 다만 관측성 개선 필요 (fix 섹션 참고) |
| H4 | execute timeout (parent side) 이 SIGKILL 을 트리거해 child 를 죽였다 |
ChildProcessManager::sendMessage (child-process.manager.ts:213-225) 에 timeout → SIGKILL 로직 존재 |
실제 신호는 SIGABRT (V8 self-abort), SIGKILL 이 아니며, executeTimeoutMs 사용 흔적도 BimCompareManager 에는 없음 (bim-compare.manager.ts:53-79 는 timeout 인자 미지정) |
Rejected |
| H5 | 컨테이너 OOMKilled (cgroup OOM) 로 인한 abort | — | signal 은 SIGABRT (V8 자체 abort), SIGKILL 이 아님. cgroup OOM 이면 no stderr, 즉시 SIGKILL. 이번 케이스는 V8 이 스스로 FATAL ERROR 를 찍고 abort |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- child fork 에
--max-old-space-size명시:packages/base/src/manager/child-process.manager.ts:73-76의fork()호출에서execArgv를 지정해 child heap 한계를 부모와 무관하게 명확히 관리하도록 한다. 현재는 parent 의NODE_OPTIONS를 상속하기 때문에 배포 이미지 변경 시 조용히 낮아질 수 있다.ChildProcessOptions에maxOldSpaceMB옵션을 추가하고BimCompareManager는 최소 8192 이상을 요구하도록 초기화한다. 방향만 제시 (구현 코드는 별도 PR). - child 사망 시 log 파편화 완화:
packages/base/src/manager/child-process.manager.ts:138-141의 stderr handler 가 라인마다logger.error를 부르지 않도록 버퍼링 후 하나의 로그 이벤트 (혹은logger.warn+ 첫 라인 요약) 로 묶는다. 이번 crash 가 25개 fingerprint 로 파편화된 것은 알럿/상관관계 분석을 저해했다.
단기 개선 (1주 이내)#
- 입력 크기 기반 방어:
BimCompareManager::execute(bim-compare.manager.ts:53-79) 진입 시params.query.entities.length등을 기준으로 heap 위험 임계를 계산·경고하고, 특정 임계 초과 시 사전에BimRevisionRequest.BimComparisonStateEnum.Error+ 명시적errorCode: BimRevisionTooLargeInput로 조기 종료. 지금은 heap 한계까지 밀어붙이다가 SIGABRT 로 끝난다. - 크래시 상관관계 로그: parent 가
close(signal=SIGABRT) 이벤트를 받았을 때, 원본 IPC message id/job id/urn을 함께 남기도록child-process.manager.ts:107-120을 확장한다. 현재 로그는 "Process killed by signal SIGABRT" 만 있어 어떤 BIM revision 인지 즉시 추적이 어렵다. - Forge extract streaming:
ForgeAgent.extract결과를 in-memoryResult.Compare.modified/removed/exist로 완전 적재하는 대신 stream/chunk 로 소비하는 옵션을 forge-agents 팀에 요청. 이번 crash 상위 프레임이Runtime_NewArray인 점이 array-heavy 소비 패턴을 시사.
장기 개선 (재발 방지)#
- child worker 리소스 계약: heap/CPU/timeout 을
ChildProcessOptions로 명시하고, agent 별 SLA (BIM revision: 8 GB / 30 min 등) 를 표준화. 지금은 각 agent 가 암묵적으로 컨테이너 default 에 의존한다. - 관측성: Node.js
--heapsnapshot-near-heap-limit=1나--report-on-fatalerror로 crash 직전 heap snapshot / diagnostic report 를 생성해 S3 로 업로드하는 hook 추가. 재발 시 어떤 객체가 heap 을 채웠는지 정량적으로 확인할 수 있다. - fingerprint 규칙: error-sweeper 쪽에서
ChildProcessManager::setupEventHandlers | Child process stderr:prefix 를 가진 로그는 한 서비스 내에서 하나의 클러스터로 뭉치도록 collector 규칙 조정 검토.
Monitoring#
writing-datadog-monitoring-queries 가이드에 따라 dashboard timeseries widget 에 그대로 넣을 수 있는 형태로 작성.
Child process fatal OOM 발생 건수:
service:cupixworks-any-bimrevision-agent status:error "JavaScript heap out of memory"
Child SIGABRT (원인 무관, 상관 알럿용):
service:cupixworks-any-bimrevision-agent status:error "Process killed by signal SIGABRT"
BimRevisionService 실패율 (per-job 관점):
service:cupixworks-any-bimrevision-agent status:error "BimRevisionService::run | error"
Mark-Compact ineffective 조기 경고 (heap 압박이 반복될 때 감지):
service:cupixworks-any-bimrevision-agent status:error "Ineffective mark-compacts"
권장 알럿: "JavaScript heap out of memory" 가 24h 내 1건 이상이면 clark-vdc 채널에 P2 알럿. 하루 5건 이상이면 heap 한계 상향 또는 입력 크기 방어 즉시 롤아웃.
Risk Assessment#
- Risk level: medium — 개별 job 실패로 격리되고 parent service 는 살아남지만, 특정 BIM revision 은 항상 실패 (동일 URN 이 다시 처리되면 재발 가능성 높음). 발생 빈도가 늘면 clark-vdc 워크플로 차단으로 승격.
- 예상 복잡도: standard —
fork()호출부의execArgv추가와 stderr handler 버퍼링은 국소 변경. 단,packages/base는 여러 agent 가 공유하므로 회귀 테스트 (packages/base/src/manager/child-process.manager.spec.ts) 갱신 필수.