본문으로 바로가기
development2026년 4월 3일·조회 76

Node.js 프로세스 Hang 장애 해결: 고아 프로세스 누적 문제 분석

자식 프로세스 관리 실패로 인한 시스템 리소스 고갈 및 해결 방법

SP

SpacePlanning

SpacePlanning AI Team

# 들어가며 멀티 프로세스 환경에서 작업하다 보면 예상치 못한 hang 현상을 경험하게 됩니다. 특히 부모 프로세스가 여러 자식 프로세스를 spawn하는 구조에서는 프로세스 생명주기 관리가 제대로 이루어지지 않을 경우, 시스템 리소스가 점진적으로 고갈되는 문제가 발생할 수 있습니다. 이번 글에서는 실제 운영 환경에서 발생한 Node.js 프로세스 hang 장애 사례를 통해, 고아 프로세스(Orphan Process) 누적 문제의 원인과 해결 방법을 공유합니다. ## 문제 상황: 반복적인 Hang 현상 ### 증상 - 특정 작업 처리 중 주기적으로 프로세스가 멈추는 현상 발생 - CPU 사용률 0%로 떨어지고 5분 이상 응답 없음 - SSH 연결 타임아웃 및 인증 실패 에러 반복 - 시스템 모니터링 결과 Node.js 프로세스 100개 이상 실행 중 (정상: 10개 미만) ### 프로세스 구조 일반적인 멀티 프로세스 아키텍처에서 부모 프로세스는 여러 자식 프로세스를 생성합니다: ``` Worker Process (부모) └── CLI Session ├── MCP Server 1 (Node.js) ├── MCP Server 2 (Node.js) ├── SSH Manager (Node.js) └── Other Services... ``` ## 근본 원인: 고아 프로세스 누적 ### 고아 프로세스 발생 메커니즘 1. **Hang 감지 및 강제 종료**: 모니터링 시스템이 CPU 0% + 출력 없음 5분 감지 시 프로세스 강제 종료 2. **불완전한 정리**: 부모 프로세스만 kill되고 자식 프로세스는 고아 상태로 남음 3. **점진적 누적**: 2-3일간 hang 발생 시마다 고아 프로세스가 계속 증가 4. **리소스 경합**: 다수의 고아 프로세스가 동시에 SSH 연결 등 리소스 접근 시도 ### 리소스 경합으로 인한 악순환 고아 프로세스가 누적되면서 다음과 같은 문제가 연쇄적으로 발생했습니다: - **네트워크 연결 경합**: 수십 개의 SSH 클라이언트가 동시 연결 시도 - **타임아웃 증가**: `ETIMEDOUT`, `ECONNREFUSED` 에러 빈발 - **새로운 작업 지연**: 정상 프로세스도 리소스 부족으로 hang 발생 - **추가 고아 프로세스 생성**: 악순환 반복 ## 해결 방법 ### 1. 고아 프로세스 식별 Windows 환경에서 PowerShell을 사용한 식별 방법: ```powershell # Node.js 프로세스 목록 확인 Get-Process node | Select-Object Id, ProcessName, StartTime, CPU # 부모 프로세스가 없는 고아 프로세스 찾기 Get-WmiObject Win32_Process -Filter "Name='node.exe'" | Where-Object { (Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue) -eq $null } ``` Linux/Mac 환경: ```bash # 고아 프로세스 확인 (PPID=1은 init/systemd가 입양한 경우) ps -eo pid,ppid,cmd | grep node | grep -v grep ``` ### 2. 고아 프로세스 정리 안전한 정리 스크립트 예시 (PowerShell): ```powershell # 특정 기준 이상 오래된 Node 프로세스 종료 $threshold = (Get-Date).AddHours(-6) Get-Process node | Where-Object { $_.StartTime -lt $threshold -and $_.CPU -eq 0 } | ForEach-Object { Write-Host "Killing process $($_.Id) started at $($_.StartTime)" Stop-Process -Id $_.Id -Force } ``` ### 3. 예방: 프로세스 트리 관리 #### 개선된 프로세스 종료 로직 부모 프로세스 종료 시 자식 프로세스까지 함께 정리: ```javascript // Node.js 예시 const { spawn } = require('child_process'); const psTree = require('ps-tree'); function killProcessTree(pid) { return new Promise((resolve, reject) => { psTree(pid, (err, children) => { if (err) return reject(err); // 자식 프로세스부터 종료 const childPids = children.map(p => p.PID); childPids.forEach(childPid => { try { process.kill(childPid, 'SIGTERM'); } catch (e) { console.error(`Failed to kill ${childPid}:`, e); } }); // 부모 프로세스 종료 setTimeout(() => { try { process.kill(pid, 'SIGKILL'); } catch (e) {} resolve(); }, 1000); }); }); } ``` #### Process Group 활용 (Linux/Mac) ```javascript const child = spawn('command', args, { detached: true, // 새로운 프로세스 그룹 생성 stdio: 'inherit' }); // 프로세스 그룹 전체 종료 process.kill(-child.pid, 'SIGTERM'); ``` ### 4. 정기 모니터링 및 자동화 #### Cron/Task Scheduler 설정 ```bash # Linux crontab 예시: 6시간마다 고아 프로세스 정리 0 */6 * * * /path/to/cleanup_orphan_processes.sh # 매일 아침 리소스 상태 점검 0 9 * * * /path/to/check_system_health.sh ``` ## 교훈 및 모범 사례 ### 1. 명시적인 리소스 정리 ```javascript class ProcessManager { constructor() { this.childProcesses = new Set(); // 프로세스 종료 시 cleanup process.on('exit', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); process.on('SIGINT', () => this.cleanup()); } spawn(command, args) { const child = spawn(command, args); this.childProcesses.add(child); child.on('exit', () => { this.childProcesses.delete(child); }); return child; } cleanup() { this.childProcesses.forEach(child => { try { child.kill('SIGTERM'); } catch (e) {} }); } } ``` ### 2. 타임아웃 및 재시도 로직 ```javascript const timeout = (promise, ms) => { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ) ]); }; // 사용 예시 await timeout(connectSSH(), 30000).catch(err => { console.error('Connection timeout:', err); // cleanup logic }); ``` ### 3. 모니터링 및 알림 - 프로세스 수 임계값 설정 (예: 50개 이상 시 알림) - CPU/메모리 사용률 모니터링 - 주기적인 헬스체크 ## 결론 고아 프로세스 누적은 겉으로 드러나지 않다가 임계점을 넘으면 시스템 전체에 영향을 미치는 전형적인 리소스 누수 문제입니다. 이번 사례를 통해 얻은 핵심 교훈은: 1. **프로세스 생명주기 관리의 중요성**: spawn한 프로세스는 반드시 정리 책임도 함께 2. **방어적 프로그래밍**: 예외 상황에서도 리소스가 정리되도록 cleanup 로직 구현 3. **모니터링 자동화**: 문제가 커지기 전에 감지할 수 있는 체계 구축 프로세스 관리는 멀티 프로세스 환경의 기본이지만, 실제 운영에서는 놓치기 쉬운 부분입니다. 주기적인 점검과 자동화된 정리 메커니즘을 통해 안정적인 시스템을 유지하시기 바랍니다.
#Node.js#프로세스관리#장애해결#고아프로세스#시스템운영#DevOps
공유하기:

이 주제에 대해 더 알아보고 싶으신가요?

프로젝트 상담을 통해 맞춤형 솔루션을 제안받으세요.