لو السيرفر بتاعك بياخد 200MB أول ما يشتغل، وبعد 6 ساعات بقى 1.4GB، انت مش محتاج RAM زيادة. عندك Memory Leak. الـ heap snapshot هيبيّنلك السبب بالظبط في أقل من 10 دقايق بدون ما تعدّل سطر كود واحد في production.
كشف Memory Leak في Node.js بـ Heap Snapshot
المشكلة باختصار
التطبيق شغّال كويس أول ساعة. بعد كده استهلاك الذاكرة بيتزايد بشكل خطي لحد ما الـ process يضرب الـ OOM (Out Of Memory) ويعمل restart. الحل اللي ناس كتير بتروح ليه: زيادة الـ RAM أو cron job يعمل restart كل ساعتين. ده مش حل، ده تأجيل. في الإنتاج الحقيقي الـ leak بيكبر مع الـ traffic وبيكسر التطبيق في وقت الذروة بالظبط.
مثال للمبتدئين: غرفة بتمتلي بكراتين متفتحتش
تخيّل عندك غرفة بتدخلها كل يوم 50 كرتونة. اللي بتخلص منها بترميها في الزبالة. المفروض بعد شهر الغرفة فاضية. لكن اللي بيحصل: الغرفة بتمتلي للسقف. السبب: في كرتونة كبيرة في الركن مكتوب عليها "أرشيف" بتحط فيها ورقة صغيرة فيها رقم كل كرتونة قبل ما ترميها. الزبّال لمّا بيشوف الكرتونة في الزبالة، بيلاقي إن في حد لسه فاكر رقمها في الأرشيف، فيرجّعها مكانها لإنه بيفترض إنها مهمة.
ده بالظبط اللي بيحصل في Node.js. الـ Garbage Collector (الزبّال) ما بيقدرش يحذف أي كائن طول ما في reference واحد ليه من مكان "عايش". كائن صغير زي Map ممكن يمسك مليون كائن تاني، وانت مش حاسس بإنك بتسرّب ذاكرة.
التعريف العلمي بدقة
الـ V8 engine بيقسّم الـ heap لجيلين أساسيين: Young generation للكائنات الجديدة (بيتنضّفوا بسرعة بـ Scavenger GC)، وOld generation للكائنات اللي عاشت أكتر من cycle (بيتنضّفوا بـ Mark-Sweep-Compact). الـ leak بيحصل لمّا كائنات تتنقل لـ Old generation وتفضل عندها reference path وصولًا للـ root (مثل global object، closure حيّ، event emitter، أو cache بدون حد). الـ GC بيمشي في الـ graph وبيلاقيهم "reachable"، فما بيحذفهمش. المقياس المهم اسمه Retained Size: إجمالي الذاكرة اللي هتتحرر فعلًا لو الكائن ده اتقطع reference بتاعه.
الأنماط الأربعة اللي بتسبب 90% من الـ leaks
- Closures في setInterval بدون clearInterval: الـ closure ماسك متغير كبير، والـ interval فاضل شغّال طول حياة الـ process.
- Event listeners ما اتشالتش:
emitter.on('data', handler)بيتضاف مع كل request جديد بدونoff. - Cache بدون TTL أو max size:
const cache = new Map()بتكبر طول النهار. - Arrays عمومية بتتعمل push بدون pop: زي
logs.push(req)على array global.
الـ playbook في 5 خطوات (three-snapshot technique)
- شغّل التطبيق وسيبه ياخد warmup لمدة دقيقتين.
- خد Snapshot 1 (baseline).
- اعمل load صناعي بـ
autocannonأو (1000 request مثلاً)، وبعدين استنى 30 ثانية لـ GC يخلّص شغله.