DEV Community

EME GUG
EME GUG

Posted on

Cách debug memory leak trong Node.js

App Node.js chạy vài ngày thì restart? Memory tăng đều? Có thể bạn đang bị memory leak. Đây là cách mình debug.

Dấu hiệu memory leak

# Theo dõi memory
node --max-old-space-size=512 app.js

# Process memory tăng đều theo thời gian
# RSS (Resident Set Size) không giảm sau GC
Enter fullscreen mode Exit fullscreen mode

Bước 1: Xác nhận leak

// Thêm endpoint này vào app
app.get('/health/memory', (req, res) => {
    const mem = process.memoryUsage();
    res.json({
        rss: (mem.rss / 1024 / 1024).toFixed(2) + ' MB',
        heapUsed: (mem.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
        heapTotal: (mem.heapTotal / 1024 / 1024).toFixed(2) + ' MB',
        external: (mem.external / 1024 / 1024).toFixed(2) + ' MB',
    });
});
Enter fullscreen mode Exit fullscreen mode

Gọi endpoint này mỗi phút. Nếu heapUsed tăng đều → leak.

Bước 2: Tìm nguồn leak

Nguyên nhân phổ biến

1. Event listeners không remove

// Leak: mỗi request thêm 1 listener, không bao giờ remove
app.get('/stream', (req, res) => {
    emitter.on('data', (data) => res.write(data));
});

// Fix: remove khi connection đóng
app.get('/stream', (req, res) => {
    const handler = (data) => res.write(data);
    emitter.on('data', handler);
    req.on('close', () => emitter.off('data', handler));
});
Enter fullscreen mode Exit fullscreen mode

2. Cache không giới hạn

// Leak: cache grow mãi mãi
const cache = {};
function getData(key) {
    if (!cache[key]) cache[key] = fetchFromDB(key);
    return cache[key];
}

// Fix: dùng LRU cache
const LRU = require('lru-cache');
const cache = new LRU({ max: 500 });
Enter fullscreen mode Exit fullscreen mode

3. Closures giữ reference

// Leak: closure giữ reference đến toàn bộ `data`
function processData() {
    const data = loadHugeFile();  // 100MB
    return function getSize() {
        return data.length;  // Giữ `data` trong memory mãi
    };
}
Enter fullscreen mode Exit fullscreen mode

4. Global variables

// Leak: mỗi request push vào array global
const logs = [];
app.use((req, res, next) => {
    logs.push({ url: req.url, time: Date.now() });
    next();
});
Enter fullscreen mode Exit fullscreen mode

Bước 3: Heap Snapshot

const v8 = require('v8');
const fs = require('fs');

app.get('/debug/heapdump', (req, res) => {
    const snapshotStream = v8.writeHeapSnapshot();
    res.json({ file: snapshotStream });
});
Enter fullscreen mode Exit fullscreen mode

Mở file .heapsnapshot trong Chrome DevTools:

  1. Chrome → F12 → Memory tab
  2. Load snapshot
  3. Sort by "Retained Size"
  4. Tìm object chiếm nhiều memory nhất

Bước 4: Theo dõi trong production

// Log memory mỗi 30 giây
setInterval(() => {
    const mem = process.memoryUsage();
    const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(1);
    if (mem.heapUsed > 400 * 1024 * 1024) {
        console.warn(`HIGH MEMORY: ${heapMB}MB`);
    }
}, 30000);
Enter fullscreen mode Exit fullscreen mode

Quick Checklist

  • [ ] Event listeners có removeListener / off khi không cần?
  • [ ] Cache có giới hạn size (LRU)?
  • [ ] Closures có giữ reference không cần thiết?
  • [ ] Global arrays/objects có clear định kỳ?
  • [ ] Streams có properly close/destroy?
  • [ ] setTimeout/setInterval có clearTimeout/clearInterval?

Bạn đã gặp memory leak chưa? Nguyên nhân là gì?

Top comments (0)