Node.js 재택근무 환경에서 발생한 메모리 누수 추적기
문제 발견
코로나로 인해 급하게 재택근무로 전환한 지 일주일째, 프로덕션 API 서버의 메모리 사용량이 지속적으로 증가하는 이슈가 발견되었다. PM2 모니터링 결과 약 6시간마다 메모리가 1.5GB를 초과하며 재시작되고 있었다.
원격 디버깅 환경 구축
사무실 서버에 직접 접근할 수 없는 상황이라 원격으로 힙 덤프를 수집할 수 있는 환경을 먼저 구축했다.
const heapdump = require('heapdump');
const path = require('path');
app.post('/admin/heapdump', authenticate, (req, res) => {
const filename = path.join('/tmp', `heapdump-${Date.now()}.heapsnapshot`);
heapdump.writeSnapshot(filename, (err) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ filename });
});
});
원인 분석
Chrome DevTools로 heapsnapshot 파일을 분석한 결과, Socket.io 이벤트 리스너가 정리되지 않고 계속 쌓이고 있었다. 클라이언트 연결 해제 시 removeAllListeners를 호출하지 않았던 게 원인이었다.
// 문제가 있던 코드
io.on('connection', (socket) => {
socket.on('subscribe', (room) => {
socket.join(room);
pubSubClient.subscribe(room, (message) => {
socket.emit('update', message);
});
});
});
// 수정한 코드
io.on('connection', (socket) => {
const handlers = new Map();
socket.on('subscribe', (room) => {
socket.join(room);
const handler = (message) => socket.emit('update', message);
handlers.set(room, handler);
pubSubClient.subscribe(room, handler);
});
socket.on('disconnect', () => {
handlers.forEach((handler, room) => {
pubSubClient.removeListener(room, handler);
});
handlers.clear();
});
});
배포 후 결과
수정 배포 후 24시간 동안 메모리 사용량을 모니터링했다. 더 이상 메모리가 증가하지 않고 400~500MB 사이에서 안정적으로 유지되었다. clinic.js로 추가 검증도 진행했다.
재택근무 환경에서도 적절한 도구만 있으면 충분히 디버깅이 가능하다는 것을 확인했다.