المستوى: محترف
Stream Backpressure في Node.js: ليه pipe() بياكل 4GB ذاكرة على ملف 200MB
لو شغّلت سكربت بسيط بيقرأ ملف 200MB ويضغطه ويكتبه على S3، ولقيت Node.js بياكل 4.2GB ذاكرة قبل ما يقع بـ JavaScript heap out of memory، المشكلة مش في حجم الملف. المشكلة إن الـ Readable بيقرأ بسرعة 480MB/s والـ Writable بيكتب بسرعة 22MB/s فقط. الفرق بيتراكم في buffer داخلي. الحل اسمه Backpressure، وموجود في Node.js من الإصدار 0.10، لكن أغلب السكربتات بتتجاهله من غير ما تحس.
المشكلة باختصار
Node.js Streams بيشتغلوا بمنطق push-based افتراضياً. الـ Readable بيرمي بيانات على الـ Writable من غير ما يسأل لو الـ consumer خلّص اللي قبله. النتيجة: لو الـ producer أسرع من الـ consumer، البيانات بتتكدّس في الذاكرة. على ملف 200MB والفرق 22x في السرعة، الـ buffer ممكن يوصل لـ 4GB+ في 9 ثواني.
تمثيل تقريبي للمبتدئ: عامل المخزن
تخيّل عامل في مخزن بيشيل صناديق من شاحنة كبيرة ويحطّها في شاحنة صغيرة. الشاحنة الكبيرة بتفرّغ 10 صناديق في الدقيقة، لكن الصغيرة ما تشيلش غير 2 في الدقيقة. لو العامل ما وقّفش الشاحنة الكبيرة، الصناديق هتتكدّس في الأرض حواليه لحد ما المخزن يطفح. Backpressure هي بالظبط الإشارة اللي العامل بيرسلها للشاحنة الكبيرة: "وقفي شوية، لسه بحمّل اللي قبل كده". في Node.js نفس الفكرة: الـ Writable بيقول للـ Readable "بطّل قراءة، الـ buffer وصل للحد".
التعريف العلمي الدقيق
Backpressure هو الميكانيزم اللي بيخلّي الـ consumer (Writable stream) يبلّغ الـ producer (Readable stream) إنه بقى مشغول ولازم يبطّئ إنتاج البيانات. في Node.js Streams، الـ writable.write() بيرجّع قيمة boolean: لو رجّع true يعني الـ internal buffer لسه فيه مساحة، ولو رجّع false يعني الـ buffer وصل للـ highWaterMark (الافتراضي 16KB في object mode، 64KB في binary mode).
طبقاً لتوثيق Node.js الرسمي على nodejs.org/api/stream.html#buffering، لما write() يرجّع false، الـ Readable لازم يستنّى event اسمه drain قبل ما يكمل القراءة. الـ readable.pipe(writable) بيدير ده تلقائياً، لكن لو إنت بتربط الـ streams يدوياً بـ on('data') و write()، إنت بتدوس على Backpressure من غير ما تحس وذاكرتك بتنفجر.
الحل القابل للنسخ
الحل الأنظف هو استخدام stream.pipeline() من Node 10+. pipeline بيدير الـ backpressure والـ error propagation والـ cleanup بشكل صحيح. ممنوع تنسى انك تتعامل مع الـ promise rejection لأنه أي error في أي stream بيتنشر للأعلى.