نشتی حافظه استریمهای Node.js در پروداکشن و راه فرارش
خلاصهٔ کاملتر
این مقاله قسمت دوم یه مجموعهٔ دوقسمتیه. نویسنده میگه قسمت اول مدل ذهنی بکپرشر، اینکه چرا highWaterMark یه تور نجات نیست، و مهاجرت از .pipe() به pipeline() رو پوشش داده بود و بحران فوری حل شده بود. ولی به گفتهٔ نویسنده یه سری حالتهای خرابی هستن که حتی با هندلکردن درستِ بکپرشر هم باقی میمونن، چون تو تستها (ماشین سریع لوکال، دیتای کوچیک، کلاینتی که هیچوقت قطع نمیشه) خودشونو نشون نمیدن.
اولین الگو اینه که کلاینت میره ولی سرور نمیفهمه. کاربر یه خروجی CSV بزرگ شروع میکنه و بعد تب مرورگر رو میبنده. با .pipe() قدیمی، کوئری دیتابیس آپاستریم همچنان اجرا میشه و حافظه بالا میره، چون .pipe() فقط روی رویداد finish تیردان رو راه میندازه که روی اتصال قطعشده هیچوقت فایر نمیشه. راهحل اینه که از pipeline() استفاده کنی که خودش مقصد رو مانیتور میکنه و موقع قطع زودهنگام، خطای ERR_STREAM_PREMATURE_CLOSE میده و کرسر دیتابیس آپاستریم رو خودکار نابود میکنه.
الگوهای دوم و سوم هم همین جنسان. تو الگوی دوم، با لیسنرهای قدیمیِ رویداد data، خروج زودهنگام از خوندن استریم سخت و پر از نشتی بود؛ نویسنده میگه async iteratorها (for await...of) امنترین راهن، چون موقع break/return/throw، خود جاوااسکریپت متد .return() ایتریتور رو صدا میزنه که Node اونو به stream.destroy() مپ میکنه:
for await (const line of fileStream) {
if (line.includes("ERROR")) {
console.log("Found error!");
break; // stream.destroy() خودکار صدا زده میشه
}
}الگوی سوم اینه که تایماوت فقط ریسپانس رو میبنده ولی بقیه رو نه. وقتی فریمورک با res.end() ریسپانس رو میبنده، این یه بستن مودبانهست و خطا نمیده، پس pipeline() اونو خرابی نمیبینه و زنجیرهٔ آپاستریم (مثل fetch از یه API بیرونی یا کرسر دیتابیس) همچنان بازه. راهحل، استفاده از AbortSignal.timeout() هست که موقع تمومشدن مهلت، کل زنجیره رو نابود میکنه و خطاش کد ABORT_ERR داره.
الگوی چهارم یه ایراد معماریه: گرهزدن تیردان دیتابیس به سرعت شبکه. خیلیا کانکشن دیتابیس رو وقتی ریسپانس HTTP تموم میشه آزاد میکنن، ولی اگه کاربر روی 3G کند پنج دقیقه طول بکشه دانلود کنه، تو پنج دقیقه یه ورکر دیتابیس رو الکی نگه داشتی و این به قحطی کانکشن منجر میشه. نویسنده میگه آپاستریم رو از دلیوری دانستریم جدا کن: کلینآپ رو به رویداد close/end خود کرسر ببند (یا با stream.finished()) تا لحظهای که آخرین ردیف اومد، کانکشن برگرده به pool. الگوی پنجم هم اینه که حتی با pipeline() درست، بین لحظهٔ خطا و رسیدن سیگنال نابودی به سورس، سورس میتونه چند چانک دیگه پوش کنه (چون destroy() ناهمگامه)؛ راهحل اینه که تو بلوک catch بهصورت صریح هم source.destroy(err) رو صدا بزنی (که اگه از قبل نابود شده باشه بیاثره).
در آخر نویسنده پلیبوک پنجقانونهٔ خودشو جمع میکنه: همیشه pipeline() بهجای .pipe()؛ احترام به مقدار بولینِ .write() و انتظار برای رویداد drain (با احتیاط نسبت به تلهٔ «drain hang» که میتونه برای همیشه معلق بمونه)؛ هرچی باز کردی خودت نابودش کن و به garbage collection تکیه نکن؛ قبل از پروداکشن پروفایل بگیر (مثلاً با --max-old-space-size=128 و گرفتن heap snapshot، یا لاگکردن stream.writableLength برای دیدن لحظهای که صف از کنترل خارج میشه)؛ و خطاها رو هندل کن.
نکات کلیدی:
- بعضی نشتیهای استریم فقط زیر ترافیک واقعی پیدا میشن، نه تو تست لوکال
- .pipe() روی قطع اتصال کلاینت تیردان نمیکنه؛ pipeline() این کارو خودکار انجام میده
- for await...of امنترین راه مصرف استریمه، چون موقع break خودکار destroy() صدا میزنه
- res.end() یه بستن مودبانهست؛ برای کشتن کل زنجیره از AbortSignal.timeout() استفاده کن
- لایفسایکل دیتابیس رو به سرعت شبکهٔ کلاینت گره نزن؛ کلینآپ رو به خود کرسر ببند
- صف بکپرشر رو با stream.writableLength مانیتور کن تا لحظهٔ نشتی رو ببینی




