ES /docs

ForgeAgent.extract OOM — SVF2 모델 파싱 힙 초과

RCA: ChildProcessManager child process V8 heap OOM

Overview#

BIM revision 비교 작업 중 자식 프로세스에서 V8 heap 이 8 GB 한계에 도달해 FATAL ERROR: Ineffective mark-compacts near heap limit으로 종료되었다. 부모 ChildProcessManager 는 stderr 로 흘러들어온 V8 crash dump 각 라인을 error 로그로 남겼고, 그중 한 프레임(13: 0x129975f [/usr/bin/node])이 대표 fingerprint 로 잡혔다.

What Happened#

2026-07-21 21:00:39 KST 에 cupixworks-any-bimrevision-agent (team clark-vdc, region us-west-2) 의 BIM 비교 자식 프로세스가 heap OOM 으로 SIGABRT 종료되었다. 부모 BimCompareManager 는 IPC 로 응답을 기다리다 자식 close 이벤트에서 Process killed by signal SIGABRT (code: null) 로 요청을 실패시켰다. 동일 fingerprint 는 지난 14일간 이번 1건이다.

Quick Facts#

Field Value
exception.class V8 fatal (v8::internal::V8::FatalProcessOutOfMemory)
exception.message FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
top_frame 13: 0x129975f [/usr/bin/node]
runtime Node.js (heap --max-old-space-size=8192, 8233 MB commit at crash)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (cupixworks-any-bimrevision-agent) 1 (fingerprint) / 25 (전체 서비스 클러스터, 같은 60 s 창) 해당 SQS 메시지의 BIM revision 비교 실패 (`BimRevisionService::run

Timeline#

  1. 2026-07-21 21:00:39 KST — 자식 프로세스가 마지막 Mark-Compact 후 Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB 를 남기고 V8 OOM 으로 abort. 부모가 stderr 각 라인을 ChildProcessManager::setupEventHandlers | Child process stderr: ... 로그로 방출.
  2. 2026-07-21 21:01:45 KST — 부모가 자식 close/exit 이벤트를 감지, Process killed by signal SIGABRT (code: null) 로 pending IPC 요청 reject. BimRevisionService::run 이 동일 메시지를 error 로그로 남기고 SQS 메시지 처리 실패.
  3. 2026-07-21 21:01:45 KST — status-board 가 같은 60 초 창 안 25개 클러스터를 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 인시던트로 묶고 resolved 로 마감.

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr: 13: 0x129975f  [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

Root Cause Summary#

BIM 비교 자식 프로세스가 ForgeAgent.extract 실행 중 8 GB (--max-old-space-size=8192) old-space 한계에 도달해 V8 OOM 으로 SIGABRT 되었다. crash dump 는 마지막 프레임이 v8::internal::Runtime_NewArrayArrayConstructInitializeElementsNewFixedArrayWithFiller 로, 큰 배열을 새로 초기화하다 실패했다. 부모 ChildProcessManager 는 자식 stderr 각 라인을 개별 error 로그로 방출하기 때문에 crash dump 의 특정 라인 (13: 0x129975f [/usr/bin/node]) 이 별도 fingerprint 로 잡혔다. 즉, 대표 메시지는 원인이 아니라 하나의 스택 프레임이며 실제 원인은 이 서비스에서 처음 관측된 heap 고갈이다.

Technical Analysis#

Code Path#

  • Entry point: applications/agents/packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:44 (BimRevisionService::run, SQS 트리거)
  • Child spawn: applications/agents/packages/base/src/manager/child-process.manager.ts:74 (부모가 fork(actualScriptPath, ...) 로 자식 생성; 별도 execArgv 지정이 없어 부모의 --max-old-space-size=8192 를 그대로 상속)
  • Child entry: applications/agents/packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67 (BimCompareProcess.executeForgeAgent.extract(...))
  • Failure point: 자식 프로세스가 V8 old-space allocation 실패로 abort. 부모는 applications/agents/packages/base/src/manager/child-process.manager.ts:138-141 의 stderr 핸들러가 crash dump 각 라인을 error 로그로 방출.

부모의 실행 커맨드:

applications/agents/docker_services/80_run-agent.sh:1-3bash
#!/bin/bash

node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \

자식 fork 지점 — execArgv 를 지정하지 않으므로 Node 는 부모의 process.execArgv 를 자동으로 상속시켜 8 GB 한계가 자식에도 적용된다:

applications/agents/packages/base/src/manager/child-process.manager.ts:73-84typescript
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 라인 단위 error 로그 방출 — crash dump 의 모든 프레임이 개별 로그로 나오면서 각기 다른 fingerprint 가 생성된다:

applications/agents/packages/base/src/manager/child-process.manager.ts:138-141typescript
this.process.stderr?.on('data', (data: Buffer) => {
    const text = data.toString();
    logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});

자식 종료 시 부모가 SIGABRT 를 감지해 pending 요청을 reject 하는 지점:

applications/agents/packages/base/src/manager/child-process.manager.ts:107-120typescript
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);
});

기대 동작: ForgeAgent.extract 가 정상적으로 결과를 반환하고 자식이 IPC 로 응답. 실제 동작: V8 이 old-space 8 GB 를 다 쓰고 mark-compact 가 회수를 못 해 abort. 부모는 exit signal 로 요청 실패로 처리.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-any-bimrevision-agent status:error @environment:production

시간 범위: 2026-07-21T11:58:00Z ~ 2026-07-21T12:02:00Z. crash dump 핵심 라인 (원문 그대로):

text
[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
[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
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

V8 네이티브 스택 최상단 프레임 (배열 확장 경로):

text
17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments<(v8::internal::ArgumentsType)1>*) [/usr/bin/node]
12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...) [/usr/bin/node]
 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(...) [/usr/bin/node]

부모 측 종료 감지 로그:

text
2026-07-21 21:01:45  ChildProcessManager::setupEventHandlers | Child process closed
2026-07-21 21:01:45  BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"

지난 14일 heap OOM 발생 빈도 ("heap out of memory" 쿼리): 1건 (이번 이벤트 뿐).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 자식 프로세스가 V8 heap 한계에 도달해 SIGABRT crash dump 명시적 FATAL ERROR: ... JavaScript heap out of memory, Mark-Compact 7916/8233 MB, top frame Runtime_NewArray / NewFixedArrayWithFiller, 부모의 Process killed by signal SIGABRT (Datadog 2026-07-21T12:00:39Z) Confirmed
H2 자식이 부모의 --max-old-space-size=8192 를 못 받아 기본 heap (~2 GB) 에서 터졌다 80_run-agent.sh:3 이 8 GB 로 부모 기동 Mark-Compact 로그가 실제로 8233.8 MB 까지 확장된 것을 보여줌 → 자식은 8 GB 를 이미 받고 있음 (fork()execArgv 기본 상속) Rejected
H3 Forge API 404 (API_NOT_FOUND) 가 이 fingerprint 의 근본 원인 같은 서비스에서 12:42 UTC 에 Resource not found: file/urn%3Aadsk.fluent... 로그가 존재 12:00:39 UTC crash 는 API_NOT_FOUND 로그보다 42 분 이전이며 로그 페이지에 이 시점의 404 는 없음. crash dump 도 네트워크 예외가 아니라 배열 할당 실패 Rejected
H4 자식이 ChildProcess 통신 실패로 hang 되어 timeout kill 됨 부모에 timeout 논리 존재 (child-process.manager.ts:213-226) Datadog 에 Execution timeout after ...ms 로그 없음. 종료 signal 은 SIGKILL 이 아닌 SIGABRT 이며 crash dump 가 그 원인을 명시 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 재현/입력 크기 확인: 실패한 SQS 메시지의 bim_revision_id 를 확보해 (해당 클러스터 파일의 부모 인시던트 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 참조) ForgeAgent.extract 가 처리한 모델 (urn, previousUrn) 크기와 query.entities.length 를 로그로 남긴다. 지금 BimCompareManager::execute debug 로그(bim-compare.manager.ts:54-62) 는 queryEntitiesLength 만 남기므로 모델 자체 크기(예: SVF2 property DB 총 fragment 수)를 함께 기록해 재발 시 원인을 분리한다.
  • 부모 stderr 로그 레벨 조정 검토: 현재 child-process.manager.ts:138-141 이 자식 stderr 각 라인을 logger.error 로 방출해 crash dump 프레임 하나하나가 별도 클러스터/fingerprint 를 만든다 (동일 60 초에 25개 클러스터). 라인 별 error 방출 대신 stderr 를 버퍼링 후 종료 시 한 번에 방출하거나, stderr 자체는 warn 으로 두고 close(signal=SIGABRT|SIGKILL) 시점에만 하나의 error 를 남기는 방향을 검토한다. UI/알림 잡음을 크게 줄인다.

단기 개선 (1주 이내)#

  • 자식 heap 한계 명시 상속 및 조정: fork() 호출에 execArgv: ['--max-old-space-size=8192'] (또는 큰 모델 처리 시 12288) 를 명시해 상속 여부가 런타임/컨테이너 재기동 정책과 무관하게 결정되도록 한다. 동시에 컨테이너 memory limit 이 heap+RSS overhead 를 감당하는지 확인 (80_run-agent.sh 는 heap 만 8 GB 로 두므로 컨테이너 8~10 GB 필요).
  • 입력 사이즈 상한/조기 실패: BimRevisionService::run 에서 query.entities.length 또는 모델 fragment 수가 임계치 초과 시 heap 을 태우기 전에 warn + drop 또는 partitioned 처리로 우회한다. forge-agents 의 배열 확장 지점이 실패 지점이므로 상한을 명확히 잡을 것.

장기 개선 (재발 방지)#

  • 자식 프로세스에 heap 사용량 모니터링: process.memoryUsage() 를 30초 주기로 IPC 로 부모에 보고, 부모가 Datadog metric (agent.child.heap_used_mb) 으로 emit. OOM 직전 growth curve 를 확보하면 리크 vs 단발 spike 를 구분할 수 있다.
  • crash dump 별도 라우팅: V8::FatalProcessOutOfMemorySIGABRT 이 감지되면 부모가 dump 전체를 하나의 error 로그에 attachment 로 실어 보내는 별도 경로를 만들어, 스택 프레임 라인이 개별 알림을 만드는 현재 구조를 완전히 대체한다.

Monitoring#

  • 자식 프로세스 종료 signal 별 카운트:
text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,@message:"Process killed by signal SIGABRT"}.as_count()
  • 자식 stderr 에러 라인 폭발 감시 (crash dump 발생 시 spike):
text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,status:error,@message:"ChildProcessManager::setupEventHandlers | Child process stderr"}.as_count()
  • BIM 비교 실패 카운트:
text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,@message:"BimRevisionService::run | error"}.as_count()
  • 컨테이너 RSS 상한 근접 감시:
text
avg:container.memory.usage{service:cupixworks-any-bimrevision-agent}

Risk Assessment#

  • Risk level: medium (14일 창 1건 이지만 발생 시 SQS 메시지 하나가 완전히 실패, 재시도 시 동일 입력이면 재발 가능)
  • 예상 복잡도: standard (execArgv 명시 + stderr 로그 방식 조정 + 입력 상한. forge-agents 자체를 건드리지 않는 범위)