Node.js 프로세스 메모리 누수 추적기
문제 상황
사내 API 서버가 운영 중 메모리 사용량이 계속 증가하다가 결국 프로세스가 종료되는 현상이 반복됐다. PM2 로그를 확인하니 약 12시간마다 OOM(Out of Memory) 에러가 발생하고 있었다.
메모리 스냅샷 수집
운영 환경에서 직접 디버깅할 수 없어 스테이징 환경에서 부하 테스트를 진행하며 힙 스냅샷을 수집했다.
const v8 = require('v8');
const fs = require('fs');
function takeSnapshot() {
const filename = `heap-${Date.now()}.heapsnapshot`;
const snapshot = v8.writeHeapSnapshot(filename);
console.log(`Snapshot written to ${snapshot}`);
}
// 1시간마다 스냅샷 저장
setInterval(takeSnapshot, 60 * 60 * 1000);
원인 분석
Chrome DevTools에서 스냅샷을 비교 분석한 결과, EventEmitter 리스너가 계속 누적되고 있었다. WebSocket 연결 처리 코드에서 removeListener를 호출하지 않아 발생한 문제였다.
// 문제 코드
wss.on('connection', (ws) => {
ws.on('message', handleMessage);
// disconnect 시 리스너 제거 안 함
});
// 수정 코드
wss.on('connection', (ws) => {
const messageHandler = (msg) => handleMessage(ws, msg);
ws.on('message', messageHandler);
ws.on('close', () => {
ws.removeListener('message', messageHandler);
});
});
모니터링 개선
재발 방지를 위해 메모리 사용량 모니터링을 추가했다.
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`
});
배포 후 일주일간 모니터링한 결과 메모리 사용량이 안정적으로 유지되는 것을 확인했다. 이벤트 리스너 관리의 중요성을 다시 한번 느꼈다.