Process killed by signal SIGSEGV (code: null)
RCA: Process killed by signal SIGSEGV (code: null)
Overview#
What Happened#
2026-07-16 03:54:53 KST에 cupixworks-capture-preprocessor-agent (us-west-2, production, tenant=cupix) 에서 native N-API addon (scenemapperutils_api.node) 을 로드해 실행 중이던 forked 자식 프로세스가 SIGSEGV 로 즉시 종료됐다. ChildProcessManager 는 close 이벤트에서 pending IPC 요청들을 rejct 해 부모 프로세스가 Error: Process killed by signal SIGSEGV (code: null) 를 throw 했고, 결과적으로 진행 중이던 SQS 메시지 740624a0-f9f4-46e0-aca5-bd703e088157 가 실패 처리됐다. 14일 retention 내 유일한 발생 건이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error (Node.js) |
| exception.message | Process killed by signal SIGSEGV (code: null) |
| top_frame | /tmp/agent/dist/app.cjs:1459:33 (bundled child-process.manager.ts:113-114 — close handler) |
| native lib | scenemapperutils_api.node (loaded from Environment.SCENEMAPPERUTILS_API_PATH) |
| runtime | Node.js child process forked via fork(actualScriptPath, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'] }) |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| flintco (capture preprocessing) | 1 | 단일 SQS 메시지 실패 — 해당 job 은 ApproximateReceiveCount:"1" 로 SQS 가 자동 재큐잉해 재처리 가능 |
Timeline#
- 2026-07-16 03:54:41 KST —
JobManager::loadJob | begin - job id: 1199825(직전 정상 job) - 2026-07-16 03:54:53 KST — 자식 프로세스 SIGSEGV.
ChildProcessManager가close/exit이벤트를 emit 하고 pending 을 reject - 2026-07-16 03:54:53 KST —
CupixAuth::handleError | Undefined response(스택 포함 warn 로그) - 2026-07-16 03:54:53 KST — SQS 메시지
740624a0-f9f4-46e0-aca5-bd703e088157삭제 시작 (실패 처리) - 2026-07-16 03:54:58 KST —
BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"740624a0-f9f4-46e0-aca5-bd703e088157","Attributes":{"ApproximateReceiveCount":"1"}}} - 2026-07-16 03:54:58 KST —
BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/735964(crashed job 의 워크스페이스 정리) - 2026-07-16 03:54:58 KST —
JobManager::loadJob | begin - job id: 1199831— agent 즉시 다음 job 처리 재개 (자체 회복)
Error Log#
Process killed by signal SIGSEGV (code: null)
동일 시각의 warn 로그에는 full stack 이 남아 있다:
CupixAuth::handleError | Undefined response: {
"stack": "Error: Process killed by signal SIGSEGV (code: null)
at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:1459:33)
at ChildProcess.emit (node:events:536:35)
at maybeClose (node:internal/child_process:1104:16)
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
at Process.callbackTrampoline (node:internal/async_hooks:130:17)",
"message": "Process killed by signal SIGSEGV (code: null)"
}
app.cjs:1459 는 아래 Code Path 에 나오는 child-process.manager.ts 의 close 핸들러가 bundling 된 위치 (line 113-114 의 signal ? ... : 표현식 line).
Impact#
- Service:
cupixworks-capture-preprocessor-agent - Team: flintco
- 발생 횟수: 1
- 최초 발생: 2026-07-16 03:54:53 KST
- 최근 발생: 2026-07-16 03:54:53 KST
- Blast radius: 단일 SQS 메시지/job (14일 retention 내 유일한 SIGSEGV — Datadog 쿼리
service:cupixworks-capture-preprocessor-agent "SIGSEGV"결과 3건 모두 같은 03:54:53 이벤트의 error/warn 중복 로그) - 자동 회복: 5초 후 다음 job (1199831) 을 정상 로드해 처리를 계속함
Root Cause Summary#
cupixworks-capture-preprocessor-agent 는 native N-API addon scenemapperutils_api.node 를 자식 프로세스에 로드해 preprocessing 을 수행한다. 이 native 라이브러리 내부에서 발생한 segmentation fault 로 자식 프로세스가 SIGSEGV 로 즉시 종료됐고, ChildProcessManager 의 close 이벤트 핸들러가 pending IPC 요청을 Error: Process killed by signal SIGSEGV (code: null) 로 reject 하면서 상위의 SQS 메시지 처리가 실패했다. JavaScript 레벨에서 재현되는 코드 결함이 아니라 native (C++) 코드 경로 — 대부분 scenemapperutils_api.node 내부의 특정 video/photo 입력에 대한 memory 관련 결함 — 이 근본 원인이다. 14일 내 단발성이며 서비스는 즉시 자동 재개됐다.
Technical Analysis#
Code Path#
Entry point: packages/cupix-capture-preprocessor-agent/src/manager/preprocessor.manager.ts:14
PreprocessorManager 생성자는 ChildProcessManager 를 만들어 process/preprocessor.process 스크립트를 fork 하도록 지정한다:
export class PreprocessorManager {
private childProcessManager: ChildProcessManager;
setJobErrorCode?: ((errorCode: string) => void);
constructor() {
this.childProcessManager = new ChildProcessManager(__dirname, 'process/preprocessor.process');
}
Child fork: packages/base/src/manager/child-process.manager.ts:74-77
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;
}
Native addon load (crash 발생 지점): packages/cupix-capture-preprocessor-agent/src/process/preprocessor.process.ts:262-289
자식 프로세스는 scenemapperutils_api.node 를 require 로 로드해 scenemapperutils 클래스를 인스턴스화하고 process() 를 호출한다. SIGSEGV 는 이 native 호출 경로 (process() 또는 setter/getter) 내에서 발생한다:
// eslint-disable-next-line @typescript-eslint/no-require-imports
const PreprocessorApi = require(this.libPath);
const Preprocessor = new PreprocessorApi.scenemapperutils();
// Set photo items
photoPaths.forEach(path => {
Preprocessor.append_photo_filepath(path);
});
// Set video items
videoInputs.forEach(it => {
Preprocessor.append_timelapse_video_filepath(it.inputFile, it.name, it.outputDir);
});
// Set APP Data
const processingOptionsFilePath = params.processingOptionsFilePath;
if (typeof processingOptionsFilePath === 'string' && processingOptionsFilePath.length > 0) {
Preprocessor.set_processing_options_filepath(processingOptionsFilePath);
}
// Set extract video frame image
const enableExtractVideoFrame = params.enableExtractVideoFrame;
if (typeof enableExtractVideoFrame === 'boolean') {
Preprocessor.set_extract_video_frame_images(enableExtractVideoFrame);
}
await CPUtils.sleep(100);
Preprocessor.process();
await CPUtils.sleep(100);
Preprocessor.process() 는 native (SCENEMAPPERUTILS_API_PATH = lib/scenemapperutils/scenemapperutils_api.node) 호출이므로 이 지점에서 SIGSEGV 가 발생하면 자식 프로세스 전체가 죽고 부모에는 close (code=null, signal='SIGSEGV') 로만 통보된다.
Failure surface (부모 프로세스 관찰 지점): packages/base/src/manager/child-process.manager.ts:107-120
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);
});
이 문자열 (Process killed by signal ${signal} (code: ${code})) 이 그대로 클러스터의 대표 에러 메시지가 됐다. code: null 인 이유는 프로세스가 정상 exit code 를 반환하지 못하고 signal 로 종료됐기 때문 — Node.js 문서에 명시된 signal-terminated 시 code=null, signal='SIGSEGV' 관례다.
기대 동작 vs 실제 동작:
- 기대:
Preprocessor.process()가 정상 반환 →photo_count(),timelapse_video_frame_count()등에서 결과 수집 → IPCsuccess:true응답 → SQS 메시지 delete. - 실제:
process()(또는 인접 native 메서드) 내부에서 native 크래시 → 자식 프로세스 SIGSEGV →ChildProcessManager가 pending IPC 를 reject →BaseService::handlingMessageErrors로 전파 → 메시지 실패 처리.
stdioMode: 'inherit' (default) 이므로 native 라이브러리가 stderr 로 남긴 crash trace 는 컨테이너 로그로 흘렀을 뿐 Datadog 로그 파이프라인에는 잡히지 않았다 — Node.js 레벨 로그만 남는다.
Log Evidence#
Datadog 쿼리 (cluster URL 그대로):
service:cupixworks-capture-preprocessor-agent status:error @environment:production "Process killed by signal SIGSEGV (code: null)"
주변 로그 확인 쿼리:
service:cupixworks-capture-preprocessor-agent
(시간 범위: 2026-07-15T18:54:00Z ~ 2026-07-15T18:56:00Z)
핵심 로그 시퀀스 (KST 기준, 시간 오름차순):
03:54:41 info JobManager::loadJob | begin - job id: 1199825
03:54:42 info PreprocessorService::loadCaptureResources | capture resource count: 1
03:54:42 info PreprocessorService::loadPanos | pano count: 0
03:54:42 info PreprocessorService::loadVideos | video count: 1
03:54:42 info JobManager::loadJob | end - job id: 1199825
03:54:53 error ChildProcessManager::setupEventHandlers | Child process exited
03:54:53 error ChildProcessManager::setupEventHandlers | Child process closed
03:54:53 error Process killed by signal SIGSEGV (code: null)
03:54:53 warn Process killed by signal SIGSEGV (code: null)
03:54:53 warn CupixAuth::handleError | Undefined response: {"stack":"Error: Process killed by signal SIGSEGV (code: null)\n at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:1459:33)...","message":"Process killed by signal SIGSEGV (code: null)"}
03:54:53 info AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/002596530511/cupix-capture-preprocessor-agent-production
03:54:53 info AwsQueueManager::deleteMessage | end - message id: 740624a0-f9f4-46e0-aca5-bd703e088157
03:54:58 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/735964
03:54:58 error BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"740624a0-f9f4-46e0-aca5-bd703e088157","Attributes":{"ApproximateReceiveCount":"1"}}}
03:54:58 info BaseService::runByMessage | id: 1199831 ← 다음 job 자동 재개
Datadog 14일 retention 내 SIGSEGV 발생 건 (service:cupixworks-capture-preprocessor-agent "SIGSEGV"):
Found 3 logs (모두 같은 03:54:53 이벤트의 error/warn 중복 로그).
/tmp/workspace/735964 — crash 당시 처리 중이던 capture 의 로컬 워크스페이스 경로. capture ID 는 735964 로 추정되나, 크래시로 job-scoped 로그가 flush 되기 전에 종료돼 runByMessage 나 loadJob 라인에 명시적으로 나오지 않는다 — cleanup path 가 유일한 단서다 (uncertain — needs verification: 실제 capture 735964 인지 확인하려면 Kibana captures 인덱스나 tesla job_id -> capture_id 매핑 조회 필요).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Native N-API addon scenemapperutils_api.node 내부에서 SIGSEGV 발생 |
스택이 child_process internals 로만 이어지고 JS 프레임 없음; 자식 프로세스가 require('scenemapperutils_api.node') 후 Preprocessor.process() 호출 (preprocessor.process.ts:263-289); Node.js 자체가 SIGSEGV 를 self-raise 하는 경우는 매우 드묾 |
— | Confirmed |
| H2 | OOM killer (SIGKILL) 로 착각 | — | 시그널이 명시적으로 SIGSEGV (signal 11) 이고 SIGKILL (signal 9) 이 아님; OOM 은 container-level 로 오면 보통 SIGKILL 로 표시됨 |
Rejected |
| H3 | JS 레벨 unhandled exception 이 자식을 죽였다 | — | 스택에 JS 사용자 프레임이 없음 (ChildProcess.<anonymous>, emit, maybeClose, _handle.onexit 만); close 이벤트에서 signal='SIGSEGV' 로 도착 — Node.js 프로세스 자체가 signal 로 종료됐다는 의미 |
Rejected |
| H4 | executeTimeoutMs timeout 으로 SIGKILL 발송 후 SIGSEGV 로 오탐 |
ChildProcessManager 는 executeTimeoutMs 초과 시 this.process.kill('SIGKILL') 만 호출 (child-process.manager.ts:221) |
signal 이 SIGKILL 이 아니라 SIGSEGV; SIGKILL 로 죽였으면 로그가 Process killed by signal SIGKILL 로 남아야 함 |
Rejected |
| H5 | Deploy 로 인한 회귀 (native 라이브러리 버전 변경) | Datadog 로그만으로는 확인 불가 | 14일 retention 내 유일 발생 — 배포 회귀라면 반복될 것으로 예상 (unverified — 배포 이력 대조 필요) | Inconclusive |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 단발성 SIGSEGV 이고 서비스가 자동 회복해 다음 job 을 계속 처리했다. SQS 메시지는 실패 처리되어 delete 됐으므로 해당 job 은 재큐잉 로직에 따라 재시도 대상이 될 수 있음 — worker/queue 측 재시도 정책 (DLQ, retry count) 확인이 필요하지만 (uncertain — needs verification) 이는 이번 클러스터의 root cause 밖.
단기 개선 (1주 이내)#
- Crash 재현/디버깅 가시성 확보:
ChildProcessManager를 native crash 가능성이 있는 workload 에 대해서는stdioMode: 'pipe'로 시작하도록 옵션을 넘겨 native 라이브러리의 stderr (V8/GLibc backtrace, address, segfault reason) 를 부모 프로세스가 캡처해 Datadog 에 흘리도록 조정. 현재 코드는 이미pipe지원 (packages/base/src/manager/child-process.manager.ts:29-30, 70-71, 138-141) —PreprocessorManager생성자에서new ChildProcessManager(__dirname, 'process/preprocessor.process', { stdioMode: 'pipe' })로 바꾸는 것만으로 활성화 가능. 근거: 재발 시 SIGSEGV 발생 위치와 입력 파일을 특정할 최소 evidence 를 남기기 위함. - 실패 메시지의 입력 컨텍스트 로깅 강화: SQS 메시지에서 파싱한
job_id/capture_id를BaseService::runByMessage진입 직후에 반드시info로 출력하도록 확인. 현재 로그를 보면 crash 직전에runByMessage | id: <job_id>는 남지만, 해당 job 에 매핑되는 capture ID 는 별도 lookup 이후에만 나와 crash 로 truncate 되면 유실됨.sqsMessage파싱 직후 attribute 를 flush 하도록 로깅 순서 검토.
장기 개선 (재발 방지)#
- Scenemapperutils native 안정성 개선 절차: SIGSEGV 가 반복 발생하기 시작하면 (1) native lib version (
lib/scenemapperutils/version.txt) 을 Datadog tag 로 emit 하고, (2) 크래시된 입력 (video/photo file) 을 격리 저장소로 백업해 로컬 재현이 가능하도록 pipeline 을 확장. 근거: native 코드는 JS 수정으로 우회 불가 — 재현 케이스가 없으면 근본 수정 불가능. - Circuit breaker / retry budget: 같은 자식 프로세스 (또는 같은 input 파라미터) 에서 SIGSEGV 가 반복되면 자동 격리하고 Slack 알림을 발화하는 감시 로직. 현재는 단발 실패 후 재큐잉 → 무한 crash-loop 가능성 있음 (uncertain — needs verification: DLQ/maxReceiveCount 설정 확인 필요).
Monitoring#
- 지표 아이디어: SIGSEGV / non-zero signal exit 빈도, agent 별 job 실패율.
- Release dashboard 용 Datadog timeseries 쿼리 (
writing-datadog-monitoring-queries룰 준수 — pipe/stats/monitor-only 문법 없음):
sum:datadog.estimated_usage.logs.ingested_events{service:cupixworks-capture-preprocessor-agent,status:error}.as_count()
SIGSEGV 문자열이 실제 대표적으로 잡히는 쿼리 (log-based metric 을 만들어 두는 편이 안정적이지만, 임시 관찰용):
sum:logs.hits{service:cupixworks-capture-preprocessor-agent,status:error,@message:*SIGSEGV*}.as_count()
BaseService::handlingMessageErrors 로 잡히는 상위 실패 카운트:
sum:logs.hits{service:cupixworks-capture-preprocessor-agent,@message:*handlingMessageErrors*}.as_count()
- 알림: 최근 10분 SIGSEGV 카운트 > 0 이면 warning, > 3 이면 alert. 알림 embed 는 monitor 쿼리 (
| stats) 를 별도로 정의해야 하며, 위 timeseries 쿼리와 혼동 금지.
Risk Assessment#
- Risk level: low (14일 내 단발성, 자동 회복 확인, blast radius = 단일 SQS 메시지).
- 예상 복잡도: trivial (JS 레벨 수정 없음; native crash 근본 원인은 재현 케이스가 없어 즉시 수정 불가; 개선 항목은 관찰성 강화에 국한).