أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Backpressure في Node.js Streams: ليه السيرفر بياكل 4GB رام عند رفع ملف 500MB

📅 ٢٨ أبريل ٢٠٢٦⏱ 4 دقائق قراءة
Backpressure في Node.js Streams: ليه السيرفر بياكل 4GB رام عند رفع ملف 500MB

مستوى المقال: متوسط  |  زمن القراءة: 7 دقائق تقريبًا

لو سيرفر Node.js عندك بيقع OOM وقت ما حد يرفع ملف 500MB، والذاكرة بتطلع من 180MB لـ 4GB في أقل من نصف دقيقة، المشكلة مش حجم الملف ولا قلّة الرام. المشكلة إن الكود بيتجاهل backpressure في الستريم.

سيرفرات Node.js في غرفة بيانات تعالج تدفقات بيانات كبيرة وذاكرة تحت الضغط

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() بدون مراقبة

JavaScript
// ❌ بيشيل الذاكرة فوق 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 الداخلي.

الكود الصح: pipeline()

JavaScript
// ✅ بيدير backpressure تلقائيًا + بيمسح الموارد عند الخطأ
const { pipeline } = require('stream/promises');
const fs = require('fs');
const http = require('http');

http.createServer(async (req, res) => {
  try {
    await pipeline(
      req,
      fs.createWriteStream('./upload.bin')
    );
    res.end('OK');
  } catch (err) {
    res.statusCode = 500;
    res.end('Upload failed: ' + err.message);
  }
}).listen(3000);

الفرق إن pipeline():

  • بيراقب قيمة write() ويعمل pause/resume للـ Readable تلقائيًا.
  • بيقفل كل الستريمات لو حصل خطأ في أي جزء (ده اللي pipe() القديم ما كانش بيعمله، وده سبب memory leak مشهور).
  • بيرجّع Promise، فالـ async/await بيشتغل طبيعي مع try/catch.

الأرقام الفعلية

قياس على سيرفر Node.js 20 بـ 1GB رام، رفع ملف 1GB من client بسرعة 100MB/s، disk بيكتب بسرعة 30MB/s:

  • قبل (write بدون مراقبة): الذاكرة طلعت لـ 1.7GB، البروسيس وقع OOM في الثانية 22.
  • بعد (pipeline): الذاكرة استقرّت عند 285MB، الرفع كمل في 38 ثانية بدون أخطاء.

الفرق 6 أضعاف في استهلاك الذاكرة، ومن "السيرفر بيقع" لـ "السيرفر شغّال طبيعي".

شاشة لابتوب تعرض رسم بياني لاستهلاك الذاكرة في عملية رفع ملف قبل وبعد ضبط backpressure

ضبط highWaterMark: متى يفيد ومتى يضر

ممكن تكبّر الـ highWaterMark لو عايز throughput أعلى:

JavaScript
const dest = fs.createWriteStream('./upload.bin', {
  highWaterMark: 1024 * 1024 // 1MB بدل 64KB الافتراضي
});

الـ trade-off: بتكسب سرعة كتابة أعلى (أقل عدد لـ context switches)، بتخسر استهلاك ذاكرة لكل اتصال متفتح. لو السيرفر بيخدم 200 upload متوازي وكل واحد بـ 1MB buffer، ده 200MB رام إضافي مخصصة للـ buffers قبل أي بيانات تطبيقية.

القاعدة: ابدأ بالقيمة الافتراضية. لما تشتغل تحت حمل حقيقي، قِس عدد الـ drain events ولو لاقيتها بتحصل آلاف المرات في الثانية، كبّر الـ highWaterMark بمقدار 4×.

متى لا تستخدم streams أصلًا

الـ streams مش الحل لكل حاجة:

  • ملفات صغيرة (< 1MB): fs.readFile أبسط وأسرع. الستريم overhead.
  • JSON parsing لكامل البيانات قبل أي عملية: لازم تستنى الكل ينزل، فالستريم مش بيوفّر حاجة.
  • Transform خفيف على بيانات صغيرة: Buffer.from(...) ومعالجة مباشرة أوضح.

الستريمات بتلمع في 3 سيناريوهات: ملفات أكبر من 10MB، خطوط بيانات (logs/CSV)، أو proxying بين شبكتين.

الخطوة التالية

افتح أي endpoint عندك بيستقبل upload أو يقرأ ملف كبير، ودوّر على req.on('data', ...) أو .pipe( بدون pipeline. استبدلهم بـ stream/promises.pipeline() النهارده، وقِس process.memoryUsage().heapUsed قبل وبعد رفع ملف 200MB. لو الفرق أكتر من 3×، ابعتلي الأرقام.

المصادر

  • Node.js Official: Backpressuring in Streams
  • Node.js API Docs: stream.pipeline()
  • Node.js API Docs: Stream Buffering & highWaterMark
  • Node.js Source: Maintaining Streams

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة