ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]
RCA: ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]
Overview#
What Happened#
2026-07-21 21:00:39 KST, cupixworks-any-bimrevision-agent 서비스에서 BIM comparison 자식 프로세스가 V8 heap OOM 으로 SIGABRT 종료됐다. 부모 프로세스의 ChildProcessManager 가 자식 stderr 를 라인 단위로 로거에 그대로 전달하는 구조이기 때문에, V8 이 crash 시 출력하는 native stack trace 한 줄 한 줄이 각각 별도의 error 로그로 쪼개져 25개의 유사 클러스터로 fingerprint 되었다. 이 클러스터(eecaced6)는 그 stack trace 중 15번 프레임(0x12c1b3b [/usr/bin/node]) 하나에 해당한다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | V8 FatalProcessOutOfMemory (parent forwards child stderr) |
| exception.message | ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node] |
| top_frame | packages/base/src/manager/child-process.manager.ts:140 (stderr listener) |
| child_crash_frame | v8::internal::ArrayConstructInitializeElements → v8::internal::Runtime_NewArray |
| runtime | Node.js on /usr/bin/node, heap limit --max-old-space-size=8192 (8 GB) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (BIM revision agent) | 1 for this cluster / 25 clusters in the same crash | 단일 BIM revision job (bim_id=17216, src_revision_id=25433, prev_revision_id=21647) 실패, 883421ms(약 14.7분) 처리 후 SIGABRT |
Timeline#
- 2026-07-21 20:45:56 KST —
BimRevisionService::run시작 (역산:REVISE-END시각 −elapsed_ms=883421) - 2026-07-21 21:00:39 KST — V8 이
Ineffective mark-compacts near heap limit로 OOM abort. 자식 프로세스가 stderr 로<--- Last few GCs --->… native stack …FATAL ERROR를 순차 출력 - 2026-07-21 21:00:39 KST — 부모
ChildProcessManager가 각 stderr 라인을 별도 error 로그로 emit (이 클러스터의 라인 포함) - 2026-07-21 21:01:45 KST — Child process
close/exit이벤트 발생,BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)" - 2026-07-21 21:01:46 KST —
BimRevisionService::REVISE-END로그로 job 실패 확정 (status:error, counts 전부 0)
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: clark-vdc
- 발생 횟수: 1 (단일 crash 이벤트의 stderr 라인 하나. 동일 crash 에 연결된 클러스터 25개 존재)
- 최초 발생: 2026-07-21 21:00:39 KST
- 최근 발생: 2026-07-21 21:00:39 KST
- 사용자 영향:
bim_id=17216의 BIM revision 비교가 실패했고, 파생 산출물(modified/removed/exist 카운트 등) 이 갱신되지 않았다.
Root Cause Summary#
BIM comparison 을 담당하는 fork 된 Node.js 자식 프로세스(bim-compare.process) 가 실행 도중 --max-old-space-size=8192 (8 GB) heap 한계에 도달했고, v8::internal::ArrayConstructInitializeElements 호출 시점의 배열 할당에서 V8 이 FatalProcessOutOfMemory 를 발생시키며 프로세스가 SIGABRT 로 종료됐다. 이 클러스터의 대표 메시지는 crash 자체가 아니라 그 native stack trace 중 한 줄(프레임 15, 0x12c1b3b)에 불과하다. 부모 ChildProcessManager 가 process.stderr.on('data') 핸들러에서 stderr chunk 를 라인 단위로 error 레벨로 로깅하기 때문에 (packages/base/src/manager/child-process.manager.ts:138-141), 하나의 crash 로부터 20여 줄의 stderr 가 각각 fingerprint 되어 별도 클러스터로 잡힌다.
Technical Analysis#
Code Path#
- Entry point:
packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts(BimRevisionService::run) - Manager:
packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:65—BimCompareManager.execute가 IPC 로execute를 자식에게 보냄 - Child process entrypoint:
packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72—ForgeAgent.extract(...)호출 (Autodesk Forge SVF2 model + previous URN 비교) - Fork 지점:
packages/base/src/manager/child-process.manager.ts:74—fork(actualScriptPath, { stdio: ['inherit', 'pipe'|'inherit', 'pipe'|'inherit', 'ipc'] }).execArgv를 override 하지 않으므로 부모의--max-old-space-size=8192를 그대로 상속함 - Failure surface:
packages/base/src/manager/child-process.manager.ts:138-141— 자식 stderr 를 라인 단위 error 로그로 forwarding
부모 run script (heap 한계 정의 지점):
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \
--env_name $CPX_ENVIRONMENT_NAME \
...
Fork 호출:
const stdOut = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';
const stdErr = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';
try {
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}`);
} catch (error: any) {
logger.error(`ChildProcessManager::start | Fork failed: ${error.message}`, {
actualScriptPath,
error: error.stack
});
throw error;
}
Stderr forwarding (라인 fanout 의 원인):
this.process.stderr?.on('data', (data: Buffer) => {
const text = data.toString();
logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});
Close/exit 이벤트 (SIGABRT 최종 신호):
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));
...
});
자식 프로세스가 실행하는 실제 작업:
private async execute(params: BimComparerParams): Promise<ForgeAgent.Result> {
try {
this.log(`BimCompareProcess::execute | forge-agents version: ${forgeAgentsPkg.version}, spec_version: ${SpecVersion}`);
this.log('BimCompareProcess::execute | start');
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
}
});
기대 동작: ForgeAgent.extract 가 두 revision 을 비교하고 결과를 IPC 로 반환.
실제 동작: 비교 도중 대용량 배열(V8 native stack 의 ArrayConstructInitializeElements)이 heap 한계(8 GB)를 초과하는 순간 V8 이 fatal OOM 을 던지고 프로세스가 SIGABRT 로 죽음. 부모는 자식 stderr 를 라인 단위로 error 로그 emit → 로그 파이프라인이 fingerprint 별 클러스터로 분산.
Log Evidence#
Cluster 파일에서 그대로 복사한 Datadog 쿼리:
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"
추가 확인 쿼리 (native stack 전체 재구성):
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager"
같은 이벤트의 stderr 를 시간순으로 재구성한 결과 (21:00:39 KST, 원문 그대로):
Child process stderr: <--- Last few GCs --->
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
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
Child process stderr: <--- JS stacktrace --->
Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Child process stderr: ----- Native stack trace -----
Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr: 2: 0xee8300 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr: 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr: 4: 0x10fb205 [/usr/bin/node]
Child process stderr: 5: 0x10fb794 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/usr/bin/node]
Child process stderr: 6: 0x1112684 v8::internal::Heap::PerformGarbageCollection(...) [/usr/bin/node]
Child process stderr: 7: 0x1112e9c v8::internal::Heap::CollectGarbage(...) [/usr/bin/node]
Child process stderr: 8: 0x10e91f1 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(...) [/usr/bin/node]
Child process stderr: 9: 0x10ea385 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(...) [/usr/bin/node]
Child process stderr: 10: 0x10c69a6 v8::internal::Factory::AllocateRaw(...) [/usr/bin/node]
Child process stderr: 11: 0x10b7e4a v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawArray(...) [/usr/bin/node]
Child process stderr: 12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...) [/usr/bin/node]
Child process stderr: 13: 0x129975f [/usr/bin/node]
Child process stderr: 14: 0x12a6793 [/usr/bin/node]
Child process stderr: 15: 0x12c1b3b [/usr/bin/node] ← 이 클러스터 (eecaced6)
Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments<(v8::internal::ArgumentsType)1>*) [/usr/bin/node]
Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
Child process stderr: 18: 0x7f9597eda476
Child process closed
Child process exited
Job-level 결과 로그 (BimRevisionService):
{
"message": "BimRevisionService::REVISE-END",
"bim_id": 17216,
"src_revision_id": 25433,
"prev_revision_id": 21647,
"si_trace_id": "a052780a-c69b-457d-b72d-3aa6332272a3",
"elapsed_ms": 883421,
"status": "error",
"error_message": "Process killed by signal SIGABRT (code: null)",
"counts": {"total": 0, "modified": 0, "removed": 0, "exist": 0}
}
Status board 결과: svc:cupixworks-any-bimrevision-agent::unknown 스코프에 25개 클러스터가 동일 crash 이벤트로 grouping 되어 있음 (id 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1, 이미 resolved). 이 클러스터도 그 25개 중 하나.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 자식 Node.js 프로세스의 V8 heap OOM 이 근본 원인이고, 이 클러스터는 그 crash native stack 의 프레임 15 한 줄이 fingerprint 된 결과이다 | 같은 timestamp(21:00:39 KST)에 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory, Mark-Compact 헤더가 7909.6 → 7909.6 MB / heap limit 8231.3 MB 로 8 GB 한계 근접, 그 직후 native stack 프레임 1 – 18 이 순차 stderr 출력. 부모 run script 에 --max-old-space-size=8192, fork() 가 execArgv override 없음 → 자식도 동일 8 GB 한계. 최종 Process killed by signal SIGABRT (code: null) job error 로그로 확정 |
— | Confirmed |
| H2 | Datadog 로그 파이프라인/컬렉터 자체의 버그로 하나의 로그가 여러 클러스터로 쪼개졌다 | — | 각 stderr chunk 가 별도 error 로그로 emit 되는 것은 ChildProcessManager 의 stderr.on('data') 구현(packages/base/src/manager/child-process.manager.ts:138-141) 이 라인 단위로 logger.error 를 호출하기 때문이며, 이는 애플리케이션 레벨 동작이다. status board 도 25개 클러스터를 정상적으로 하나의 서비스 인시던트로 묶었다 |
Rejected |
| H3 | Autodesk Forge API 의 API_NOT_FOUND 오류가 root cause |
21:42 시간대 로그에 ForgeUtilsError: Resource not found / errorCode: 'API_NOT_FOUND' 다수 존재 |
해당 로그의 timestamp 는 21:42 KST 로 이 클러스터의 21:00:39 KST 와 42분 차이. 21:00:39 시점에는 Forge API 오류 대신 V8 OOM stack 만 출력됨 (`BimRevisionService::run | error` 도 signal SIGABRT 로 확정). Forge 404 는 별개 사건 |
| H4 | Sidekiq/timeout 등 프로세스 외부 요인이 SIGABRT 를 유발 | — | SIGABRT 는 V8 이 OOMErrorHandler 에서 abort() 호출 시 발생하는 신호이고, stack trace 프레임 1 이 node::OOMErrorHandler 로 시작. 외부 kill 신호 (SIGKILL/SIGTERM) 가 아님. ChildProcessManager::sendMessage 의 timeout 경로는 SIGKILL 을 보내므로(packages/base/src/manager/child-process.manager.ts:221) SIGABRT 와 매치되지 않음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 클러스터링 fingerprint 개선 (파이프라인 측): 하나의 V8 OOM crash 가 20여 개 클러스터로 쪼개지는 것은 대응·리포팅 노이즈를 크게 키운다.
ChildProcessManagerstderr chunk 를 로거로 흘릴 때 개별 라인 대신 crash boundary(예:<--- Last few GCs --->시작부터Native stack trace종료까지)를 하나의 로그 이벤트로 묶어 emit 하도록 검토. 파일:packages/base/src/manager/child-process.manager.ts:138-141. 이 조치는 프런트엔드와 무관하지만 로깅 스키마 변경이므로 downstream(Datadog fingerprint, error-sweeper) 영향 확인 필요 — 별도 트랙으로 진행 권장. - 실패한 job 재처리 여부 판단:
bim_id=17216, src_revision_id=25433, prev_revision_id=21647는 실패 후 카운트 0 으로 종료. 재시도가 자동인지, 동일 revision 조합에서 OOM 이 재현되는지(→ 결정적 결함) 확인 필요.
단기 개선 (1주 이내)#
- 자식 프로세스 heap 한계 명시:
fork()호출 시execArgv를 명시적으로 지정해 자식이 어떤 heap 한계로 도는지 코드만으로 파악 가능하게 만들 것. 현재는 shell script(80_run-agent.sh)를 확인해야 한다. 파일:packages/base/src/manager/child-process.manager.ts:74. - BIM comparison memory instrumentation:
bim-compare.process의execute진입/종료 및 주기적 지점에서process.memoryUsage()(heapUsed, external, rss)를 debug 로 남기고, 임계치 초과 시 warn 로그. 어떤query.entities.length/ 두 URN 조합에서 heap 이 급증하는지 재현 조건을 좁힐 수 있음. 파일:packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-81. - OOM 이벤트 명시적 판단: 부모
close핸들러에서signal === 'SIGABRT'이거나 최근 stderr 에FATAL ERROR: ... heap out of memory가 있었다면 error 로그 필드에oom: true태그를 붙여 fingerprint 개선을 도울 것. 파일:packages/base/src/manager/child-process.manager.ts:107-120,138-141.
장기 개선 (재발 방지)#
- BIM 비교 파이프라인의 대용량 데이터 처리 재설계: 8 GB 한계에서 터진다는 것은 model / query.entities 크기가 특정 데이터셋에서 예측 불가능하게 폭증한다는 뜻.
ForgeAgent.extract내부에서 entity/model 을 스트리밍/청크 처리하거나, 사전에 entity 수 상한을 정하고 초과 시 batching 하도록 리팩터. - 컨테이너 메모리 vs
--max-old-space-size정합성 재검토: 인스턴스 total memory 대비 heap limit 이 적정한지, 두 프로세스(부모 + 자식) 총합이 컨테이너 한도를 초과하지 않는지 확인. 필요하면 자식만 상향 조정하되 RSS 관측을 붙일 것. - Job 실패 시 관찰 가능성 향상:
BimRevisionService::REVISE-END에crash_kind: v8_oom | timeout | other같은 구조적 필드 추가로 SRE 대시보드에서 단일 grouping 가능하게.
Monitoring#
REQUIRED SUB-SKILL: 이 쿼리들은 release dashboard timeseries widget 에 그대로 들어간다는 전제로 monitor-only 문법(| stats, count by(...) 등)을 피했다.
- 부모 프로세스가 감지한 자식 OOM 관련 stderr 폭발 (라인 fanout 을 그대로 세는 프록시):
service:cupixworks-any-bimrevision-agent status:error @environment:production "FATAL ERROR: Ineffective mark-compacts near heap limit"
- BIM revision job 실패 (SIGABRT):
service:cupixworks-any-bimrevision-agent status:error @environment:production "Process killed by signal SIGABRT"
- 자식 프로세스 close/exit 이벤트 (신호 상관없이 종료 자체):
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers | Child process closed"
- Job-level 실패 (elapsed_ms, bim_id 확인용):
service:cupixworks-any-bimrevision-agent @environment:production "BimRevisionService::REVISE-END" @status:error
Risk Assessment#
- Risk level: medium — 단일 job 이 실패했고 시스템 전반은 영향 없음. 다만 25개 클러스터로 fanout 되어 알림 소음을 크게 만들었고, 동일 조건 재현 시 재발 가능성 있음.
- 예상 복잡도: standard — 즉시 조치(로그 grouping, 재시도 여부 판단)는 trivial ~ standard. 장기 개선(대용량 BIM 비교 스트리밍화)은 별도 프로젝트 규모.