مستوى المقال: متوسط | زمن القراءة: 7 دقائق تقريبًا
لو سيرفر Node.js عندك بيقع OOM وقت ما حد يرفع ملف 500MB، والذاكرة بتطلع من 180MB لـ 4GB في أقل من نصف دقيقة، المشكلة مش حجم الملف ولا قلّة الرام. المشكلة إن الكود بيتجاهل backpressure في الستريم.
Backpressure في Node.js Streams بالعربي: السبب الحقيقي لانفجار الذاكرة
المشكلة باختصار
الستريم في Node.js عبارة عن أنبوب بين مصدر بيانات (قراءة) ومستقبل (كتابة). لو المصدر أسرع من المستقبل، البيانات بتتراكم في buffer داخلي في الذاكرة. لو ما حدش بيوقف المصدر مؤقتًا لحد ما المستقبل يلحقه، الـ buffer بيكبر بدون سقف عملي وبيستهلك رام السيرفر كلها.
الافتراض هنا: شغّال على Node.js 18+ مع اتصال شبكي عادي وسيرفر بـ 1–2GB رام.
مثال للمبتدئين: المطبخ والنادل
تخيّل مطبخ بيخرج 100 طبق في الساعة، والنادل بيقدر يوصّل 10 أطباق في الساعة. لو الشيف فضل يطبخ بدون ما ياخد باله من النادل، الأطباق هتتراكم على الكاونتر. بعد ساعة هيكون فيه 90 طبق منتظر، وبعد ساعتين 180 طبق، لحد ما الكاونتر يقع.
الحل المنطقي: الشيف يبص على الكاونتر، ولما يلاقي 10 أطباق منتظرة يوقف الطبخ، ويكمّل لما النادل يفضّى. ده بالظبط معنى backpressure.
التعريف الدقيق
الـ Writable stream في Node.js عنده خاصية اسمها highWaterMark (الافتراضي 16KB للستريم العادي، 64KB لستريم الملفات). دي العتبة اللي ساعتها writable.write(chunk) بترجّع false، يعني "أنا اتملّيت، اوقف بعتلي".
المسؤولية على الـ Readable stream إنه يلتقط الإشارة دي ويوقف القراءة (pause)، ويستنى حدث drain من الـ Writable عشان يكمّل. لو الكود بيتجاهل قيمة الـ return من write()، البيانات بتتدفق بدون توقّف وبتتكدّس في buffer داخل الذاكرة.
الكود الغلط: write() بدون مراقبة
// ❌ بيشيل الذاكرة فوق 4GB لو المصدر سريع
const fs = require('fs');
const http = require('http');
http.createServer((req, res) => {
const dest = fs.createWriteStream('./upload.bin');
req.on('data', (chunk) => {
dest.write(chunk); // قيمة الـ return مش متفحوصة
});
req.on('end', () => {
dest.end();
res.end('OK');
});
}).listen(3000);الكود ده شكله سليم، لكنه بيستهلك ذاكرة بشكل خطير لما الـ disk أبطأ من الشبكة. كل chunk بيتكدّس في buffer Node.js الداخلي.