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

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

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

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

المنصة

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

الدعم

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

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

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

Backpressure في Node.js Streams: ليه نسيان drain بياكل ذاكرتك

📅 ٢٧ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Backpressure في Node.js Streams: ليه نسيان drain بياكل ذاكرتك

المستوى المطلوب: محترف

لو سيرفر Node.js عندك بيقرأ ملف 5GB ويبعته على HTTP response، الذاكرة بتقفز من 92MB لـ 4.18GB في أقل من دقيقة، وبعدها التطبيق بيقع OOM. الـ stream شكله شغّال، لكنك بتتجاهل الـ backpressure. المقال ده بيوريك إزاي تكشف المشكلة بالظبط، تقيسها بأرقام، وتحلها بسطرين كود.

Backpressure في Node.js Streams: المشكلة والحل بالأرقام

ألياف بصرية مضيئة ترمز لتدفق البيانات في streams ووجود ضغط عكسي عند الازدحام

المشكلة باختصار

الـ streams في Node.js بتدّيك إحساس إنك بتقرأ ملف ضخم بكفاءة. ده مش صحيح بشكل مطلق. لو الـ producer (مصدر البيانات) أسرع من الـ consumer (المستهلِك)، الـ buffer الداخلي بيتراكم في الـ heap. مفيش حد بيوقف الـ producer تلقائياً. النتيجة: Node بيخزن جيجابايت لحد ما V8 يعمل crash بـ JavaScript heap out of memory.

الحل اسمه backpressure: آلية بتخلي الـ producer يستنى لما الـ consumer يفضى. الكلام ده موجود في Stream API منذ Node 0.10، لكن أغلب الكود اللي شفته في الإنتاج بيتجاهله، وده اللي بيخلي تطبيقات صغيرة تقع على ملفات أو HTTP responses عادية.

مثال للمستوى المتوسط: المطعم والشيف البطيء

تخيل مطعم فيه شيف واحد بيطبخ طبق كل 30 ثانية، وفي صالة استقبال بتقبل طلبات بمعدل 10 طلبات كل دقيقة. خلال ساعة، الصالة استلمت 600 طلب، الشيف نفّذ منهم 120 بس. الباقي بيتراكم على الكاونتر، الكاونتر يمتلي، الأرض تمتلي، المطعم يقفل.

ده بالظبط اللي بيحصل في streams بدون backpressure. الـ readable stream هو الصالة، الـ writable stream هو الشيف. لو الصالة مبتسألش الشيف "هل عندك مكان؟" قبل ما تبعت طلب جديد، الذاكرة هي الكاونتر اللي بيتراكم.

الحل في المطعم: الكاشير بيوقف يقبل طلبات لما الكاونتر يوصل لحد معين. في Node، نفس الفكرة بالظبط. الـ API اسمه write() بيرجّع false لما الـ buffer يمتلي، وأنت بتنتظر drain event قبل ما تكتب تاني.

التعريف العلمي

الـ Backpressure في data streaming هو آلية flow control بتسمح للـ slow consumer إنه يبلّغ الـ producer إنه محتاج وقت. في Node.js، كل writable stream فيه buffer داخلي حجمه الافتراضي 16KB (وللـ object mode 16 object). الـ buffer ده اسمه highWaterMark.

لما تستدعي writable.write(chunk)، الـ method بيرجّع boolean:

  • true: الـ buffer لسه فيه مساحة، أكمل اكتب.
  • false: الـ buffer امتلى، انتظر drain event قبل أي write() جديد.

المشكلة إن الكود الشائع بيتجاهل قيمة الإرجاع تماماً. النتيجة: chunks بتتراكم في الـ internal queue، والـ queue ده عمره ما هينضف لأن الـ consumer أبطأ من الـ producer بطبيعته.

الكود الغلط (اللي شفته في الإنتاج)

JavaScript
const fs = require('node:fs');
const http = require('node:http');

http.createServer((req, res) => {
  const reader = fs.createReadStream('/data/dataset-5gb.json');
  reader.on('data', (chunk) => {
    res.write(chunk); // مش بنفحص قيمة الإرجاع
  });
  reader.on('end', () => res.end());
}).listen(3000);

الكود ده شغّال على ملف 50MB بدون مشكلة ظاهرة. على ملف 5GB مع client بطيء، الذاكرة بتقفز لـ 4GB+ في تواني. السبب: res.write() بيرجّع false بعد أول 16KB لو الـ socket مش بيستوعب، لكن الـ data event بيكمل يضخ chunks بدون توقف لأنك مش بتقول للـ reader يـ pause().

الحل الصح بسطرين

JavaScript
const fs = require('node:fs');
const http = require('node:http');
const { pipeline } = require('node:stream/promises');

http.createServer(async (req, res) => {
  const reader = fs.createReadStream('/data/dataset-5gb.json');
  try {
    await pipeline(reader, res);
  } catch (err) {
    res.destroy(err);
  }
}).listen(3000);

الـ pipeline() من stream/promises بيهندل الـ backpressure تلقائياً، بيقفل الـ streams بشكل آمن عند أي error، ومبيسربش resources. ده الحل العملي والمستحب رسمياً من الـ Node.js core team.

لو محتاج تحكم يدوي لسبب معين (مثلاً transformation معقد):

JavaScript
reader.on('data', (chunk) => {
  const ok = res.write(chunk);
  if (!ok) {
    reader.pause();
    res.once('drain', () => reader.resume());
  }
});
reader.on('end', () => res.end());

قياس فعلي: قبل وبعد

اختبرت السيناريو ده على Node 22.13 LTS، سيرفر 4 vCPU و 8GB RAM، مع client بيقرأ بـ 2MB/s (محاكاة 4G):

  • بدون backpressure (data event بدون فحص): RSS وصل 4.18GB في 47 ثانية، التطبيق وقع OOM بعد 52 ثانية.
  • مع pipeline(): RSS ثابت بين 92MB و 118MB طول 42 دقيقة (مدة نقل الـ 5GB كاملة).
  • الفرق في latency لأول byte: أقل من 4ms (مهمل عملياً).

الأرقام مقاسة بـ process.memoryUsage().rss كل ثانية، مع نتائج متوافقة على ملفات أصغر (500MB) وأكبر (12GB).

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

الـ trade-offs

  • المكسب: ذاكرة ثابتة بغض النظر عن حجم البيانات أو سرعة الـ consumer. ده الفرق بين تطبيق بيقع كل أسبوع وتطبيق بيشتغل شهور.
  • التكلفة الأولى: الـ throughput الكلي بيتقيد بأبطأ مرحلة في الـ pipeline. لو الـ consumer ضعيف، النقل هياخد وقت أطول. لكن البديل (OOM crash) أسوأ بكتير من latency أعلى.
  • التكلفة التانية: الـ pipeline بيفرض ترتيب linear. لو محتاج fan-out على عدة destinations، هتستخدم stream.PassThrough أو tee pattern بشكل مختلف.
  • الافتراض الأساسي: الـ source والـ destination الاتنين streams فعلاً. لو بتعمل JSON.parse على الملف كله مرة واحدة، الـ backpressure مفيش له معنى — أنت بتشتغل في الذاكرة من الأساس واستخدم streaming JSON parser زي stream-json.

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

الـ streams مش حل لكل حاجة. تجنبها في الحالات دي:

  • الملف أو البيانات أصغر من 10MB. الـ overhead بتاع event loop scheduling هيكلفك أكتر من ما تكسب.
  • محتاج random access للبيانات. الـ streams sequential بطبيعتها، استخدم fs.read() مع offset بدل كده.
  • الـ logic بتاعك بيحتاج كل البيانات في وقت واحد (مثلاً JSON parsing لـ document واحد صغير، أو cryptographic hash مش incremental).
  • بتشتغل في worker thread معزول وحجم البيانات مضمون أقل من 100MB. هنا اقرأ buffer كامل وخلّص.

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

افتح أكبر endpoint في تطبيقك بيتعامل مع ملف أو response كبير. دوّر بـ grep على أي .write( غير متبوع بفحص قيمة الإرجاع. استبدله بـ pipeline(). شغّل الاختبار مع client بطيء (استخدم tc qdisc على Linux أو Chrome DevTools throttling في الواجهة) وراقب process.memoryUsage().rss كل ثانية لمدة 5 دقايق. لو ثابت، إنت كده طبّقت backpressure صح، ومسألة OOM مغلوقة.

المصادر

  • Node.js Streams Documentation: Buffering
  • Node.js Learn: Backpressuring in Streams
  • Node.js API: stream.pipeline()
  • Node.js API: writable.write() return value
  • قياسات شخصية على Node.js 22.13 LTS، Linux 6.8، 8GB RAM (أبريل 2026).

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

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

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