ChildProcessManager fork — child process heap exhaustion during BIM comparison
RCA: Process killed by signal SIGKILL (code: null)
Error Log#
Process killed by signal SIGKILL (code: null)
Impact#
- Service:
cupixworks-any-room-agent - Team: gad
- 발생 횟수: 3
- 최초 발생: 2026-04-07T10:53:33.309Z
- 최근 발생: 2026-04-07T13:36:35.481Z
Root Cause Summary#
cupixworks-any-room-agent의 child process가 BIM 모델(ID 17926, 17928)에서 room을 추출하는 도중 SIGKILL 신호로 종료되었습니다. 이 서비스는 ECS Fargate에서 12 GB 메모리 제한으로 실행되며, 메인 프로세스에 --max-old-space-size=8192 (8 GB)가 설정되어 있습니다. 그러나 ChildProcessManager가 fork()로 생성하는 room extraction child process에는 별도의 V8 heap 제한이 설정되지 않아 기본값(약 4 GB)까지 메모리를 사용할 수 있습니다. 대형 BIM 모델을 처리할 때 메인 프로세스(8 GB) + child process + filebeat + OS 오버헤드의 합계가 Fargate의 12 GB hard limit을 초과하면, 컨테이너 레벨의 OOM killer가 SIGKILL을 보내 child process를 강제 종료합니다. 성공한 작업들은 210분 내 완료되었으나, SIGKILL이 발생한 작업들은 710분간 실행된 후 종료되었으며 이는 메모리가 점진적으로 증가하다 한계에 도달했음을 시사합니다.
Technical Analysis#
Code Path#
- Entry point: 메인 프로세스가 SQS 메시지를 수신하고
RoomService.run()을 호출합니다.packages/cupix-tesla-room-agent/src/room-service.ts:58
// packages/cupix-tesla-room-agent/src/room-service.ts:58-68
protected run = async (targetId: number): Promise<void> => {
const cpBim = await this.createCPBimByBimId(targetId);
if (cpBim) {
this._modelInProcess = cpBim;
this._forgeApi.updateRegionFromBim(cpBim.forgeRegion);
if (cpBim.workspaceDirPath) this.setModelWorkspace(cpBim.workspaceDirPath);
if (cpBim.isGenerateMasterViews) {
await this.forgeAuth.authenticate();
await this.updateBimRoomState(targetId, TESLA.UpdateBimRequest.RoomStateEnum.Extracting);
await this.runRoomExtractor(cpBim);
- Room extraction 시작:
RoomExtractManager.execute()가ChildProcessManager.execute()를 통해 child process에 작업을 위임합니다.packages/cupix-tesla-room-agent/src/manager/room-extract.manager.ts:25-31
// packages/cupix-tesla-room-agent/src/manager/room-extract.manager.ts:11-12
constructor() {
this.childProcessManager = new ChildProcessManager(__dirname, 'process/room-extractor.process');
}
여기서 ChildProcessManager가 executeTimeoutMs 없이 생성되므로 child process에 대한 실행 시간 제한이 없습니다.
- Child process fork:
ChildProcessManager.start()에서fork()로 child process를 생성합니다. 이때--max-old-space-size플래그가 전달되지 않습니다.packages/base/src/manager/child-process.manager.ts:74
// packages/base/src/manager/child-process.manager.ts:73-76
try {
this.process = fork(actualScriptPath, {
stdio: ['inherit', stdOut, stdErr, 'ipc'],
});
fork() 호출 시 execArgv 옵션이 지정되지 않아, 부모 프로세스의 --max-old-space-size=8192가 child process에도 상속됩니다. 즉 child process도 최대 8 GB heap을 사용할 수 있어, 메인(8 GB) + child(8 GB) = 16 GB로 컨테이너 제한(12 GB)을 초과할 수 있습니다.
- Room extraction 실행: child process에서
ForgeAgent.extract()또는RoomExtractor.room_extract()가 대형 BIM 모델의 SVF2 매니페스트를 파싱하고 3D geometry를 메모리에 로드합니다.packages/cupix-tesla-room-agent/src/process/room-extractor.process.ts:39-56
// packages/cupix-tesla-room-agent/src/process/room-extractor.process.ts:39-56
if (params.formatType === 'svf2') {
const result = await ForgeAgent.extract(params.apiConfig, params.urn, {
extractRoom: true,
extractMeta: true,
region: params.region
});
if (result.Error == undefined) {
extract = result;
} else if (result.Error === ForgeAgentErrorCode.FORGE_SOURCE_INVALID_MANIFEST) {
this.log('RoomExtractorProcess::execute - svf2 manifest is invalid, try svf1 process');
extract = await RoomExtractor.room_extract(params.token, params.urn);
} else {
throw new Error('Failed to get rooms from svf2');
}
} else {
extract = await RoomExtractor.room_extract(params.token, params.urn);
}
- Failure point: Fargate가 컨테이너 메모리 hard limit(12 GB) 초과를 감지하고 child process에 SIGKILL을 전송합니다.
ChildProcessManager의close이벤트 핸들러가 이를 감지하여 에러를 생성합니다.packages/base/src/manager/child-process.manager.ts:107-119
// packages/base/src/manager/child-process.manager.ts:107-119
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);
});
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-room-agent (SIGKILL OR "Process killed")
service:cupixworks-any-room-agent (status:error OR status:warn)
service:cupixworks-any-room-agent (OOM OR "Out of Memory" OR "oom-kill" OR "memory limit" OR "Cannot allocate memory" OR "cgroup" OR "memory.max")
service:cupixworks-any-room-agent status:info
3건의 SIGKILL 인시던트 — 동일한 패턴:
각 인시던트는 4개의 에러 로그를 순서대로 생성합니다:
Incident A (BIM 17928, 호스트 ip-10-1-103-72, 10:53:33Z):
10:53:33.307Z [error] ChildProcessManager::setupEventHandlers | Child process exited {signal: SIGKILL}
10:53:33.308Z [error] ChildProcessManager::setupEventHandlers | Child process closed {signal: SIGKILL}
10:53:33.309Z [error] Process killed by signal SIGKILL (code: null)
Stack: Error: Process killed by signal SIGKILL (code: null)
at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:5159:33)
10:53:33.563Z [error] BaseService::handlingMessageErrors | Error and message object -
{"error":"undefined response","sqsMessage":{"MessageId":"04e5c63b-...","Attributes":{"ApproximateReceiveCount":"1"}}}
Incident B (BIM 17926, 호스트 ip-10-1-41-66, 12:47:17Z):
12:47:17.076Z [error] ChildProcessManager::setupEventHandlers | Child process exited {signal: SIGKILL}
12:47:17.077Z [error] ChildProcessManager::setupEventHandlers | Child process closed {signal: SIGKILL}
12:47:17.078Z [error] Process killed by signal SIGKILL (code: null)
12:47:17.341Z [error] BaseService::handlingMessageErrors | Error and message object -
{"error":"undefined response","sqsMessage":{"MessageId":"c4948c4a-...","Attributes":{"ApproximateReceiveCount":"1"}}}
Incident C (BIM 17926, 호스트 ip-10-1-162-9, 13:36:35Z):
13:36:35.480Z [error] ChildProcessManager::setupEventHandlers | Child process exited {signal: SIGKILL}
13:36:35.480Z [error] ChildProcessManager::setupEventHandlers | Child process closed {signal: SIGKILL}
13:36:35.481Z [error] Process killed by signal SIGKILL (code: null)
13:36:35.742Z [error] BaseService::handlingMessageErrors | Error and message object -
{"error":"undefined response","sqsMessage":{"MessageId":"d6182e65-...","Attributes":{"ApproximateReceiveCount":"1"}}}
처리 시간 비교 (info 로그 기반):
| BIM ID | 시작 시간 | 종료/SIGKILL | 소요 시간 | 결과 |
|---|---|---|---|---|
| 17928 | 10:44:06 | 10:53:33 (SIGKILL) | ~9분 | 실패 |
| 17926 | 12:37:17 | 12:47:17 (SIGKILL) | ~10분 | 실패 |
| 17926 | 13:29:28 | 13:36:35 (SIGKILL) | ~7분 | 실패 (재시도) |
| 18904 | 10:46:57 | 10:48:48 | ~2분 | 성공 (room size: 0) |
| 83 | 10:50:59 | 10:54:05 | ~3분 | 성공 (room size: 0) |
| 455 | 11:49:08 | 11:52:47 | ~4분 | 성공 (room size: 934) |
| 17927 | 11:58:08 | 12:06:33 | ~8분 | 성공 (room size: 176) |
| 17925 | 12:58:13 | 13:07:49 | ~10분 | 성공 (room size: 469) |
OOM 관련 로그 부재: OOM, Out of Memory, oom-kill, memory limit, Cannot allocate memory, cgroup, memory.max 키워드로 검색한 결과 0건입니다. SIGKILL은 애플리케이션이 catch할 수 없는 신호이므로, 애플리케이션 레벨 OOM 로그가 남지 않는 것은 Fargate 컨테이너 레벨 OOM kill과 일치합니다.
BIM 17926은 두 번 SIGKILL: 서로 다른 호스트(ip-10-1-41-66, ip-10-1-162-9)에서 실패하여 호스트 특정 문제가 아닌 BIM 모델 자체의 크기 문제임을 확인합니다.
인프라 리소스 설정 (Terraform):
# cupix-infrastructure/cupix-service/main.tf:945-947
container_cpu = 2048 # 2 vCPUs
container_memory = 12288 # 12 GB (hard limit)
container_memory_reservation = 12288 # 12 GB (soft limit)
메인 프로세스 Node.js heap 설정:
# applications/agents/docker_services/80_run-agent.sh:3
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs
Fix Recommendation#
즉시 조치 (Critical)#
-
Child process의 V8 heap 제한 설정:
ChildProcessManager.start()(packages/base/src/manager/child-process.manager.ts:74)에서fork()호출 시execArgv를 통해 child process의--max-old-space-size를 명시적으로 제한해야 합니다. 현재fork()가execArgv를 지정하지 않아 부모의--max-old-space-size=8192가 상속됩니다. Child process의 heap을 3~4 GB로 제한하면 컨테이너 총 메모리가 12 GB를 넘지 않도록 보장할 수 있습니다. -
Room extraction에 execution timeout 추가:
RoomExtractManager생성자 (packages/cupix-tesla-room-agent/src/manager/room-extract.manager.ts:12)에서ChildProcessManager에executeTimeoutMs옵션을 전달하여, SIGKILL 전에 graceful한 타임아웃 처리를 할 수 있도록 해야 합니다.CupixRoomExtractorTimeOut(1시간) 상수가 이미 존재하므로 이를 활용할 수 있습니다.
단기 개선 (1주 이내)#
-
대형 BIM 모델에 대한 사전 검증 로직 추가: BIM 모델의 크기나 복잡도(level 수, room 수)를 사전에 확인하여 메모리 한계를 초과할 가능성이 있는 모델에 대해 경고 로그를 남기거나 별도의 큰 리소스 task definition으로 라우팅하는 방안을 검토해야 합니다.
-
메모리 사용량 모니터링 로그 추가: child process 시작 시와 주기적으로
process.memoryUsage()를 로깅하여 OOM 직전의 메모리 상태를 추적할 수 있도록 해야 합니다.
장기 개선 (재발 방지)#
-
컨테이너 메모리 확대 또는 동적 리소스 할당: 다른 agent(예: mesh agent)처럼 필요에 따라 더 큰 리소스 프로파일로 실행할 수 있는 구조를 검토해야 합니다.
-
BIM 모델 스트리밍 처리: 현재
ForgeAgent.extract()가 모든 geometry를 메모리에 로드하는 방식에서, 레벨별 또는 청크별 스트리밍 처리로 전환하여 피크 메모리 사용을 줄이는 방안을 검토해야 합니다.
Monitoring#
- Fargate 컨테이너 메모리 사용률:
aws.ecs.memory_utilization메트릭을 모니터링하여 80% 이상 시 알림 - SIGKILL 발생 빈도 추적:
service:cupixworks-any-room-agent "Process killed by signal SIGKILL"
- Child process 처리 시간 추적: 7분 이상 실행되는 room extraction 작업에 대한 알림 설정
- 실패한 BIM ID 추적: 동일 BIM이 반복 실패하는 경우 자동 알림
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — child process의
execArgv설정은ChildProcessManager한 곳만 수정하면 되나, 메모리 할당 비율(메인 vs child) 최적화를 위한 테스트가 필요합니다. 현재 3건의 실패가 특정 대형 BIM 모델(17926, 17928)에 집중되어 있어 전체 서비스 영향은 제한적이지만, 대형 BIM 모델 처리 실패는 고객 워크플로우에 직접적 영향을 줍니다.