재택근무 환경에서 Node.js 메모리 누수 디버깅하기
문제 상황
3월 초 전사 재택근무 전환 이후, 기존에는 문제없던 Node.js API 서버의 메모리 사용량이 서서히 증가하는 현상이 관찰됐다. 대략 48시간마다 재시작이 필요한 수준이었다.
트래픽이 늘어난 것도 아니었고, 최근 배포도 없었다. 다만 WebSocket 연결 수가 평소보다 2배 이상 증가했다는 점이 유일한 변화였다.
디버깅 과정
1. heapdump 수집
먼저 heapdump 패키지를 설치하고 프로덕션 서버에서 일정 시간 간격으로 힙 스냅샷을 수집했다.
const heapdump = require('heapdump');
const path = require('path');
// 6시간마다 자동으로 힙 덤프 생성
setInterval(() => {
const filename = path.join('/tmp', `heap-${Date.now()}.heapsnapshot`);
heapdump.writeSnapshot(filename);
console.log('Heap snapshot written:', filename);
}, 6 * 60 * 60 * 1000);
2. Chrome DevTools로 분석
수집한 heapsnapshot 파일을 Chrome DevTools의 Memory 탭에서 비교 분석했다. Comparison 뷰에서 시간대별 스냅샷을 비교하니, Buffer 객체와 특정 클로저가 계속 증가하고 있었다.
3. 원인 파악
WebSocket 연결 핸들러 코드를 다시 살펴보니 문제를 발견했다.
// 문제가 있던 코드
wss.on('connection', (ws) => {
const interval = setInterval(() => {
ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
ws.on('message', (data) => {
// 메시지 처리
});
});
close 이벤트 핸들러에서 clearInterval을 호출하지 않아, 연결이 끊어져도 interval이 계속 실행되고 있었다. 재택근무로 인해 사용자들이 브라우저를 켜둔 채 자리를 비우는 경우가 많아지면서, 좀비 연결이 누적된 것이다.
해결
wss.on('connection', (ws) => {
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
ws.on('close', () => {
clearInterval(interval);
});
ws.on('message', (data) => {
// 메시지 처리
});
});
배포 후 48시간 경과했지만 메모리 사용량이 안정적으로 유지되고 있다.
교훈
- 예상치 못한 사용 패턴 변화가 숨어있던 버그를 드러낼 수 있다
setInterval,setTimeout사용 시 반드시 cleanup 로직 작성- heapdump는 프로덕션 환경에서도 충분히 실용적