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
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',
});
});
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));
});
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 });
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
};
}
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();
});
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 });
});
Mở file .heapsnapshot trong Chrome DevTools:
- Chrome → F12 → Memory tab
- Load snapshot
- Sort by "Retained Size"
- 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);
Quick Checklist
- [ ] Event listeners có
removeListener/offkhi 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)