المستوى المطلوب: متوسط — يفترض إنك مرتاح مع Node.js و JavaScript، وعملت debugging بـ console.log أو DevTools قبل كده على الأقل.
Memory Leak في Node.js: ازاي تكشفها بـ Heap Snapshot قبل ما السيرفر يقع
لو RAM السيرفر عندك بيكبر من 240MB لـ 2.8GB في 4 أيام، الـ Heap Snapshot هيوريك بالظبط الـ object اللي بيتراكم في الذاكرة. المقال ده هياخدك من فهم الـ leak، لقراءة الـ snapshot، لحد إصلاحها بمثال من إنتاج فعلي.
المشكلة باختصار
Node.js شغّال على V8، والـ V8 فيه garbage collector بيحرر الذاكرة الخاصة بأي object مفيش حد بيشاور عليه. لو في object فضل متشاور عليه (حتى لو انت مش محتاجه فعلاً)، الـ GC مش هيلمسه. مع الوقت، عدد الـ objects المعلّقة دي بيكبر، استهلاك الذاكرة بيطلع، ولحد ما السيرفر يضرب OOM (Out Of Memory) ويقع.
المشكلة دي شائعة في الحالات دي:
- Caches بدون حد أقصى ولا TTL.
- Event listeners بتتسجّل في كل request وما بتتشالش.
- Closures بتمسك references لـ data كبير بدون داعي.
- Global Maps أو Arrays بتتراكم فيها entries على طول.
تخيّل الموضوع كده الأول
تخيّل إنك عندك دولاب في المطبخ، وكل ما تطبخ بتحط فيه طبق جديد بس عمرك ما بتغسل القديم. أول أسبوع الدولاب فيه مكان. تاني أسبوع بدأ يضيق. الشهر التالت، الباب مش بيقفل، وفي الآخر الدولاب نفسه بيقع. الـ memory leak بنفس الفكرة بالظبط: انت بتحفظ objects جديدة في الذاكرة، بس مفيش حد بيشيل القديمة، حتى لو ولا حد محتاجها.
التعريف العلمي: Memory leak هي حالة بتفضل فيها مساحة من الـ heap محجوزة لـ object مش هيُستخدم مرة تانية، لأن في reference نشط (من root) بيمنع الـ garbage collector من تحريرها. في V8، الـ roots بتشمل الـ global object، الـ stack، والـ closures المعلّقة.
السيناريو الفعلي
سيرفر Express بيخدم 50K request/يوم على instance بـ 2GB RAM. أول يوم: الذاكرة 240MB. اليوم الرابع: 2.8GB والسيرفر بيتعاد تشغيله أوتوماتيك كل ساعتين بسبب OOM. الـ logs نظيفة، الـ CPU 12% بس، والـ requests كلها 200 OK. المشكلة في الذاكرة لوحدها، ومش ظاهرة في أي metric غير الـ RSS.
الكود اللي بيسبّب التسريب
ده مثال مبسّط لكود حقيقي بيسرّب ذاكرة:
const cache = new Map();
app.get('/user/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
// المشكلة: الـ key بيضم timestamp الطلب، يعني عمره ما هيتعاد استخدامه
cache.set(`${req.params.id}-${Date.now()}`, user);
res.json(user);
});
كل request بيضيف entry جديد في الـ Map. مفيش cache.delete، ومفيش حد أقصى. بعد 50K request، فيه 50K user object معلّقين في الذاكرة بدون أي داعي. ده اللي بيحصل فعلاً في كثير من الـ codebases اللي شفتها.